数据类型
对象包装器
针对原始类型
原始类型理论上只提供单个原始值,但却允许访问字符串、数字、布尔值和symbol的属性和方法,这是因为进行了额外的操作:使用对象包装器,使用后即被销毁。
- 字符串包装器:
String - 数字包装器:
Number - 布尔包装器:
Boolean - Symbol包装器:
Symbol - 大整型包装器:
BigInt
let str = "Hello";
console.info(str.toUpperCase()); // HELLO
// 相当于
console.info(String(str).toUpperCase());
/** 执行过程:
* 访问原始值属性时,会创建一个包含字符串字面值的特殊对象,并且具有可用的方法
* 该方法运行并返回一个新的字符串
* 特殊对象被销毁,只留下原始值 str
*/
// 数字类型
console.info(1.23123.toFixed(3)); // 1.231
// 但类型会变为字符串
console.info(typeof 1.23123.toFixed(3)); // string
包装器构造器仅供内部使用:
因为存在歧义
// "number"
console.info(typeof 0);
// "object"!
console.info(typeof new Number(0));
// 如果使用if进行判断,则
if (new Number(0)) {
// 完成打印 true
console.info("true");
}
// 此外不带 new 进行使用
// 将字符串转成数字
let num = Number("123");
null和undefined无任何方法,真正的原始值
数字类型
在所有数字函数中,包括 isFinite,空字符串或仅有空格的字符串均被视为 0
精确度丢失
现代JS中存在两种数字类型:
- 采用IEEE-754 规则
64位格式存储,即双精度浮点数- 对于任意长度的大整数采用
BigInt进行表示,这是因为第一种整数在(2**53 - 1)到-(2**53 - 1)范围外存在精确度问题。
IEEE 754 双精度浮点数的表示方式:
- 1位符号位(S):表示数字的正负
- 11位指数位(Exponent):表示数字的指数部分
- 52位尾数位(Mantissa 或 Fraction):表示数字的有效数字部分
- 因此最大整数
Number.MAX_SAFE_INTEGER是2**53−1
// 数据过大,变成Infinity true
console.info(2**53) == (2**53 + 1);
// 超出最大安全表示,自动+1
// 10000000000000000
console.info(9999999999999999);
// 注意!!!
// true
console.info(0 === -0);
0.1、0.2实际上在二进制形式中是无限循环小数,因此在二进制中无法精确存储。
// 小数位不精确 false
console.info(0.1 + 0.2 == 0.3);
IEEE-754 解决方法:通过将数字舍入到最接近的可能数字来解决此问题
默认返回字符串,可使用一元加法转换
此外将小数扩大到整数再计算,完成后再缩小一定程度上可以减少误差,但并不会消除
// 0.3
console.info(+(0.1 + 0.2).toFixed(1));
数字编写方法
let billion = 1000000000;
let mcs = 0.000001;
// 添加分隔符
let billion = 1_000_000_000;
// 科学计数法
let billion = 1e9;
let million = 1.23e6;
let mcs = 1e-6;
// 十六进制
console.info(0xff); // 255
// 小写没影响
console.info(0xFF); // 255
// 八进制
console.info(0o377); // 255
// 二进制
console.info(0b11111111); // 255
// 不同进制可以直接比较
console.info(0b11111111 == 0o377); // true
toString(base)
返回在给定 base 进制数字系统中 num 的字符串表示形式
不建议直接放置在数字后,第一个点会被标记为小数部分,或者
使用两个点:123..toString(8)
let num = 255;
// ff
console.info(num.toString(16));
// 11111111
console.info(num.toString(2));
常见用例:
- base=16 用于十六进制颜色,字符编码等,数字可以是 0..9 或 A..F。
- base=2 主要用于调试按位操作,数字可以是 0 或 1。
- base=36 是最大进制,数字可以是 0..9 或 A..Z。所有拉丁字母都被用于了表示数字,可以用于长标识符转换为短标识符
舍入
| 函数/值 | 功能描述 | 3.1 | 3.6 | -1.1 | -1.6 |
|---|---|---|---|---|---|
Math.floor | 向下舍入 | 3 | 3 | -2 | -2 |
Math.ceil | 向上舍入 | 4 | 4 | -1 | -1 |
Math.round | 向最近的整数舍入 | 3 | 4 | -1 | -2 |
Math.trunc | 移除小数部分 | 3 | 3 | -1 | -1 |
舍入到小数点后n位:
- 乘除法
let num = 1.23456;
// 1.23456 -> 123.456 -> 123 -> 1.23
console.info(Math.round(num * 100) / 100);
- toFixed
let num = 12.34;
// "12.3"
console.info(num.toFixed(1));
按位取反
将数字转换为 32-bit 整数(如果存在小数部分,则删除小数部分),然后对其二进制表示形式中的所有位均取反。
简单来说:对于 32-bit 整数,~n 等于 -(n+1)
console.info(~2); // -3,和 -(2+1) 相同
console.info(~1); // -2,和 -(1+1) 相同
console.info(~0); // -1,和 -(0+1) 相同
console.info(~-1); // 0,和 -(-1+1) 相同
特殊值判断
isNaN:用于判断NaN(表示计算错误)特殊值
NaN === NaN为false,无法和自身判断isFinite:用于判断Infinity和-Infinity(正无穷和负无穷)
// true
console.info(isNaN(NaN));
// true
console.info(isNaN("str"));
// true
console.info(isFinite("15"));
// false,特殊值NaN
console.info(isFinite("str"));
// false,特殊值Infinity
console.info(isFinite(Infinity));
字符串数字解析
在隐式转换时,对于非纯数字字符串会转换失败(字符串开头或结尾的空格会被忽略)
console.info(+"100px"); // NaN
parseInt和parseFloat:
从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。
// 100
console.info(parseInt('100px'));
// 12.5
console.info(parseFloat('12.5em'));
// 12,只有整数部分被返回了
console.info(parseInt('12.3'));
// 12.3,在第二个点出停止了读取
console.info(parseFloat('12.3.4'));
// NaN,第一个符号停止了读取
console.info(parseInt('a123'));
// parseInt允许指定数字基数radix作为第二个参数
// 255
console.info(parseInt('0xff', 16));
// 255,没有 0x 仍然有效
console.info(parseInt('ff', 16));
// 123456
console.info(parseInt('2n9c', 36));
其余数学函数
详见:Math 对象文档
// 0-1随机数(不包含1)
console.info(Math.random());
console.info(Math.random());
// 返回最大值 5
console.info(Math.max(3, 5, -10, 0, 1));
// 返回最小值 1
console.info(Math.min(1, 2));
// 给定次幂 1024
console.info(Math.pow(2, 10));
字符串
字符串编写方式
- 单引号:
let single = 'single-quoted'; - 双引号:
let double = "double-quoted"; - 反引号:
// 允许换行和嵌入表达式 let backticks = `backticks`; let guestList = `Guests: * John * Pete * Mary `; function sum(a, b) { return a + b; } console.info(`1 + 2 = ${sum(1, 2)}.`);
特殊转义字符
| 字符 | 描述 |
|---|---|
\n | 换行 |
\r | 在 Windows 文本文件中,两个字符 \r\n 的组合代表一个换行。而在非 Windows 操作系统上,它就是 \n。这是历史原因造成的,大多数的 Windows 软件也理解 \n。 |
\', \" | 单引号和双引号 |
\\ | 反斜线 |
\t | 制表符 |
\b, \f, \v | 退格,换页,垂直标签 —— 为了兼容性,现在已经不使用了。 |
\xXX | 具有给定十六进制 Unicode XX 的 Unicode 字符,例如:\x7A 和 z 相同。 |
\uXXXX | 以 UTF-16 编码的十六进制代码 XXXX 的 Unicode 字符,例如 \u00A9 —— 是版权符号 © 的 Unicode。它必须正好是 4 个十六进制数字。 |
\u{X…XXXXXX} | 具有给定 UTF-32 编码的 Unicode 符号。一些罕见的字符用两个 Unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。 |
字符串属性
字符串长度:
// 3
console.info(`My\n`.length);
获取单个字符:
区别:当没有找到字符时,
[ ] 返回 undefined
charAt 返回一个空字符串
let str = `Hello`;
// 第一个字符
console.info(str[0]); // H
console.info(str.charAt(0)); // H
// 最后一个字符
console.info(str[str.length - 1]); // o
遍历:
for (let char of "Hello") {
// 依次 H,e,l,l,o
console.info(char);
}
字符串一旦创建不可变!!!
let str = 'Hi';
// 修改不生效
// 严格模式会报错
str[0] = 'h';
// Hi 并没有被修改
console.info(str);
function strReadOnly() {
"use strict";
str = "qwe";
str[0] = "e";
console.info(str);
console.info(str[0]);
}
// TypeError: 0 is read-only
strReadOnly();
现代方法:includes,startsWith,endsWith
includes(substr, pos):根据 str 中是否包含 substr 来返回 true/false
// true
console.info("Widget with id".includes("Widget"));
// false
console.info("Hello".includes("Bye"));
startsWith,endsWith:判断起始或结尾是否为特定字符
// true
console.info("Widget".startsWith("Wid"));
// true
console.info("Widget".endsWith("get"));
获取子字符串
str.slice(start [, end]):返回字符串从 start 到(但不包括)end 的部分。此方法足够日常使用
let str = "stringify"; // 'strin' // 如果不设置第二个参数,则默认匹配到末尾 console.info(str.slice(0, 5)); // 也有可能是负值。是起始位置从字符串结尾计算 // 'gif' console.info(str.slice(-4, -1));str.substring(start [, end]):与slice基本相同,但允许start大于end,但不支持负数参数let str = "stringify"; // 这些对于 substring 是相同的 // "ring" console.info(str.substring(2, 6)); // "ring" 但slice则返回 空字符串 "" console.info(str.substring(6, 2));str.substr(start [, length]):返回从start开始的给定length的部分let str = "stringify"; // 'ring' console.info( str.substr(2, 4) ); // 'gi' 从末尾往后取 console.info( str.substr(-4, 2) );
从码点获取字符
let str = '';
for (let i = 65; i <= 220; i++) {
str += String.fromCodePoint(i);
}
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
// ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ
console.info( str );
// 获取字符的码点位置
// 不同的字母有不同的代码
console.info( "z".codePointAt(0) ); // 122
console.info( "Z".codePointAt(0) ); // 90
字符串比较
常规字符串按照字母顺序逐字比较。
变音符号字母会存在乱序情况。
// 使用默认比较下
// true 但实际情况应该为false
console.info( 'Österreich' > 'Zealand' );
正确比较方法:str.localeCompare(str2)
- 如果 str 排在 str2 前面,则返回负数。
- 如果 str 排在 str2 后面,则返回正数。
- 如果它们在相同位置,则返回 0。
// -1
console.info( 'Österreich'.localeCompare('Zealand') );
更多属性方法:MDN手册
Unicode
了解即可
// ©
console.info("\u00A9");
// 𠌱,罕见的中国象形文字(长 Unicode)
console.info("\u{20331}");
// 😍,笑脸符号(长 Unicode)
console.info("\u{1F60D}");
大小写转换
// INTERFACE
console.info('Interface'.toUpperCase());
// interface
console.info('Interface'.toLowerCase());
// 'i'
console.info('Interface'[0].toLowerCase());
查找子字符串:str.indexOf
str.lastIndexOf(substr, pos):从末尾开始查询
let str = 'Widget with id';
// 0,因为 'Widget' 一开始就被找到
console.info(str.indexOf('Widget'));
// -1,没有找到,检索是大小写敏感的
console.info(str.indexOf('widget'));
// 1,"id" 在位置 1 处(……idget 和 id)
console.info(str.indexOf("id"));
// 允许从一个给定的位置开始检索
// 12 此时检索到最后的id
// 可以借助这个参数配合循环找到所有的匹配字符串
console.info(str.indexOf('id', 2));
代理对:
常用字符都是2字节,但只存在65536个组合,所以稀有的符号被称为“代理对”的一对 2 字节的符号编码。
代理对被认为是2个字符,代理对的各部分没有任何意义。
// 2,大写数学符号 X
console.info('𝒳'.length);
// 2,笑哭表情
console.info('😂'.length);
// 2,罕见的中国象形文字
console.info('𩷶'.length);
处理代理对方法:
String.fromCodePointstr.codePointAt
变音符号:
// Ṡ
console.info('S\u0307');
// Ṩ
console.info('S\u0307\u0323');
// 视觉相同但实际不同
let s1 = 'S\u0307\u0323'; // Ṩ,S + 上点 + 下点
let s2 = 'S\u0323\u0307'; // Ṩ,S + 下点 + 上点
console.info(`s1: ${s1}, s2: ${s2}`);
// false
console.info(s1 == s2);
规范化处理:str.normalize()
实际上将一个由 3 个字符组成的序列合并为一个
// 1
console.info("S\u0307\u0323".normalize().length);
// true
console.info("S\u0307\u0323".normalize() == "S\u0323\u0307".normalize());
Unicode 标准附录:Unicode 规范化形式
数组及方法
数组:存储有序数据的集合。
创建和下标获取元素
创建
// 空数组
let arr = new Array();
let arr = [];
// 带有初始化数据数组
let fruits = ["Apple", "Orange", "Plum"];
// 或者
let fruits = [
"Apple",
"Orange",
"Plum",
];
// 数组对元素类型无限制
let arr = [ 'Apple', { name: 'John' }, true, function() { console.info('hello'); } ];
下标获取数组元素
let arr = [ 'Apple', { name: 'John' }, true, function() { console.info('hello'); } ];
// 普通下标,不支持负索引
// 直接使用负索引 [-1]这种会返回undefined
console.info( arr[1].name ); // John
// 获取索引为 3 的函数并执行
arr[3](); // hello
// 使用at 方法,支持负索引
console.info(arr.at(-3)); // 'Apple'
console.info(arr.at(1).name); // John
arr.at(-1)(); // hello
前置知识补充
队列:FIFO(先进先出)
- push:在末端添加一个元素.
- shift:取出队列首端的一个元素,整个队列往前移
栈:LIFO(后进先出)
- push:在末端添加一个元素.
- pop:从末端取出一个元素.
双端队列:允许从首端/末端来添加/删除元素,支持队列和栈的操作
- pop:取出并返回数组的最后一个元素
- push:在数组末端添加元素(支持一次添加多个元素)
- shift:取出数组的第一个元素并返回
- unshift:在数组的首端添加元素(支持一次添加多个元素)
数组本质
仍然为对象
使用arr[0]语法其实为对象操作(对象+key),不过key为有序数列。
虽然可以向对待普通对象一样处理数组,但这样会使得引擎对数组的优化失效。
失效情形:
- 非数字属性
- 数据空洞,不连续
- 倒叙填充数组
let fruits = [];
// 数据断层,不连续(即数组并非真的有99999个元素)
fruits[99999] = 5;
// 非数字属性
fruits.age = 25;
// 先填充99 再填充98
fruits[99] = 5;
fruits[98] = 1;
性能
push/pop 方法运行的比较快,而 shift/unshift 比较慢。
原因:
shift操作
- 移除0号索引元素
- 所有元素左移,补充0号位置
- 更新length
unshift也一样,不过是新增右移元素
push/pop仅在末尾操作,不需要移动元素。数组元素越多,移动所花时间越长,操作内存越多。
循环
- 古老方式:
for
let arr = ["Apple", "Orange", "Pear"];
for (let i = 0; i < arr.length; i++) {
console.info( arr[i] );
}
for of
let fruits = ["Apple", "Orange", "Plum"];
// 遍历数组元素
for (let fruit of fruits) {
console.info( fruit );
}
for in
由于本质为对象,方法通用
但会遍历所有属性,且存在性能问题,仅对对象做优化,不适用于数组,慢10-100 倍
let arr = ["Apple", "Orange", "Pear"];
for (let key in arr) {
console.info( arr[key] ); // Apple, Orange, Pear
}
length
技术上并非数组长度,其实为最大索引值+1,在常规状态下由于正常添加元素其自动更新,因此表现为数组的长度。
let fruits = [];
fruits[123] = "Apple";
console.info( fruits.length ); // 124
!!!length 为可写!!!
手动增加length的值不会对数组本身产生影响,最多使得引擎优化失效,但如果减少则会对数组进行截断,造成数据丢失。
但反过来可以将length置为0达到清空数组的目的。
尽量避免使用
let arr = new Array(2);
这种方法创建数组,并不会使得arr[0]为2
而是创建了一个长度为2的数组,但内容为未定义。
多维数组
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 转为字符串自动铺平
// "1,2,3,4,5,6,7,8,9"
console.info(String(matrix));
toString
console.info( [] + 1 ); // "1"
console.info( [1] + 1 ); // "11"
console.info( [1,2] + 1 ); // "1,21"
原因:
[]是数组对象,与原始值计算,先转为原始值,其对应原始值为""- 字符串与数字计算,则为字符串拼接
[1]与[1,2]同理,分别返回字符串"1","1,2"
相等性比较
数组的比较参考对象之间的比较,
- 仅同为一个对象的引用时,使用
==才相等 - 一边对象,一边原始值,会触发对象转为原始值
null == undefined相等外,各自不等于任何其他的值
数组方法
在数组中操作元素
arr.splice(start[, deleteCount, elem1, ..., elemN]):允许添加、删除和插入元素
从索引 start (支持为负数,从末尾开始索引)开始修改 arr:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回组合后的新数组。插入元素时,deleteCount 必须填写,不需要删除则置为0
let arr = ["I", "study", "JavaScript"];
// 从索引 2 开始
// 删除 0 个元素
// 然后插入 "complex" 和 "language"
arr.splice(2, 0, "complex", "language");
// "I", "study", "complex", "language", "JavaScript"
console.info( arr );
arr.slice([start], [end]):支持获取指定长度的子数组
从索引 start 到 end(不包括 end)的数组项复制到一个新的数组并返回,start 和 end 都支持负数。start > end 时返回空数组。不使用任何参数则返回此数组的副本。
let arr = ["t", "e", "s", "t"];
// e,s(复制从位置 1 到位置 3 的元素)
console.info( arr.slice(1, 3) );
// s,t(复制从位置 -2 到尾端的元素)
console.info( arr.slice(-2) );
arr.concat(arg1, arg2...):将多个数组或值压缩到一个数组中
如果参数 argN 是一个数组,那么其中的所有元素都会被复制。否则,将复制参数本身(支持任何类型数据,如果是类数组对象,且存在Symbol.isConcatSpreadable 属性,则当作数组进行对待)。
let arr = [1, 2];
// 从 arr、[3,4] 和 [5,6] 创建一个新数组
// 1,2,3,4,5,6
console.info( arr.concat([3, 4], [5, 6]) );
// 从 arr、[3,4]、5 和 6 创建一个新数组
// 1,2,3,4,5,6
console.info( arr.concat([3, 4], 5, 6) );
arr.forEach 遍历:允许为数组的每个元素都运行一个函数
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
console.info(`${item} is at index ${index} in ${array}`);
});
在数组中搜索
查找特定元素下标:
arr.indexOf(item, from):从索引 from 开始搜索 item,如果找到则返回索引,否则返回 -1。
判断特定元素是否存在:
arr.includes(item, from):从索引 from 开始搜索 item,如果找到则返回 true,否则返回false。
indexOf和includes内部都为===严格判断,但includes可以正确判断NaN的情况,而indexOf不可以。
arr.lastIndexOf为从末尾查找
具有特定条件的数组对象元素:
arr.find:返回 true,则返回 item 并停止迭代,返回false则返回undefined。
let result = arr.find(function(item, index, array) {
// item 是元素。
// index 是它的索引。
// array 是数组本身。
});
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let user = users.find(item => item.id == 1);
console.info(user.name); // John
arr.findIndex/arr.findLastIndex:与arr.find同理,但返回下标,或者从右搜索。
过滤符合特定条件的数据元素:
arr.filter(fn):语法与 find 大致相同,但是 filter 返回的是所有匹配元素组成的数组。
let results = arr.filter(function(item, index, array) {
// 如果 true item 被 push 到 results,迭代继续
// 如果什么都没找到,则返回空数组
});
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// 返回前两个用户的数组
let someUsers = users.filter(item => item.id < 3);
console.info(someUsers.length); // 2
数组元素转换
arr.map:对数组的每个元素都调用函数,并返回结果数组。
let result = arr.map(function(item, index, array) {
// 返回新值而不是当前元素
})
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
console.info(lengths); // 5,7,6
arr.sort:对数组进行原地排序,原数组修改(默认按照字符串规则进行排序,需要手动指定规则函数)
规则函数原则:
- 如果第一个值比第二个值大,返回正值
- 两个值相等,返回0
- 第一个值比第二个值小,返回负值
// [ -2, 0, 1, 2, 8, 15 ]
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
return a - b;
});
arr.reverse:倒置数组元素
let arr = [1, 2, 3, 4, 5];
arr.reverse();
console.info( arr ); // 5,4,3,2,1
数组拼接字符串字符串分割为数组:
str.split(delim):通过给定的分隔符 delim 将字符串分割成一个数组。
let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', ');
// [ "Bilbo", "Gandalf", "Nazgul" ]
console.info(arr);
// 也可以限制返回的数组长度
arr = names.split(', ', 2);
// [ "Bilbo", "Gandalf" ]
console.info(arr);
arr.join(glue):创建一串由 glue 粘合的 arr 项。
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
// 使用分号 ; 将数组粘合成字符串
let str = arr.join(';');
// Bilbo;Gandalf;Nazgul
console.info( str );
reduce/reduceRight:用于根据数组计算单个值。
reduceRight从末尾开始计算
let value = arr.reduce(function(accumulator, item, index, array) {
/*
accumulator —— 是上一个函数调用的结果,第一次等于 initial(如果提供了 initial 的话)。本质上是累加器,用于存储所有先前执行的组合结果,用于最终的结果。
item —— 当前的数组元素。
index —— 当前索引。
arr —— 数组本身。
*/
}, [initial]);
示例:
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce(
(sum, current) => sum + current,
0
);
console.info(result); // 15
区分对象和数组:
由于本质上都是对象,因此使用typeof无法进行区分
console.info(typeof {}); // object
console.info(typeof []); // object
使用Array.isArray:
console.info(Array.isArray({})); // false
console.info(Array.isArray([])); // true
arr.some(fn):检查数组中是否有任意一个元素满足函数 fn 的条件,若有则返回 true,否则返回 false。
let arr = [1, 2, 3, 4, 5];
let result = arr.some(value => value > 3);
console.info(result); // true
arr.every(fn):检查数组中所有元素是否都满足函数 fn 的条件,若都满足则返回 true,否则返回 false。
let arr = [1, 2, 3, 4, 5];
let result = arr.every(value => value > 0);
console.info(result); // true
实现一个数组相等判断:
function arraysEqual(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}
console.info( arraysEqual([1, 2], [1, 2])); // true
arr.fill(value, start, end):从索引 start 到 end,用重复的 value 填充数组。
let arr = [1, 2, 3, 4, 5];
arr.fill(0, 2, 4);
console.info(arr); // [1, 2, 0, 0, 5]
arr.copyWithin(target, start, end):将从位置 start 到 end 的所有元素复制到自身的 target 位置(覆盖现有元素)。
let arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3);
console.info(arr); // [4, 5, 3, 4, 5]
arr.flat(depth):从多维数组创建一个新的扁平数组,depth 指定扁平化的深度。
let arr = [1, [2, [3, [4, 5]]]];
let result = arr.flat(2);
console.info(result); // [1, 2, 3, [4, 5]]
arr.flatMap(fn):先对数组的每个元素调用函数 fn,然后将结果扁平化一层。
let arr = [1, 2, 3];
let result = arr.flatMap(value => [value, value * 2]);
console.info(result); // [1, 2, 2, 4, 3, 6]
Array.of(element0[, element1[, …[, elementN]]]):基于可变数量的参数创建一个新的 Array 实例,而不需要考虑参数的数量或类型。
let arr = Array.of(1, 2, 3, 4, 5);
console.info(arr); // [1, 2, 3, 4, 5]
数组详细知识链接:Array手册
可迭代对象
可以应用
for..of的对象被称为 可迭代的。
迭代器对象与被迭代对象
为普通对象添加一个Symbol.iterator 的方法(一个专门用于使对象可迭代的内建 symbol),即迭代器对象,即可使得普通对象成为可迭代的。
普通对象添加外置迭代器对象:
外置迭代器时,每个
for of都是独立迭代状态。
let range = {
from: 1,
to: 5
};
// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function() {
// ……它返回迭代器对象(iterator object):
// 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值
return {
current: this.from,
last: this.to,
// 3. next() 在 for..of 的每一轮循环迭代中被调用
next() {
// 4. 它将会返回 {done:.., value :...} 格式的对象
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// 现在它可以运行了!
for (let num of range) {
console.info(num); // 1, 然后是 2, 3, 4, 5
}
普通对象内置迭代器对象:
内置迭代器时,每个
for of都是共享迭代状态。
let range = {
from: 1,
to: 5,
current: 1,
[Symbol.iterator]() {
// 此语句会对每一个 for of 进行状态重置
// this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
console.info(num); // 1, 然后是 2, 3
if (num === 3) break;
}
// 迭代器状态共享
for (let num of range) {
console.info(num); // 4, 5
}
显式调用迭代器
创建迭代器,手动对可迭代对象进行迭代
let str = "Hello";
// 和 for..of 做相同的事
// for (let char of str) console.info(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
// { value: "H", done: false }
console.info(result);
}
// 此时再去继续迭代
let final = iterator.next();
// { value: undefined, done: true }
console.info(final);
可迭代与类数组
- 可迭代(Iterable):是实现了 Symbol.iterator 方法的对象。
- 类数组(Array-like):是有索引和 length 属性的对象(看起来很像数组)。
可迭代对象和类数组对象通常都 不是数组,它们没有 push 和 pop 等方法。
Array.from:接受一个可迭代或类数组的值,并从中获取一个“真正的”数组。
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike); // (*)
console.info(arr.pop()); // World(pop 方法有效)
// range 为具有可迭代属性的对象
let arrByIterable = Array.from(range);
// [ 1, 2, 3, 4, 5 ]
console.info(arrByIterable);
完整语法:Array.from(obj[, mapFn, thisArg])
- obj:可迭代对象或者类数组
- mapFn:映射函数
- thisArg:参数指向对象,默认即可
// range 为具有可迭代属性的对象
let arr = Array.from(range, num => num * num);
console.info(arr); // 1,4,9,16,25
代理对感知
由于迭代可以正确感知代理对,可以创建代理感知(surrogate-aware)的slice 方法(译注:也就是能够处理 UTF-16 扩展字符)
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
// 😂𩷶
// 原生方法不支持识别代理对(译注:UTF-16 扩展字符)
console.info( slice(str, 1, 3) );
// 乱码(两个不同 UTF-16 扩展字符碎片拼接的结果)
console.info( str.slice(1, 3) );
映射和集合
- 对象:键值对数据集合(Key仅支持String和Symbol)
- 数组:有序集合
- 映射:键值对数据集合(Key支持任何类型键,包括对象)
- 集合:值唯一集合
映射 Map
属性总览:
-
new Map():创建一个新的Map对象。let myMap = new Map(); -
map.set(key, value):根据键存储值。myMap.set('key1', 'value1'); // 使用对象 let john = { name: "John" }; myMap.set(john, 123);set方法每次调用后都会返回map对象自身,
因此支持进行链式调用:
map.set('1', 'str1').set(1, 'num1').set(true, 'bool1'); -
map.get(key):根据键来返回值,如果Map中不存在对应的键,则返回undefined。let value = myMap.get('key1'); // 返回 'value1' // 获取对象键对应的值 let objKeyValue = myMap.get(john); // 123如果在对象中将对象作为key,
则作为Key的对象会变为字符串"[object Object]",不再是对象自身。Map 使用 SameValueZero 算法进行 key 的判断,与 === 功能一致,但功能更加强大,能够进行 NaN 的判断,因此支持将 NaN 作为 key
-
map.has(key):如果键存在则返回true,否则返回false。let hasKey = myMap.has('key1'); // 返回 true -
map.delete(key):删除指定键的值。myMap.delete('key1'); // 删除键 'key1' -
map.clear():清空Map。myMap.clear(); // 清空整个 Map -
map.size:返回当前元素个数。console.log(myMap.size); // 输出 Map 的大小
Map 迭代:
迭代顺序与插入顺序一致
-
map.keys():遍历并返回一个包含所有键的可迭代对象。let myMap = new Map([['key1', 'value1'], ['key2', 'value2']]); for (let key of myMap.keys()) { console.log(key); // 输出:key1, key2 } -
map.values():遍历并返回一个包含所有值的可迭代对象。for (let value of myMap.values()) { console.log(value); // 输出:value1, value2 } -
map.entries():遍历并返回一个包含所有实体[key, value]的可迭代对象,for..of在默认情况下使用的就是这个。for (let [key, value] of myMap.entries()) { // 输出:key1 value1, key2 value2 console.log(key, value); } // for..of 默认行为 for (let [key, value] of myMap) { // 输出:key1 value1, key2 value2 console.log(key, value); }
内建 forEach:
myMap.forEach( (value, key, map) => {
// cucumber: 500 etc
console.log(`${key}: ${value}`);
});
对象 与 Map
对象到 Map:
内建方法 Object.entries(obj),该方法返回对象的键/值对数组,该数组格式完全按照 Map 所需的格式。
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
console.log( map.get('name') ); // John
Map 到对象:
方法Object.entries(obj),给定一个具有 [key, value] 键值对的数组,它会根据给定数组创建一个对象。
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
// 创建一个普通对象(plain object)(*)
let obj = Object.fromEntries(map.entries());
// 或者
// let obj = Object.fromEntries(map);
// map本身就是一个标准迭代对象
console.log(obj.orange); // 2
集合 Set
属性总览:
-
new Set(iterable):创建一个新的Set,如果提供了一个可迭代对象(通常是数组),将会从该对象中复制值到Set中。let mySet = new Set([1, 2, 3, 4]); console.log(mySet); // Set(4) {1, 2, 3, 4} -
set.add(value):添加一个值到Set中,返回Set本身。因此支持链式调用。
mySet.add(5); console.log(mySet); // Set(5) {1, 2, 3, 4, 5} -
set.delete(value):删除指定的值,如果该值在调用时存在,则返回true,否则返回false。console.log(mySet.delete(3)); // true console.log(mySet.delete(6)); // false -
set.has(value):如果指定的值在Set中,则返回true,否则返回false。console.log(mySet.has(2)); // true console.log(mySet.has(3)); // false -
set.clear():清空Set。mySet.clear(); console.log(mySet); // Set(0) {} -
set.size:返回Set中的元素个数。console.log(mySet.size); // 0
Set 迭代:
为了兼容 Map,存在妥协。
forEach第二个参数重复- 支持
set.keys(),返回包含所有值的可迭代对象set.values()作用与set.keys()相同set.entries()返回[value, value]格式的可迭代对象
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) console.log(value);
// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
console.log(value);
});
弱映射和弱集合
内存堆积
在JavaScript引擎中,只要认为值是可达的,则会一直贮存在内存当中。只有当这个值的所有引用都被更改,
才会在适当的时机完成清除。
同理,在数组、Map、Set中的对象也是被认为可达的,即使此对象的引用已经被覆盖更改:
let john = { name: "John" };
let array = [ john ];
// 覆盖引用,但可以通过 array[0] 获取到它
// 所以它不会被垃圾回收机制回收
john = null;
let map = new Map();
map.set(john, "...");
// 覆盖引用,但可以使用 map.keys() 来获取它
// 也不会被回收
john = null;
let set = new Set();
set.add(john);
// 覆盖引用,但可以使用 set.values() 来获取它
// 也不会被回收
john = null;
WeakMap
WeakMap仅支持使用对象作为 key,不能是原始值。
在WeakMap中对象只作为键存在,当对象的引用全部失效时,该对象会被自动回收。并且在WeakMap中自动删除(这也是为什么不支持去迭代这些属性的原因。因为对象的删除时间是不受控制的,迭代的结果会存在不一致情况)
在属性上,仅有:
-
weakMap.get(key)
返回key对应的值;若key不存在,返回undefined。const wm = new WeakMap(); const obj = {}; wm.set(obj, 42); console.log(wm.get(obj)); // 42 -
weakMap.set(key, value)
设置键值对,返回WeakMap自身(链式调用)。wm.set(obj, 100).set(document.body, 'root'); -
weakMap.delete(key)
删除指定键;成功返回true,否则返回false。console.log(wm.delete(obj)); // true console.log(wm.delete(obj)); // false -
weakMap.has(key)
若key存在于WeakMap中返回true,否则返回false。console.log(wm.has(document.body)); // true
与Map相比,不支持 *迭代,keys(),values(),entries()*这些属性。
WeakSet
同理WeakMap:
- 只能添加对象作为值
- 对象失去所有引用时会被自动删除
- 仅支持
add,has,delete方法,不可迭代,不支持size,keys
属性总览:
-
weakSet.add(obj)
将对象加入集合,返回WeakSet自身。const ws = new WeakSet(); let foo = {}; ws.add(foo).add(window); -
weakSet.has(obj)
若对象存在于集合中返回true,否则false。console.log(ws.has(foo)); // true -
weakSet.delete(obj)
删除指定对象;成功返回true,否则false。console.log(ws.delete(foo)); // true console.log(ws.delete(foo)); // false
Object.keys,values,entries
数据结构设计规则
每个数据结构都有的通用方法:keys,values,entries
Map, Set, Array都支持这些方法,同理,自实现的数据结构也应当遵守这种要求,也要实现这三个方法。
对于普通对象,内部也符合这种要求:
-
Object.keys(obj)
返回一个包含对象自身所有可枚举键的数组。const user = { name: 'Ann', age: 18 }; console.log(Object.keys(user)); // ['name', 'age'] -
Object.values(obj)
返回一个包含对象自身所有可枚举值的数组。console.log(Object.values(user)); // ['Ann', 18] -
Object.entries(obj)
返回一个包含对象自身所有可枚举[key, value]键值对的二维数组。console.log(Object.entries(user)); // [['name', 'Ann'], ['age', 18]]
要注意,这些静态方法返回的都是数组,并非可迭代对象,而map.keys()
返回的是可迭代对象。
此外,这三个静态方法对于Symbol属性会进行忽略。
只获取Symbol类型键可以使用:Object.getOwnPropertySymbols
或者获取所有的键:Reflect.ownKeys(obj)
为什么返回数组,而不是可迭代对象?
- 出现得早:
Object.keys在 ES5(2009)就定型,那时 JavaScript 还没有成熟的“可迭代协议”概念,语言规范里最常见的“列表”就是数组,于是直接把结果做成数组最自然。 - 保持兼容:后续新增的
Object.values/Object.entries(ES2017)沿用了同一模式,避免同族方法行为不一致。 - 生态惯性:早期代码都假设拿到的是数组,可以立即用
.sort()、.concat()等数组方法;如果突然改成返回“可迭代对象”会破坏大量现有脚本,因此继续保留“真数组”返回。
一句话:它们诞生时“可迭代对象”尚未普及,规范为了向后兼容,只能把“返回真正的数组”这一既成事实延续下来 。
对象与其他数据结构转换
如何对一个对象使用数组方法?
- 使用 Object.entries(obj) 从 obj 获取由键/值对组成的数组。
- 对该数组使用数组方法,例如 map,对这些键/值对进行转换。
- 对结果数组使用 Object.fromEntries(array) 方法,将结果转回成对象。
let prices = {
banana: 1,
orange: 2,
meat: 4,
};
let doublePrices = Object.fromEntries(
// 将价格转换为数组,将每个键/值对映射为另一对
// 然后通过 fromEntries 再将结果转换为对象
Object.entries(prices).map(entry => [entry[0], entry[1] * 2])
);
console.log(doublePrices.meat); // 8
Object.is 与 ===
在以下情况存在区别:
- 对
NaN的判断更加准确Object.is(NaN, NaN) === true; // true NaN === NaN; // false - 对
+0和-0对待方式不同Object.is(0, -0) === false; // true 0 === -0; // true
其余情况二者功能一致。当内部算法需要比较两个值是否完全相同时,应当使用 Object.is(内部称为 SameValue)
解构赋值
对于Object和Array这两种数据结构,当传递给函数时,不一定需要整个对象或者数组,而是其中一部分。
结构规则
- 等式右边需为可迭代对象
- 左边可使用任意数量的变量进行接受(对象的属性也允许)
- 可使用
...rest这种格式接受多余的数据
可迭代对象解构
数组:
let arr = ["John", "Smith"]
// 解构赋值
// 设置 firstName = arr[0]
// 以及 surname = arr[1]
let [firstName, surname] = arr;
let user = {};
[user.name, user.surname] = arr;
console.info(firstName); // John
console.info(surname); // Smith
字符串:
let [firstName, ,surname] = "John extra Smith".split(' ');
console.info(firstName); // John
console.info(surname); // Smith
let [a, b, c] = "abc"; // ["a", "b", "c"]
允许添加额外的逗号丢弃不需要的数据
集合:
let [one, two, three] = new Set([1, 2, 3]);
整体类似于Python的自动解包:
// 对象
let user = {
name: "John",
age: 30
};
// 使用循环遍历键—值对
for (let [key, value] of Object.entries(user)) {
console.info(`${key}:${value}`); // name:John, then age:30
}
// Map
let userMap = new Map();
userMap.set("name", "John");
userMap.set("age", "30");
// Map 是以 [key, value] 对的形式进行迭代的,非常便于解构
for (let [key, value] of userMap) {
console.info(`${key}:${value}`); // name:John, then age:30
}
值交换技巧:
let guest = "Jane";
let admin = "Pete";
[guest, admin] = [admin, guest];
...rest:
如果左边变量数量少于右边数据总量,多余的数据会被省略,或者使用...进行收集。
如果左边变量数量少于右边数据总量,缺少对应值的变量都会被赋 undefined。
不过支持设置默认值(也可以设置为函数):
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
console.info(name1); // Julius
console.info(name2); // Caesar
// 其余数组项未被分配到任何地方
或者:
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// rest 是包含从第三项开始的其余数组项的数组
console.info(rest[0]); // Consul
console.info(rest[1]); // of the Roman Republic
console.info(rest.length); // 2
对象解构
等号右侧是一个已经存在的对象,拆分到左侧相应属性的类对象模式中。
let options = {
title: "Menu",
width: 100,
height: 200
};
// 允许乱序处理,但属性名称需要一致
let {title, height, width} = options;
console.info(title); // Menu
console.info(width); // 100
console.info(height); // 200
指定属性名称映射:
let options = {
title: "Menu",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
// “从对象中什么属性的值 :赋值给哪个变量”
let {width: w, height: h, title} = options;
// 将options对象中的...赋值给...
// width -> w
// height -> h
// title -> title
console.info(title); // Menu
console.info(w); // 100
console.info(h); // 200
同理也支持设置默认值:
let {width = console.info("width?"), title = console.info("title?")} = options; // 或者 let {width: w = 100, height: h = 200, title} = options;
对象剩余模式:
和可迭代对象一致,储存多余的对象属性。
let options = {
title: "Menu",
height: 200,
width: 100
};
// title = 名为 title 的属性
// rest = 存有剩余属性的对象
let {title, ...rest} = options;
// 现在 title="Menu", rest={height: 200, width: 100}
console.info(rest.height); // 200
console.info(rest.width); // 100
尽量不去提前申明:
let title, width, height; // 这一行发生了错误 {title, width, height} = {title: "Menu", width: 200, height: 100};// 等式左边被认为是一个代码块,而不是对象解构
对象嵌套解构:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
// 为了清晰起见,解构赋值语句被写成多行的形式
let {
size: { // 把 size 赋值到这里
width,
height
},
items: [item1, item2], // 把 items 赋值到这里
title = "Menu" // 在对象中不存在(使用默认值)
} = options;
console.info(title); // Menu
console.info(width); // 100
console.info(height); // 200
console.info(item1); // Cake
console.info(item2); // Donut
智能函数参数
对于一个函数的参数,大部分只使用默认值就行,只需要修改部分参数的情况下,推荐此方式。
// 常规方法
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
// ...
}
// 使用时对于中间参数使用默认值需要传递undefined,不优雅
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])
使用对象解构:
// 传入一个对象,缺少的属性使用默认值处理
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
console.info( `${title} ${width} ${height}` );
}
// 调用更加优雅
showMenu(); // Menu 100 200
日期和时间
日期对象创建
当前日期:
let now = new Date();
// 标准 Date对象
// Date Wed Dec 31 2025 10:46:10 GMT+0800 (中国标准时间)
console.info(now);
建议使用
Date.now()方法创建当前日期,减少对象创建的开销。
传入时间戳创建:
传入的整数参数代表的是自 1970-01-01 00:00:00 以来经过的毫秒数,该整数被称为 时间戳
// 0 表示 01.01.1970 UTC+0
let Jan01_1970 = new Date(0);
console.info( Jan01_1970 );
// 现在增加 24 小时,得到 02.01.1970 UTC+0
let Jan02_1970 = new Date(24 * 3600 * 1000);
console.info( Jan02_1970 );
// 支持传入负数
let Dec31_1969 = new Date(-24 * 3600 * 1000);
console.info( Dec31_1969 );
传入字符串创建:
未指定具体时区,所以假定时间为格林尼治标准时间(GMT)的午夜零点
并根据运行代码时的用户的时区进行调整
let date = new Date("2017-01-26");
console.info(date);
传入时间数字创建:
- year 传入四位数表示年份。
- month 计数从 0(一月)开始,到 11(十二月)结束。
- date 是当月的具体某一天,如果缺失,则为默认值 1。
- 如果 hours/minutes/seconds/ms 缺失,则均为默认值 0。
console.info(new Date(2011, 0, 1)); // 时分秒等均为默认值 0
let date = new Date(2011, 0, 1, 2, 3, 4, 567);
console.info( date ); // 1.01.2011, 02:03:04.567
字符串日期解析:
Date.parse(str) —— 字符串 → 时间戳
-
标准格式
YYYY-MM-DDTHH:mm:ss.sssZYYYY-MM-DD日期部分T分隔符HH:mm:ss.sss时间部分Z或±HH:mm时区(省略则为本地时区)
-
返回
成功:自 1970-01-01 00:00:00 UTC 起的毫秒数
失败:NaN -
简写也支持
YYYY-MM-DD、YYYY-MM、YYYY
// 完整格式
const ms = Date.parse('2012-01-26T13:51:50.417-07:00');
console.log(ms); // 1327611110417
// 立即生成 Date 实例
const date = new Date(ms);
console.log(date); // 对应本地时间的 Date 对象
访问日期相关方法
本地时区
-
getFullYear()
返回 4 位年份(永远不要使用已废弃的getYear())。const d = new Date(); console.log(d.getFullYear()); // 2025 -
getMonth()
返回月份,0 起始(0 = 一月,11 = 十二月)。console.log(d.getMonth()); // 11(12 月) -
getDate()
返回当月第几天(1–31)。console.log(d.getDate()); // 31 -
getDay()
返回星期几,0 起始(0 = 星期日,6 = 星期六)。console.log(d.getDay()); // 2(星期二) -
getHours()/getMinutes()/getSeconds()/getMilliseconds()
返回本地时区下的时、分、秒、毫秒。console.log(d.getHours(), d.getMinutes()); // 例如 14 45
UTC 对应项(时区 +0)
在 get 后插入 UTC 即可获得 UTC 时区数值:
getUTCFullYear()、getUTCMonth()、getUTCDate()、getUTCDay()、getUTCHours() …
console.log(d.getUTCHours()); // 比本地小时少/多若干小时
特殊方法(无 UTC 版本)
-
getTime()
返回时间戳——自 1970-01-01 00:00:00 UTC 起的毫秒数。console.log(d.getTime()); // 1704102023845 -
getTimezoneOffset()
返回本地与 UTC 的时差(分钟),UTC 以东为负,以西为正。console.log(d.getTimezoneOffset()); // 例如 -480(UTC+8)
设置日期相关方法(本地时区)
-
setFullYear(year[, month][, date])
设置年(必传)、月、日;未给定的月/日保持原值。const d = new Date(); d.setFullYear(2026, 5, 15); // 2026-06-15 -
setMonth(month[, date])
设置月(0 起始)、日;未给定日期时保留原日。d.setMonth(11); // 12 月,日不变 -
setDate(date)
设置当月第几天(1–31)。d.setDate(1); // 当月 1 号 -
setHours(hour[, min][, sec][, ms])
设置时、分、秒、毫秒;未提及的组件不修改。d.setHours(0); // 仅把小时改成 0 d.setHours(0, 0, 0, 0); // 时间设为 00:00:00.000,日期不变 -
setMinutes(min[, sec][, ms])
设置分、秒、毫秒;未给定部分保持原值。d.setMinutes(30); // 分改为 30,秒/毫秒不变 -
setSeconds(sec[, ms])
设置秒、毫秒;未给定毫秒时保留原毫秒。d.setSeconds(45); -
setMilliseconds(ms)
仅设置毫秒(0–999)。d.setMilliseconds(123); -
setTime(milliseconds)
用自 1970-01-01 00:00:00 UTC 起的毫秒数一次性重置整个日期;无 UTC 变体。d.setTime(1800000000000); // 2027-01-19 00:00:00.000 UTC
所有方法(除
setTime)均有对应的 UTC 变体:setUTCFullYear()、setUTCHours()…
自动校准
设置超范围的数值,它会自动校准,将多余的部分放到合适的位置。
let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
console.info(date); // ……是 1st Feb 2013!
对于闰年情况也不需要自己考虑:
let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);
console.info( date ); // 1 Mar 2016
Date对象与数字转换:(隐式转换与显示调用date.getTime()一致)
let date = new Date();
// 以毫秒为单位的数值,与使用 date.getTime() 的结果相同
console.info(+date);
性能差异
对于对象使用减号进行隐式转换和使用显示调用情况下,存在性能差异:
日期可以相减,得到的是以毫秒表示的两者的差值。因为当 Date 被转换为数字时,Date 对象会被转换为时间戳
function diffSubtract(date1, date2) {
return date2 - date1;
}
function diffGetTime(date1, date2) {
return date2.getTime() - date1.getTime();
}
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
let start = Date.now();
for (let i = 0; i < 100000; i++) f(date1, date2);
return Date.now() - start;
}
// Time of diffSubtract: 7ms
console.info( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' );
// Time of diffGetTime: 1ms
console.info( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' );
现代JavaScript引擎会对热代码进行优化,因此增加预热步骤:
// 在主循环中增加预热环节
bench(diffSubtract);
bench(diffGetTime);
// 开始度量
for (let i = 0; i < 10; i++) {
time1 += bench(diffSubtract);
time2 += bench(diffGetTime);
}
JSON (JavaScript Object Notation)
本质功能是将复杂对象处理成字符串,以供输出或传输。是一种跨语言的对象表示方法。
核心方法:
JSON.stringify // 将对象转换为 JSON。
JSON.parse // 将 JSON 转换回对象。
对象原始转换toString
通过自定义原始值转换方法,对已有对象进行转换,但难以维护,属性变动就需要更改方法。
let user = {
name: "John",
age: 30,
toString() {
return `{name: "${this.name}", age: ${this.age}}`;
}
};
console.info(`${user}`); // {name: "John", age: 30}
JSON.stringify
完整语法:
let json = JSON.stringify(value[, replacer, space])
- value:要编码的值。
- replacer:要编码的属性数组或映射函数 function(key, value)。
- space:用于格式化的空格数量或者字符。
数字则使用空格进行格式化,字符则使用给定字符进行格式化处理。
使用"AAA"进行格式化结果:{ AAA"name": "John", AAA"age": 25, AAA"roles": { AAAAAA"isAdmin": false, AAAAAA"isEditor": true AAA} }
replacer:
- 类型:
Array<string>|function(key, value) - 作用:决定哪些属性被保留、如何替换或跳过值。
示例数据:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
- 数组模式
只有列出的属性名会被序列化,且对整个对象树生效。JSON.stringify(meetup, ['title','participants','place','name','number']) // 循环引用属性 occupiedBy 被自动排除 - 函数模式
对每条(key, value)调用,返回新值:- 返回
undefined⇒ 跳过该属性(可用来剔除循环引用)。 - 返回其他值 ⇒ 用该值继续序列化。
首次调用时key为空字符串,value为整棵对象树,方便一次性过滤或替换。
JSON.stringify(meetup, function (key, value) { return key === 'occupiedBy' ? undefined : value; // 去除循环引用 }) - 返回
将对象变成字符串:
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
spouse: null
};
let json = JSON.stringify(student);
console.info(typeof json); // we've got a string!
/* JSON 编码的对象:
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"spouse": null
}
*/
console.info(json);
处理后的
json字符串被称作: JSON 编码(JSON-encoded) 或 序列化(serialized) 或 字符串化(stringified) 或 编组化(marshalled) 的对象。
注意点:
- 编码后的对象的字符串属性使用双引号
- 对象的属性名称变成双引号字符串
- 支持Object、Array、String、Number、Boolean、Null
- 支持嵌套对象的转换,但不能存在循环引用
- 对于独属于JavaScript的特性,不会被转换:函数属性方法、Symbol类型的键与值、undefined的属性
// 数字在 JSON 还是数字
console.info( JSON.stringify(1) ) // 1
// 字符串在 JSON 中还是字符串,只是被双引号扩起来
console.info( JSON.stringify('test') ) // "test"
console.info( JSON.stringify(true) ); // true
console.info( JSON.stringify([1, 2, 3]) ); // [1,2,3]
let user = {
sayHi() { // 被忽略
console.info("Hello");
},
[Symbol("id")]: 123, // 被忽略
something: undefined // 被忽略
};
console.info( JSON.stringify(user) ); // {}(空对象)
// 嵌套对象
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
console.info( JSON.stringify(meetup) );
toJSON
对象内建方法,以供JSON.stringify调用,对其进行转换。
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
console.info( JSON.stringify(room) ); // 23
console.info( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"room": 23
}
*/
JSON.parse
将 JSON 转换回对象
完整语法:
let value = JSON.parse(str, [reviver]);
- str:要解析的 JSON 字符串。
let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'; let user = JSON.parse(userData); console.info( user.friends[1] ); // 1 - reviver:选的函数 function(key,value),该函数将为每个 (key, value) 对调用,并可以对值进行转换。
reviver:
- 类型:
function(key, value)|undefined - 作用:在解析过程中对每条
(key, value)调用,可返回转换后的值,实现“即时反序列化”。
示例数据:
let schedule = `{
"meetups": [
{"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
{"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
]
}`;
schedule = JSON.parse(schedule, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
- 调用规则
从最深层次开始,逐层向上;首次调用key为空字符串,value为整个待解析对象。 - 常见用法
识别日期字符串并转成Date对象;也可用于过滤、计算等。const meetup = JSON.parse(str, (key, value) => key === 'date' ? new Date(value) : value ); - 嵌套场景
同样适用于数组或深层结构:只要键匹配即可统一转换。const schedule = JSON.parse(scheduleJSON, (key, value) => key === 'date' ? new Date(value) : value );