数据类型

对象包装器

针对原始类型

原始类型理论上只提供单个原始值,但却允许访问字符串、数字、布尔值和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"); 

nullundefined无任何方法,真正的原始值

数字类型

在所有数字函数中,包括 isFinite,空字符串或仅有空格的字符串均被视为 0

精确度丢失

现代JS中存在两种数字类型:

  1. 采用IEEE-754 规则 64位 格式存储,即双精度浮点数
  2. 对于任意长度的大整数采用 BigInt进行表示,这是因为第一种整数在(2**53 - 1)-(2**53 - 1)范围外存在精确度问题。

IEEE 754 双精度浮点数的表示方式:

  • 1位符号位(S):表示数字的正负
  • 11位指数位(Exponent):表示数字的指数部分
  • 52位尾数位(Mantissa 或 Fraction):表示数字的有效数字部分
  • 因此最大整数Number.MAX_SAFE_INTEGER2**53−1
// 数据过大,变成Infinity  true
console.info(2**53) == (2**53 + 1);
// 超出最大安全表示,自动+1
// 10000000000000000
console.info(9999999999999999);

// 注意!!!
// true
console.info(0 === -0);

0.10.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.13.6-1.1-1.6
Math.floor向下舍入33-2-2
Math.ceil向上舍入44-1-1
Math.round向最近的整数舍入34-1-2
Math.trunc移除小数部分33-1-1

舍入到小数点后n位:

  1. 乘除法
let num = 1.23456;
// 1.23456 -> 123.456 -> 123 -> 1.23
console.info(Math.round(num * 100) / 100); 
  1. 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 === NaNfalse,无法和自身判断

  • 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

parseIntparseFloat

从字符串中“读取”数字,直到无法读取为止。如果发生 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 字符,例如:\x7Az 相同。
\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();

现代方法:includesstartsWithendsWith

includes(substr, pos):根据 str 中是否包含 substr 来返回 true/false

// true
console.info("Widget with id".includes("Widget"));
// false
console.info("Hello".includes("Bye"));

startsWithendsWith:判断起始或结尾是否为特定字符

// 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.fromCodePoint
  • str.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操作

  1. 移除0号索引元素
  2. 所有元素左移,补充0号位置
  3. 更新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。

indexOfincludes内部都为===严格判断,但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,存在妥协。

  1. forEach 第二个参数重复
  2. 支持set.keys(),返回包含所有值的可迭代对象
  3. set.values()作用与set.keys()相同
  4. 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)

为什么返回数组,而不是可迭代对象?

  1. 出现得早Object.keys 在 ES5(2009)就定型,那时 JavaScript 还没有成熟的“可迭代协议”概念,语言规范里最常见的“列表”就是数组,于是直接把结果做成数组最自然。
  2. 保持兼容:后续新增的 Object.values / Object.entries(ES2017)沿用了同一模式,避免同族方法行为不一致。
  3. 生态惯性:早期代码都假设拿到的是数组,可以立即用 .sort().concat() 等数组方法;如果突然改成返回“可迭代对象”会破坏大量现有脚本,因此继续保留“真数组”返回。

一句话:它们诞生时“可迭代对象”尚未普及,规范为了向后兼容,只能把“返回真正的数组”这一既成事实延续下来 。

对象与其他数据结构转换

如何对一个对象使用数组方法?

  1. 使用 Object.entries(obj) 从 obj 获取由键/值对组成的数组。
  2. 对该数组使用数组方法,例如 map,对这些键/值对进行转换。
  3. 对结果数组使用 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)

解构赋值

对于ObjectArray这两种数据结构,当传递给函数时,不一定需要整个对象或者数组,而是其中一部分。

结构规则

  1. 等式右边需为可迭代对象
  2. 左边可使用任意数量的变量进行接受(对象的属性也允许)
  3. 可使用...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.sssZ

    • YYYY-MM-DD 日期部分
    • T 分隔符
    • HH:mm:ss.sss 时间部分
    • Z±HH:mm 时区(省略则为本地时区)
  • 返回
    成功:自 1970-01-01 00:00:00 UTC 起的毫秒数
    失败:NaN

  • 简写也支持
    YYYY-MM-DDYYYY-MMYYYY

// 完整格式
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
  1. 数组模式
    只有列出的属性名会被序列化,且对整个对象树生效。
    JSON.stringify(meetup, ['title','participants','place','name','number'])
    // 循环引用属性 occupiedBy 被自动排除
    
  2. 函数模式
    对每条 (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;
});
  1. 调用规则
    从最深层次开始,逐层向上;首次调用 key 为空字符串,value 为整个待解析对象。
  2. 常见用法
    识别日期字符串并转成 Date 对象;也可用于过滤、计算等。
    const meetup = JSON.parse(str, (key, value) =>
      key === 'date' ? new Date(value) : value
    );
    
  3. 嵌套场景
    同样适用于数组或深层结构:只要键匹配即可统一转换。
    const schedule = JSON.parse(scheduleJSON, (key, value) =>
      key === 'date' ? new Date(value) : value
    );