Skip to content

JavaScript 系统学习文档 ​

从零开始,系统掌握 JavaScript 核心知识 版本:ES6+ 为主,2026-07-09 已补充 ES2015 到 ES2026 的主要新增特性。具体兼容性请以 MDN、Can I use、Node.js/V8/浏览器版本为准。


目录 ​

  1. JavaScript 简介与历史

  2. 开发环境与工具

  3. 基础语法

  4. 变量与数据类型

  5. 运算符与表达式

  6. 控制流程

  7. 函数

  8. 数组

  9. 对象与原型

  10. 字符串与正则表达式

  11. ES6+ 核心新特性

  12. 面向对象编程

  13. 异步编程

  14. 模块化

  15. DOM 操作与 BOM

  16. 事件机制

  17. 错误处理与调试

  18. 浏览器存储

  19. 网络请求

  20. 常用设计模式

  21. 性能优化基础

  22. 学习路线总结

  23. 核心知识点深度补全


1. JavaScript 简介与历史 ​

1.1 什么是 JavaScript ​

JavaScript(简称 JS)是一种轻量级的、解释型或即时编译型的编程语言。它是 Web 开发的三大核心技术之一(HTML 定义内容、CSS 定义样式、JavaScript 定义行为)。

1.2 发展简史 ​

时间事件
1995Brendan Eich 在 Netscape 创造了 JavaScript(最初叫 Mocha → LiveScript → JavaScript)
1997ECMAScript 1 标准发布
2009ES5 发布,带来严格模式、JSON 支持等
2015ES6/ES2015 发布——现代 JavaScript 的里程碑
2016-现在每年发布一个新版本(ES2016 之后持续演进,本文补充到 ES2026 主要特性)
2009Node.js 诞生,JavaScript 可以运行在服务器端
2012TypeScript 首次公开发布(v0.8),为 JS 添加了静态类型系统

1.3 JavaScript 的应用场景 ​

  • 前端开发:React、Vue、Angular 等框架

  • 后端开发:Node.js(Express、Koa、NestJS)

  • 移动端:React Native、Flutter(Dart 但生态相关)

  • 桌面端:Electron(VSCode、Slack、Discord)

  • 游戏开发:Cocos Creator、Three.js

  • 数据库:MongoDB 的查询语言

  • 物联网:Espruino、Johnny-Five

    1.4 ECMAScript 与 JavaScript 的关系 ​

  • ECMAScript(ES):语言标准规范

  • JavaScript:对 ECMAScript 标准的具体实现

  • 我们常说的 ES6 就是 ECMAScript 2015 版本的简称


2. 开发环境与工具 ​

2.1 浏览器控制台 ​

打开方式:F12 / 右键 → 检查 → Console(控制台)面板

javascript
console.log("Hello, JavaScript!");

2.2 推荐编辑器 ​

  • VS Code: 最流行的 JS 编辑器

  • 推荐插件:ESLint、Prettier、JavaScript (ES6) code snippets

    2.3 Node.js 环境 ​

    安装 Node.js 后即可在终端运行 JS 文件:

    bash
    node index.js

    或者进入交互模式:

    bash
    node
    > console.log("Hello")

    2.4 HTML 中引入 JavaScript ​

    html
    <!-- 方式一:内部脚本 -->
    <script>
    console.log("Hello");
    </script>
    
    <!-- 方式二:外部脚本(推荐) -->
    <script src="script.js"></script>
    
    <!-- 推荐使用 defer,让脚本在 HTML 解析完成后执行 -->
    <script defer src="script.js"></script>

3. 基础语法 ​

3.1 语句与分号 ​

javascript
let a = 1;
let b = 2;

3.2 注释 ​

javascript
// 单行注释

/*
  多行注释
*/

/**
 * JSDoc 注释
 * @param {string} name
 */
function greet(name) {
  return `Hello, ${name}`;
}

3.3 严格模式 ​

javascript
"use strict";
// 禁止意外创建全局变量、禁止对只读属性赋值等

3.4 标识符命名规则 ​

  • 区分大小写:name 和 Name 不同

  • 首字符:字母、下划线 _ 或美元符号 $

  • 后续字符:字母、数字、下划线、美元符号

  • 不能使用保留字(如 if、for、class 等)

    命名规范:

    风格示例使用场景
    camelCase(驼峰)userName, getData变量名、函数名
    PascalCase(大驼峰)UserInfo, HttpClient类名、构造函数
    UPPER_SNAKE_CASEMAX_SIZE, API_URL常量

    3.5 输入与输出 ​

    javascript
    console.log("普通日志");
    console.warn("警告");
    console.error("错误");
    console.table([{name: "Tom"}, {name: "Jerry"}]);
    
    alert("提示信息");
    const result = confirm("确定吗?");
    const name = prompt("请输入名字:");

4. 变量与数据类型 ​

4.1 变量声明 ​

javascript
// var(旧方式,函数作用域,不推荐)
var oldWay = "不推荐使用";

// let(现代方式,块级作用域)
let name = "Alice";
name = "Bob"; // 可重新赋值

// const(常量,声明时必须初始化,不可重新赋值)
const PI = 3.14159;
// PI = 3; // 报错!

// 但 const 对象的属性可以修改
const user = { name: "Alice" };
user.name = "Bob"; // 可以

let vs const 使用原则:

  • 默认优先使用 const

  • 只有当变量需要重新赋值时才用 let

  • 永远不使用 var(除非维护旧代码)

4.1.1 变量提升(Hoisting)与暂时性死区(TDZ) ​

var 和函数声明会被提升(hoisting):在声明语句之前就能访问(var 得到 undefined,函数声明可以正常调用)。let / const 也会提升,但在声明语句执行前处于暂时性死区(Temporal Dead Zone,TDZ),此时访问会抛 ReferenceError。

javascript
console.log(a); // undefined(var 提升并初始化为 undefined)
foo();          // "ok"(函数声明整体提升)

var a = 1;
function foo() { console.log("ok"); }

console.log(b); // ReferenceError(let 提升但处于 TDZ,不可访问)
let b = 2;

⚠️ TDZ 也是循环里 var 和 let 行为不同的根源:var 只有一个共享的循环变量,let 每次迭代都会创建新的绑定(详见《深入详解篇》2.3)。

4.2 基本数据类型 ​

类型示例说明
Number42, 3.14, Infinity, NaN所有数字,不区分整数/浮点
String"hello", 'world', `template`字符串
Booleantrue, false布尔值
Nullnull空值(主动设置)
Undefinedundefined未定义
SymbolSymbol("desc")唯一标识符(ES6)
BigInt999n, BigInt(999)超大整数(ES2020)
javascript
let age = 25;
let price = 19.99;
let template = `模板字符串,支持 ${age} 插值`;
let isActive = true;
let empty = null;
let notAssigned;
console.log(notAssigned); // undefined
let sym = Symbol("key");
let big = 9007199254740991n;

4.2.1 Number 的精度与陷阱 ​

JavaScript 只有一种数字类型 Number,底层是 IEEE 754 双精度浮点数(64 位)。这带来三个必须记住的结论:

  1. 小数运算可能不精确:0.1 + 0.2 不等于 0.3,因为十进制小数在二进制里无法精确表示。
  2. 整数有安全范围:超过 Number.MAX_SAFE_INTEGER(2^53 - 1 = 9007199254740991)后,整数会丢失精度。
  3. 有特殊值:NaN、Infinity、-Infinity、-0。
javascript
0.1 + 0.2;             // 0.30000000000000004
0.1 + 0.2 === 0.3;     // false!

// 比较小数用误差范围,而不是 ===
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true(EPSILON ≈ 2.22e-16)

Number.MAX_SAFE_INTEGER;               // 9007199254740991
9007199254740992 === 9007199254740993; // true!超出范围后无法区分

// 超出安全整数:改用 BigInt
const big = 9007199254740993n;

NaN 的陷阱:typeof NaN 是 "number",且 NaN === NaN 是 false。

判断方式isNaN("abc")Number.isNaN("abc")isNaN(NaN)Number.isNaN(NaN)
结果truefalsetruetrue

isNaN 会先把参数转成数字再判断,所以 isNaN("abc") 是 true;Number.isNaN 只判断值本身是不是 NaN。判断 NaN 一律用 Number.isNaN 或 Object.is(x, NaN)。

4.3 检测类型:typeof ​

javascript
typeof 42;            // "number"
typeof "hello";       // "string"
typeof true;          // "boolean"
typeof undefined;     // "undefined"
typeof null;          // "object" —— 历史遗留 bug!
typeof Symbol();      // "symbol"
typeof 42n;           // "bigint"
typeof {};            // "object"
typeof [];            // "object"

Array.isArray([]);    // true 检测数组的正确方式

4.4 类型转换 ​

javascript
// 隐式转换
console.log(1 + "2");   // "12"(字符串拼接)
console.log("5" - 2);   // 3(算术运算)
console.log(1 == "1");  // true(值比较)
console.log(1 === "1"); // false(严格比较)

// 显式转换
String(123);         // "123"
Number("123");       // 123
parseInt("12.5");    // 12
parseFloat("12.5");  // 12.5
Boolean(0);          // false
Boolean("");         // false
Boolean("abc");      // true
!!123;               // true(简洁转布尔)

4.4.1 8 个 falsy 值 ​

转布尔时,只有下面 8 个值是 false,其余都是 true:

falsy 值说明
false布尔假
0、-0、0n零(含 BigInt 零)
""、''空字符串
null空值
undefined未定义
NaN非数字

⚠️ 容易踩坑的 truthy 值:"0"、"false"、[]、{} 都是 true。

4.4.2 显式转换的陷阱 ​

Number() 的转换规则和直觉不完全一致:

javascript
Number("");        // 0(空字符串转 0)
Number("  ");      // 0(纯空白也转 0)
Number(null);      // 0
Number(undefined); // NaN
Number([]);        // 0(空数组)
Number([1]);       // 1(单元素数组)
Number([1, 2]);    // NaN(多元素数组)

parseInt 的陷阱:不传进制时,"0x" 开头会被当作十六进制,历史实现还会把 "0" 开头当作八进制。

javascript
parseInt("0x10");  // 16(十六进制)
parseInt("10", 2); // 2(二进制)
parseInt("08");    // 8(现代引擎按十进制,老引擎可能按八进制得 0)
// 建议:总是显式传第二个参数 radix

4.4.3 == 的隐式转换算法(为什么建议用 ===) ​

== 在类型不同时会按规则转换,规则多且反直觉。核心顺序:

  1. 类型相同 → 直接按 === 比较。
  2. null == undefined → true(两者互相相等,但不等于任何其他值)。
  3. number == string → 把字符串转成数字再比。
  4. boolean == 其他 → 先把布尔转成数字(true → 1、false → 0)。
  5. object == 原始值 → 把对象转成原始值(ToPrimitive,先 valueOf 再 toString)。
javascript
null == undefined; // true
null == 0;         // false!null 不会被转成 0
"" == 0;           // true(空字符串转 0)
"0" == 0;          // true
[] == false;       // true([] → "" → 0;false → 0)
[] == ![];         // true(![] 是 false,然后同上)

💡 工程建议:除非明确知道自己在做什么,一律用 ===。== 的转换规则容易埋下隐蔽 bug,Lint 规则(如 ESLint 的 eqeqeq)默认也会提示改用 ===。

4.5 值类型与引用类型 ​

javascript
// 值类型:存储在栈内存
let a = 10;
let b = a;
b = 20;
console.log(a); // 10(a 不受影响)

// 引用类型:存储在堆内存
let obj1 = { name: "Alice" };
let obj2 = obj1;
obj2.name = "Bob";
console.log(obj1.name); // "Bob"

// 浅拷贝 vs 深拷贝
let copy1 = { ...obj1 };
let copy2 = Object.assign({}, obj1);
let deepCopy = JSON.parse(JSON.stringify(obj1));

4.5.1 深拷贝的三种方式与陷阱 ​

  • 浅拷贝:只复制第一层,{ ...obj }、Object.assign({}, obj) 都是浅拷贝,嵌套对象仍共享引用。
  • JSON.parse(JSON.stringify(obj)):最简单,但有明显缺陷。
  • structuredClone(obj):现代推荐方案。
javascript
const source = {
  date: new Date(),
  fn: () => 1,
  undef: undefined,
  nan: NaN,
  nested: { x: 1 },
};

const viaJSON = JSON.parse(JSON.stringify(source));
// ⚠️ 陷阱:date 变字符串、fn 和 undef 丢失、NaN 变 null
console.log(typeof viaJSON.date); // "string"(不再是 Date)
console.log(viaJSON.fn);          // undefined
console.log(viaJSON.nan);         // null

structuredClone(Node 17+ / Chrome 98+,2022 年 3 月起主流浏览器已普遍支持)能正确复制绝大多数类型:

javascript
const copy = structuredClone(source);
copy.nested.x = 2;
console.log(source.nested.x); // 1(真深拷贝)
copy.date instanceof Date;    // true

⚠️ structuredClone 也不能复制函数、Symbol、DOM 节点(会抛错),但能正确处理循环引用(JSON.stringify 遇到循环引用会直接抛错)。按数据形态取舍。


5. 运算符与表达式 ​

5.1 算术运算符 ​

javascript
let a = 10, b = 3;
a + b;  // 13  加法
a - b;  // 7   减法
a * b;  // 30  乘法
a / b;  // 3.333...  除法
a % b;  // 1   取余
a ** b; // 1000  幂运算(ES7)

let x = 1;
x++;    // 后递增
++x;    // 前递增
x--;    // 后递减
--x;    // 前递减

5.2 赋值运算符 ​

javascript
let x = 10;
x += 5;   // x = x + 5 → 15
x -= 3;   // x = x - 3 → 12
x *= 2;   // x = x * 2 → 24
x /= 4;   // x = x / 4 → 6
x %= 4;   // x = x % 4 → 2
x **= 3;  // x = x ** 3 → 8

5.3 比较运算符 ​

javascript
5 > 3;    // true   大于
5 < 3;    // false  小于
5 >= 5;   // true   大于等于
5 <= 4;   // false  小于等于
5 == "5";  // true   抽象相等(会类型转换)
5 === "5"; // false  严格相等(值和类型都必须相等)
NaN == NaN; // false  NaN 和自己都不相等
Object.is(NaN, NaN); // true

5.4 逻辑运算符 ​

javascript
true && false;  // false  与
true || false;  // true   或
!true;          // false  非

// 短路求值
let name = user && user.name;
let defaultName = input || "默认值";
let displayName = name ?? "匿名";   // 空值合并 ??(ES2020)

5.5 条件(三元)运算符 ​

javascript
let age = 20;
let status = age >= 18 ? "成年" : "未成年";

5.6 可选链与空值合并(ES2020) ​

javascript
const user = { address: { city: "Beijing" } };
console.log(user?.address?.city);    // "Beijing"
console.log(user?.contact?.phone);   // undefined(不会报错)

let value = 0;
let a = value || "default";   // "default"(0 是 falsy)
let b = value ?? "default";    // 0(?? 只在 null/undefined 时取默认值)

6. 控制流程 ​

6.1 条件语句 ​

javascript
// if...else
let score = 85;
if (score >= 90) {
  console.log("优秀");
} else if (score >= 80) {
  console.log("良好");
} else {
  console.log("加油");
}

// switch
let day = 3;
switch (day) {
  case 1: console.log("周一"); break;
  case 2: console.log("周二"); break;
  case 3: console.log("周三"); break;
  default: console.log("未知");
}

// 三元表达式
const price = isMember ? 100 : 200;

6.2 循环语句 ​

javascript
// for
for (let i = 0; i < 5; i++) {
  console.log(i); // 0, 1, 2, 3, 4
}

// while
let i = 0;
while (i < 5) { console.log(i); i++; }

// do...while(至少执行一次)
let j = 0;
do { console.log(j); j++; } while (j < 5);

// for...of(遍历可迭代对象,ES6)
const arr = [10, 20, 30];
for (const item of arr) { console.log(item); }

// for...in(遍历对象属性)
const obj = { a: 1, b: 2 };
for (const key in obj) { console.log(key, obj[key]); }

// break 与 continue
for (let i = 0; i < 10; i++) {
  if (i === 3) continue;  // 跳过
  if (i === 7) break;     // 跳出
  console.log(i);
}

7. 函数 ​

7.1 函数定义方式 ​

javascript
// 函数声明(有提升)
function sum(a, b) { return a + b; }

// 函数表达式(没有提升)
const multiply = function(a, b) { return a * b; };

// 箭头函数(ES6,没有自己的 this)
const divide = (a, b) => a / b;
const getPerson = (name, age) => ({ name, age }); // 返回对象需加括号

7.2 参数处理 ​

javascript
// 默认参数(ES6)
function greet(name = "游客") {
  return `你好,${name}`;
}

// 剩余参数(ES6)
function sumAll(...numbers) {
  return numbers.reduce((t, n) => t + n, 0);
}

// 展开运算符传参
const nums = [1, 2, 3];
console.log(Math.max(...nums)); // 3

7.3 作用域与闭包 ​

javascript
// 闭包:函数可以访问外部作用域的变量
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    getCount: () => count
  };
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

7.4 高阶函数 ​

javascript
// 函数作为参数
function operate(a, b, callback) {
  return callback(a, b);
}
operate(5, 3, (x, y) => x + y); // 8

// 函数作为返回值(工厂函数)
function createMultiplier(factor) {
  return (number) => number * factor;
}
const double = createMultiplier(2);
console.log(double(5)); // 10

7.5 this 指向规则 ​

javascript
// 普通函数:调用时决定
function showThis() { console.log(this); }

// 箭头函数:定义时决定
const obj = {
  name: "Alice",
  sayName() { console.log(this.name); },
  sayNameArrow: () => console.log(this.name) // undefined
};

// 手动绑定
function greet() { console.log(`Hi, ${this.name}`); }
const user = { name: "Bob" };
greet.call(user);
greet.apply(user);
const bound = greet.bind(user);

7.5.1 this 的四种绑定规则与优先级 ​

this 不是在定义时决定的,而是在调用时根据调用方式决定的。四种绑定规则:

规则触发方式this 指向
默认绑定直接调用 fn()非严格模式 window/globalThis;严格模式 undefined
隐式绑定obj.fn()调用点左侧的对象 obj
显式绑定fn.call(obj) / fn.apply(obj) / fn.bind(obj)指定的 obj
new 绑定new Fn()新建的实例对象

优先级:new > 显式绑定 > 隐式绑定 > 默认绑定。箭头函数不参与这四种绑定,永远取定义处外层作用域的 this(词法绑定)。

⚠️ 最常见的坑:把方法从对象里取出来单独调用,this 会丢失。

javascript
const user = {
  name: "Alice",
  sayName() { console.log(this.name); },
};

const fn = user.sayName;
fn(); // undefined(默认绑定,严格模式下 this 是 undefined)

const bound = user.sayName.bind(user);
bound(); // "Alice"

8. 数组 ​

8.1 创建数组 ​

javascript
const arr1 = [1, 2, 3];           // 字面量(推荐)
const arr2 = new Array(5);        // 长度为 5 的空数组
const fromStr = Array.from("hi"); // ['h', 'i']
const zeros = new Array(3).fill(0); // [0, 0, 0]

8.2 数组基本操作 ​

javascript
const arr = [10, 20, 30];
arr.push(40);        // 末尾添加
arr.pop();           // 删除末尾
arr.unshift(0);      // 开头添加
arr.shift();         // 删除开头
arr.splice(1, 1);    // 从索引 1 删除 1 个
arr.splice(1, 0, 15); // 在索引 1 插入 15
const sub = arr.slice(1, 3); // 截取子数组
const combined = [...arr, 40, 50]; // 合并

8.3 数组核心方法 ​

javascript
const numbers = [1, 2, 3, 4, 5];

// forEach:遍历
numbers.forEach((n, i) => console.log(i, n));

// map:映射
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]

// filter:筛选
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]

// reduce:归约
const sum = numbers.reduce((acc, cur) => acc + cur, 0); // 15

// find:查找第一个
const firstEven = numbers.find(n => n % 2 === 0); // 2

// some / every
numbers.some(n => n > 4);  // true
numbers.every(n => n > 0); // true

// includes
numbers.includes(3); // true

// sort(注意:默认按字符串排!)
[3, 1, 4, 1, 5].sort((a, b) => a - b);

// flat / flatMap
[[1, 2], [3, [4]]].flat(Infinity); // [1, 2, 3, 4]

// 链式调用
numbers
  .filter(n => n > 2)
  .map(n => n * 10)
  .reduce((a, b) => a + b); // 120

8.3.1 会修改原数组 vs 返回新数组 ​

⚠️ 数组方法分两类,混用是常见 bug 来源:

类别方法是否修改原数组
修改原数组push、pop、shift、unshift、splice、sort、reverse、fill、copyWithin是
返回新数组map、filter、slice、concat、flat、flatMap否

React / Vue 等框架要求不可变更新(immutable update),因此常用返回新数组的方法;ES2023 又新增 toSorted、toReversed、toSpliced、with,把「修改型」方法变成「返回新数组」版本。

8.3.2 sort 的默认排序陷阱 ​

⚠️ sort() 默认把元素转成字符串后按 Unicode 码点排序,数字排序会出错:

javascript
[3, 1, 4, 1, 5].sort(); // [1, 1, 3, 4, 5](单个数字看起来对)
[10, 9, 1].sort();      // [1, 10, 9](错误!按字符串 "1" < "10" < "9")

[10, 9, 1].sort((a, b) => a - b); // [1, 9, 10](数字排序必须传比较函数)

[3, 1, 2].toSorted(); // [1, 2, 3](ES2023,不修改原数组)

9. 对象与原型 ​

9.1 对象基础 ​

javascript
const person = {
  name: "Alice",
  age: 25,
  sayHello() { return `Hi, I'm ${this.name}`; }
};

// 访问
person.name;      // "Alice"
person["age"];    // 25

// 动态属性名(ES6)
const key = "email";
const user = { [key]: "a@b.com" };

// 属性简写(ES6)
const name = "Bob";
const user2 = { name }; // 等价于 { name: name }

// 增删改
person.job = "Engineer";
delete person.age;

// 检查
"name" in person;              // true
person.hasOwnProperty("name"); // true

// 遍历
Object.keys(person);    // ["name", "sayHello", "job"]
Object.values(person);
Object.entries(person);

9.2 对象展开与合并 ​

javascript
const base = { name: "Alice", age: 25 };
const details = { city: "Beijing" };
const full = { ...base, ...details, age: 26 };

9.3 解构赋值 ​

javascript
const person = { name: "Alice", age: 25 };
const { name, age } = person;
const { name: userName } = person;          // 重命名
const { job = "Unemployed" } = person;       // 默认值

// 嵌套解构
const user = { profile: { firstName: "A" } };
const { profile: { firstName } } = user;

// 函数参数解构
function printUser({ name, age }) {
  console.log(`${name} is ${age} years old`);
}

9.4 原型链 ​

javascript
const animal = { eat() { console.log("Eating"); } };
const dog = { bark() { console.log("Woof"); } };
Object.setPrototypeOf(dog, animal);
dog.eat();  // 继承自 animal

// 原型链:dog → animal → Object.prototype → null

10. 字符串与正则表达式 ​

10.1 字符串方法 ​

javascript
const str = "  Hello, JavaScript World!  ";

str.toUpperCase();          // "  HELLO, JAVASCRIPT WORLD!  "
str.toLowerCase();
str.trim();                 // "Hello, JavaScript World!"
str.includes("Java");       // true
str.startsWith("  H");      // true
str.indexOf("Java");        // 9
str.slice(2, 7);            // "Hello"
"a,b,c".split(",");         // ["a", "b", "c"]
["a", "b"].join("-");       // "a-b"
str.replace("World", "JS"); // 只替换第一个
str.replaceAll("a", "A");   // ES2021
"Hi".repeat(3);             // "HiHiHi"
"5".padStart(3, "0");       // "005"

// 模板字符串
const name = "Alice";
const msg = `Name: ${name}, Sum: ${1 + 2 + 3}`;
const html = `
  <div>
    <p>Hello</p>
  </div>
`;

10.2 正则表达式 ​

javascript
const re1 = /\d+/g;
const re2 = new RegExp("\\d+", "g");

// 常用 flags:g(全局)、i(忽略大小写)、m(多行)

/\d+/.test("Hello 123");           // true
"Hello 123".match(/\d+/g);         // ["123"]
"Hello 123".search(/\d+/);         // 6
"Hello 123".replace(/\d+/, "NUM"); // "Hello NUM"

10.2.1 常用元字符与分组速查 ​

语法含义示例
.任意单个字符(s 模式下含换行)/a.c/ 匹配 "abc"
\d \w \s数字 / 字母数字下划线 / 空白/\d+/ 匹配 "123"
\D \W \S对应取反/\D/ 匹配非数字
[abc] [a-z] [^a]字符类 / 范围 / 取反/[a-z]+/
* + ?0 次及以上 / 1 次及以上 / 0 或 1 次/ab*c/
{n} {n,} {n,m}精确 / 至少 / 区间次数/\d{2,4}/
^ $行首 / 行尾(m 模式按行)/^\d+$/
(x)捕获组/(ab)+/
(?:x)非捕获组(只分组不捕获)/(?:ab)+/
(?<name>x)命名捕获组(ES2018)/(?<y>\d{4})/
x(?=y) / x(?!y)正向前瞻 / 负向前瞻/\d(?=[a-z])/
(?<=y)x / (?<!y)x正向后顾 / 负向后顾(ES2018)/(?<=a)b/

常用 flags:

flag含义
g全局匹配
i忽略大小写
m多行模式,^/$ 匹配每行首尾
sdotAll,. 也匹配换行(ES2018)
uUnicode 模式
y粘性匹配(从 lastIndex 开始)
javascript
"1a 2b 3c".match(/\d(?=[a-z])/g); // ["1", "2", "3"](数字后跟字母)
"ab ab".match(/(?<=a)b/g);        // ["b", "b"](b 前面是 a)

// 命名捕获组
"2026-09-01".match(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/).groups;
// { y: "2026", m: "09", d: "01" }

10.2.2 正则的两个高频陷阱 ​

⚠️ 陷阱一:带 g/y flag 的正则实例有 lastIndex 状态。同一个正则复用 test()/exec() 时,会从上次结束的位置继续:

javascript
const re = /a/g;
re.test("a"); // true,lastIndex 变为 1
re.test("a"); // false!从索引 1 开始找不到 a

re.lastIndex = 0;
re.test("a"); // true(手动重置)

⚠️ 陷阱二:构造动态正则时,用户输入里的特殊字符需要转义。.、+、? 等会被当作元字符。ES2025 的 RegExp.escape() 可安全转义(见 11.12),在此之前需手写转义函数。

💡 遍历所有匹配(含捕获组)推荐用 String.prototype.matchAll(ES2020):

javascript
[..."a1b2c3".matchAll(/(\d)/g)].map(m => m[1]); // ["1", "2", "3"]

11. ES6+ 核心新特性 ​

ES6+ 通常指 ES2015 及之后的 ECMAScript 版本。ES2015 是现代 JavaScript 的分水岭,之后 ECMAScript 改为每年发布一次标准版本。

资料核对原则:

  • 语言标准进度以 TC39 finished proposals 和 ECMAScript 规范为准。
  • 日常学习和兼容性说明以 MDN 为主。
  • 新特性能否在项目中使用,要看浏览器、Node.js、构建工具和 Babel/SWC/Oxc/TypeScript 的支持情况。

11.1 ES6+ 年份总览 ​

年份版本代表特性
2015ES2015 / ES6let/const、箭头函数、模板字符串、解构、class、module、Promise、Generator、Map/Set、Symbol、Proxy、Reflect
2016ES2016Array.prototype.includes、指数运算符 **
2017ES2017async/await、Object.values/entries、字符串 padding、Object.getOwnPropertyDescriptors、尾逗号参数
2018ES2018对象 rest/spread、异步迭代、Promise.prototype.finally、正则增强
2019ES2019Array.prototype.flat/flatMap、Object.fromEntries、String.prototype.trimStart/trimEnd、可选 catch 绑定、稳定排序
2020ES2020BigInt、可选链 ?.、空值合并 ??、Promise.allSettled、动态 import()、globalThis、import.meta
2021ES2021逻辑赋值、数字分隔符、String.prototype.replaceAll、Promise.any、AggregateError、WeakRef、FinalizationRegistry
2022ES2022顶层 await、class fields、私有字段/方法、静态初始化块、Array.prototype.at、Object.hasOwn、Error cause
2023ES2023Change Array by Copy、findLast/findLastIndex、Hashbang Grammar、Symbols as WeakMap keys
2024ES2024Object.groupBy、Map.groupBy、Promise.withResolvers、ArrayBuffer.prototype.transfer、正则 v flag、String.prototype.isWellFormed/toWellFormed
2025ES2025Set 方法、Iterator helpers、RegExp.escape、Import Attributes、JSON modules、Promise.try、Float16Array、正则修饰符等
2026ES2026Array.fromAsync、Error.isError、Uint8Array Base64/Hex、Math.sumPrecise、JSON.parse source text access、Iterator.concat、Map/WeakMap upsert 等 finished proposals
2027+未来版本Temporal、Explicit Resource Management、Atomics.pause、Joint Iteration 等继续关注

注意:表格中的 ES2026 和 2027+ 信息来自 TC39 finished proposals 的 expected publication year。真实项目使用前必须检查运行时是否已经支持。

11.2 ES2015:现代 JavaScript 的基础 ​

let 和 const ​

let 和 const 是块级作用域,存在暂时性死区。

javascript
{
  let count = 1;
  const name = "Alice";
}

// console.log(count); // ReferenceError

实践建议:

  • 默认用 const。
  • 需要重新赋值时用 let。
  • 尽量不要再用 var,除非维护老代码。

箭头函数 ​

箭头函数没有自己的 this、arguments、super,也不能作为构造函数。

javascript
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2);

const user = {
  name: "Alice",
  sayLater() {
    setTimeout(() => {
      console.log(this.name);
    }, 100);
  },
};

不要把对象方法无脑写成箭头函数:

javascript
const user = {
  name: "Alice",
  // this 不指向 user
  say: () => console.log(this.name),
};

模板字符串 ​

支持多行和插值。

javascript
const name = "Alice";
const html = `
  <section>
    <h1>Hello, ${name}</h1>
  </section>
`;

标签模板可以自定义字符串处理逻辑,常见于 i18n、SQL 安全拼接、CSS-in-JS。

javascript
function tag(strings, value) {
  return `${strings[0]}${String(value).toUpperCase()}${strings[1]}`;
}

tag`hello ${"world"}!`; // "hello WORLD!"

解构赋值 ​

数组解构:

javascript
const [first, second] = ["a", "b"];

对象解构:

javascript
const user = { id: 1, profile: { name: "Alice" } };
const {
  id,
  profile: { name },
} = user;

函数参数解构:

javascript
function createUser({ name, role = "user" }) {
  return { name, role };
}

默认参数、剩余参数、展开语法 ​

javascript
function request(url, options = {}) {
  return { url, ...options };
}

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

const a = [1, 2];
const b = [...a, 3];
const merged = { ...{ a: 1 }, b: 2 };

注意:对象展开是浅拷贝。

javascript
const state = { user: { name: "Alice" } };
const next = { ...state };
next.user.name = "Bob";

console.log(state.user.name); // "Bob"

增强对象字面量 ​

javascript
const name = "Alice";
const key = "role";

const user = {
  name,
  [key]: "admin",
  sayHi() {
    console.log(`Hi, ${this.name}`);
  },
};

Class ​

class 是基于原型继承的语法层封装,不是 Java/C# 那种完全不同的对象模型。

javascript
class User {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    return `Hi, ${this.name}`;
  }
}

class Admin extends User {
  sayHi() {
    return `[Admin] ${super.sayHi()}`;
  }
}

Module ​

ES Module 是静态模块系统,支持静态分析、tree shaking 和顶层作用域隔离。

javascript
// math.js
export function add(a, b) {
  return a + b;
}

// main.js
import { add } from "./math.js";

关键特点:

  • import / export 必须在模块顶层。
  • 默认是严格模式。
  • 模块作用域不会污染全局。
  • 静态结构有利于构建工具分析。

Promise ​

Promise 表示一个异步操作的最终结果。

javascript
function getUser(id) {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

getUser(1)
  .then((user) => console.log(user))
  .catch((error) => console.error(error));

常用静态方法:

javascript
Promise.resolve(1);
Promise.reject(new Error("fail"));
Promise.all([p1, p2]);
Promise.race([p1, p2]);

Iterator、Iterable、for...of ​

可迭代对象实现 Symbol.iterator。

javascript
const range = {
  start: 1,
  end: 3,
  [Symbol.iterator]() {
    let current = this.start;
    return {
      next: () => {
        if (current <= this.end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
};

for (const n of range) {
  console.log(n);
}

Generator ​

Generator 函数可以暂停和恢复执行。

javascript
function* ids() {
  yield 1;
  yield 2;
  yield 3;
}

for (const id of ids()) {
  console.log(id);
}

Generator 常用于迭代器、状态机、早期异步流程库。现代业务异步通常优先用 async/await。

Map、Set、WeakMap、WeakSet ​

javascript
const map = new Map();
map.set({ id: 1 }, "object key");

const set = new Set([1, 2, 2, 3]); // Set {1, 2, 3}

const weakMap = new WeakMap();
const key = {};
weakMap.set(key, "metadata");

选择建议:

  • 键不是字符串,或者需要稳定插入顺序:用 Map。
  • 去重原始值:用 Set。
  • 需要和对象生命周期绑定的元数据:用 WeakMap。
  • WeakMap / WeakSet 不可遍历,因为键可能随垃圾回收消失。

Symbol ​

Symbol 创建唯一标识符,常用于避免属性名冲突,也用于语言内置协议。

javascript
const id = Symbol("id");
const user = {
  [id]: 1,
  name: "Alice",
};

内置 Symbol 示例:

javascript
const obj = {
  [Symbol.toStringTag]: "CustomObject",
};

Object.prototype.toString.call(obj); // "[object CustomObject]"

Proxy 与 Reflect ​

Proxy 可以拦截对象操作,Reflect 提供默认语义方法。

javascript
const user = { name: "Alice" };

const proxy = new Proxy(user, {
  get(target, key, receiver) {
    console.log("get", key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log("set", key, value);
    return Reflect.set(target, key, value, receiver);
  },
});

proxy.name;
proxy.name = "Bob";

Vue 3 响应式系统就是 Proxy 的典型应用方向之一。

11.3 ES2016:小而实用 ​

Array.prototype.includes ​

javascript
[1, 2, 3].includes(2); // true

和 indexOf 相比,includes 更语义化,并且能判断 NaN。

javascript
[NaN].includes(NaN); // true
[NaN].indexOf(NaN);  // -1

指数运算符 ​

javascript
2 ** 3; // 8

等价于:

javascript
Math.pow(2, 3);

11.4 ES2017:异步语法进入主流 ​

async/await ​

async 函数总是返回 Promise,await 会等待 Promise 结果并暂停当前 async 函数的后续执行。

javascript
async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error("Request failed");
  }
  return response.json();
}

并发请求不要写成串行:

javascript
// 慢
const user = await fetchUser();
const posts = await fetchPosts();

// 快
const [user2, posts2] = await Promise.all([
  fetchUser(),
  fetchPosts(),
]);

Object.values / Object.entries ​

javascript
const user = { id: 1, name: "Alice" };

Object.values(user);  // [1, "Alice"]
Object.entries(user); // [["id", 1], ["name", "Alice"]]

常用于对象转数组:

javascript
for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

字符串补全 ​

javascript
"5".padStart(2, "0"); // "05"
"5".padEnd(3, "-");   // "5--"

常用于日期、序号、表格对齐。

Object.getOwnPropertyDescriptors ​

一次性获取对象所有自有属性描述符。

javascript
const descriptors = Object.getOwnPropertyDescriptors(obj);

比 Object.assign 更适合复制 getter/setter 和属性特性。

11.5 ES2018:对象展开、异步迭代和正则增强 ​

对象 rest/spread ​

javascript
const user = { id: 1, name: "Alice", password: "secret" };
const { password, ...safeUser } = user;

const next = {
  ...safeUser,
  role: "admin",
};

注意仍是浅拷贝。

Promise.prototype.finally ​

javascript
fetch("/api/data")
  .then((res) => res.json())
  .catch((err) => console.error(err))
  .finally(() => {
    loading = false;
  });

适合关闭 loading、释放资源、记录日志。

异步迭代 for await...of ​

javascript
async function* createStream() {
  yield "a";
  yield "b";
}

for await (const chunk of createStream()) {
  console.log(chunk);
}

常用于流式数据、分页拉取、异步队列。

正则增强 ​

ES2018 增强了正则表达式能力,包括命名捕获组、dotAll、lookbehind、Unicode property escapes 等。

javascript
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = "2026-07-09".match(re);

console.log(match.groups.year); // "2026"

11.6 ES2019:数组扁平化与对象反转 ​

Array.prototype.flat / flatMap ​

javascript
[1, [2, [3]]].flat();     // [1, 2, [3]]
[1, [2, [3]]].flat(2);    // [1, 2, 3]

[1, 2, 3].flatMap((n) => [n, n * 2]);
// [1, 2, 2, 4, 3, 6]

Object.fromEntries ​

把键值对数组转回对象。

javascript
const entries = [["id", 1], ["name", "Alice"]];
Object.fromEntries(entries); // { id: 1, name: "Alice" }

配合 Object.entries 很适合处理对象转换。

javascript
const query = { page: "1", size: "20" };
const normalized = Object.fromEntries(
  Object.entries(query).map(([key, value]) => [key, Number(value)])
);

trimStart / trimEnd ​

javascript
"  hello  ".trimStart(); // "hello  "
"  hello  ".trimEnd();   // "  hello"

可选 catch 绑定 ​

javascript
try {
  JSON.parse(input);
} catch {
  console.log("invalid json");
}

如果不需要错误对象,可以省略 catch (error)。

11.7 ES2020:空值、安全访问和 BigInt ​

可选链 ?. ​

javascript
const city = user?.profile?.address?.city;
const result = api?.getUser?.(1);

只在左侧为 null 或 undefined 时短路。

不要滥用可选链掩盖数据结构错误。关键业务字段应该尽早校验。

空值合并 ?? ​

javascript
const pageSize = input.pageSize ?? 20;

只有左侧是 null 或 undefined 时才取右侧。它和 || 不同:

javascript
0 || 10;  // 10
0 ?? 10;  // 0

"" || "default"; // "default"
"" ?? "default"; // ""

BigInt ​

用于表示超过 Number.MAX_SAFE_INTEGER 的整数。

javascript
const id = 9007199254740993n;
const next = id + 1n;

注意:

  • BigInt 不能和 Number 直接混合运算。
  • JSON 默认不能序列化 BigInt。
  • 金融小数不要直接用 BigInt 解决,通常用整数分单位或 decimal 库。

Promise.allSettled ​

等待所有 Promise 都完成,无论成功失败。

javascript
const results = await Promise.allSettled([
  fetchUser(),
  fetchPosts(),
  fetchNotifications(),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log(result.value);
  } else {
    console.error(result.reason);
  }
}

适合首页多个模块独立加载、批量任务汇总。

动态 import() ​

javascript
const module = await import("./feature.js");
module.run();

常用于路由懒加载、按需加载重型库。

globalThis ​

跨环境访问全局对象。

javascript
globalThis.setTimeout(() => {
  console.log("works in browser and Node.js");
}, 0);

import.meta ​

模块元信息。浏览器和构建工具常用 import.meta.url,Vite 还扩展了 import.meta.env。

javascript
console.log(import.meta.url);

11.8 ES2021:逻辑赋值、Promise.any 和弱引用 ​

逻辑赋值运算符 ​

javascript
options.timeout ||= 3000;       // falsy 时赋值
cache.enabled &&= isAllowed;    // truthy 时赋值
config.baseUrl ??= "/api";      // null/undefined 时赋值

最常用的是 ??=,因为它不会误伤 0、""、false。

数字分隔符 ​

javascript
const amount = 1_000_000;
const mask = 0b1111_0000;

只影响可读性,不改变数值。

String.prototype.replaceAll ​

javascript
"a-b-c".replaceAll("-", "_"); // "a_b_c"

如果第一个参数是正则,必须带 g。

Promise.any 和 AggregateError ​

Promise.any 返回第一个 fulfilled 的结果,所有都 rejected 时抛出 AggregateError。

javascript
try {
  const fastest = await Promise.any([
    fetchFromMirrorA(),
    fetchFromMirrorB(),
  ]);
  console.log(fastest);
} catch (error) {
  console.log(error instanceof AggregateError);
  console.log(error.errors);
}

适合多个镜像源、多个备用服务,只要一个成功即可。

WeakRef 与 FinalizationRegistry ​

它们用于弱引用和对象被垃圾回收后的清理通知。

javascript
const ref = new WeakRef(obj);
const value = ref.deref();

实际业务中慎用。垃圾回收时机不可预测,不应该依赖它实现关键逻辑。

11.9 ES2022:Class 成熟化与顶层 await ​

Class fields、私有字段和私有方法 ​

javascript
class Counter {
  count = 0;
  static type = "counter";
  #step = 1;

  #log() {
    console.log(this.count);
  }

  increment() {
    this.count += this.#step;
    this.#log();
  }
}

私有字段是真私有,类外不能访问。

javascript
const counter = new Counter();
// counter.#step; // SyntaxError

静态初始化块 ​

javascript
class Config {
  static env;

  static {
    Config.env = globalThis.__APP_ENV__ ?? "development";
  }
}

用于复杂静态初始化逻辑。

顶层 await ​

只在 ES Module 顶层可用。

javascript
const config = await fetch("/config.json").then((res) => res.json());

export default config;

注意:顶层 await 会影响依赖它的模块加载时序。不要在公共基础模块里随意使用长耗时顶层 await。

Array.prototype.at ​

javascript
const list = [1, 2, 3];
list.at(0);  // 1
list.at(-1); // 3

字符串也支持:

javascript
"hello".at(-1); // "o"

Object.hasOwn ​

比 obj.hasOwnProperty(key) 更安全。

javascript
Object.hasOwn({ id: 1 }, "id"); // true

Error cause ​

保留错误因果链。

javascript
try {
  JSON.parse(input);
} catch (cause) {
  throw new Error("解析配置失败", { cause });
}

11.10 ES2023:不可变数组方法 ​

Change Array by Copy ​

这些方法返回新数组,不修改原数组。

javascript
const nums = [3, 1, 2];

nums.toSorted();              // [1, 2, 3]
nums.toReversed();            // [2, 1, 3]
nums.toSpliced(1, 1, 99);     // [3, 99, 2]
nums.with(0, 100);            // [100, 1, 2]

console.log(nums);            // [3, 1, 2]

它们非常适合 React/Vue 状态更新,因为不会原地修改旧数组。

findLast / findLastIndex ​

javascript
const logs = [
  { level: "info" },
  { level: "error" },
  { level: "info" },
];

logs.findLast((item) => item.level === "info");
logs.findLastIndex((item) => item.level === "error");

Hashbang Grammar ​

允许 JS 文件第一行使用 shebang:

javascript
#!/usr/bin/env node

console.log("CLI");

常用于 Node.js CLI 脚本。

11.11 ES2024:分组、Promise 工具和正则增强 ​

Object.groupBy ​

按回调返回值把数组分组成普通对象。

javascript
const orders = [
  { type: "book", price: 80 },
  { type: "food", price: 30 },
  { type: "book", price: 120 },
];

const grouped = Object.groupBy(orders, (order) => order.type);

console.log(grouped.book);

返回对象的 key 是字符串或 Symbol。如果分组 key 可能是对象,使用 Map.groupBy。

Map.groupBy ​

javascript
const vip = { name: "vip" };
const normal = { name: "normal" };

const users = [
  { name: "Alice", group: vip },
  { name: "Bob", group: normal },
];

const grouped = Map.groupBy(users, (user) => user.group);
console.log(grouped.get(vip));

Promise.withResolvers ​

返回 { promise, resolve, reject },方便把 Promise 的控制权暴露给外部。

javascript
const { promise, resolve, reject } = Promise.withResolvers();

setTimeout(() => resolve("done"), 100);

const result = await promise;

常见于事件桥接、队列、一次性信号。不要滥用成到处手动控制异步状态。

ArrayBuffer transfer ​

ArrayBuffer.prototype.transfer 可以转移底层内存,适合二进制数据处理场景。

javascript
const buffer = new ArrayBuffer(8);
const bigger = buffer.transfer(16);

使用前要检查运行时兼容性。

正则 v flag ​

v flag 是 Unicode sets 模式,增强 Unicode 字符集表达能力。

javascript
const re = /\p{Script=Greek}/v;

正则新特性兼容性通常比普通语法更需要谨慎确认。

String.prototype.isWellFormed / toWellFormed ​

用于检查和修复字符串中的不合法代理项。

javascript
const text = "\uD800";

text.isWellFormed(); // false
text.toWellFormed(); // "�"

适合处理 Unicode 边界、URL、编码转换等场景。

11.12 ES2025:Set 方法、迭代器增强和安全转义 ​

Set 方法 ​

ES2025 增加了一组集合运算方法。

javascript
const a = new Set([1, 2, 3]);
const b = new Set([3, 4]);

a.union(b);                   // Set {1, 2, 3, 4}
a.intersection(b);            // Set {3}
a.difference(b);              // Set {1, 2}
a.symmetricDifference(b);     // Set {1, 2, 4}
a.isSubsetOf(new Set([1, 2, 3, 4])); // true
a.isSupersetOf(new Set([1, 2]));     // true
a.isDisjointFrom(new Set([9]));      // true

适合权限集合、标签集合、差异比较。

Iterator helpers ​

迭代器 helper 让惰性数据处理更方便。

javascript
const result = Iterator.from([1, 2, 3, 4])
  .filter((n) => n % 2 === 0)
  .map((n) => n * 10)
  .toArray();

console.log(result); // [20, 40]

它和数组方法的区别是:迭代器处理可以是惰性的,更适合大数据流或无限序列。

RegExp.escape ​

把用户输入安全地转义为正则字面意义。

javascript
const keyword = "a+b?";
const re = new RegExp(RegExp.escape(keyword), "g");

"a+b? aab".match(re); // ["a+b?"]

这是搜索框、过滤器中非常实用的安全特性。

Import Attributes 与 JSON modules ​

Import Attributes 用来给 import 附加导入类型信息。JSON modules 场景常见:

javascript
import config from "./config.json" with { type: "json" };

注意:构建工具和运行时对 JSON modules 的支持策略可能不同,项目里要按 Vite/Webpack/Node/浏览器实际支持确认。

Promise.try ​

Promise.try 用于把同步异常和异步结果统一包装成 Promise 流程。

javascript
Promise.try(() => {
  return maybeSyncOrAsync();
}).then(handleResult, handleError);

它适合封装“可能同步抛错,也可能返回 Promise”的函数。

Float16Array 与 DataView float16 ​

提供 16 位浮点相关能力。

javascript
const values = new Float16Array([1.5, 2.5]);

常见于图形、机器学习、二进制数据压缩等领域。

11.13 ES2026:新近 finished proposals,使用前查兼容 ​

ES2026 相关特性在 TC39 finished proposals 中已经列出,但在真实项目里必须先查浏览器、Node.js、Babel/TypeScript/构建工具支持。

Error.isError ​

用于判断一个值是否是真正的 Error 对象,避免跨 realm 或伪对象判断问题。

javascript
if (Error.isError(value)) {
  console.log(value.message);
}

在支持前,项目通常仍会看到 value instanceof Error 或自定义错误判断。

Uint8Array Base64 / Hex ​

为二进制和字符串编码转换提供更直接的 API。

示意:

javascript
const bytes = Uint8Array.fromBase64("SGVsbG8=");
bytes.toBase64();
bytes.toHex();

适合文件、加密摘要、网络协议、二进制 payload。

Array.fromAsync ​

把异步可迭代对象、同步可迭代对象或类数组对象转换为数组,并返回 Promise。

javascript
async function* createPages() {
  yield "page-1";
  yield "page-2";
}

const pages = await Array.fromAsync(createPages());

它类似 for await...of 收集结果:

javascript
const result = [];
for await (const item of createPages()) {
  result.push(item);
}

Math.sumPrecise ​

用于更精确地求和浮点数组,减少普通累加的误差。

javascript
const total = Math.sumPrecise([0.1, 0.2, 0.3]);

注意:它不是任意精度 decimal,也不等于金融计算完整解决方案。

JSON.parse source text access ​

JSON.parse 的 reviver 可以访问原始 JSON 片段,有助于处理超大数字精度丢失问题。

javascript
const data = JSON.parse(
  '{"id": 999999999999999999}',
  (key, value, context) => {
    if (key === "id") {
      return BigInt(context.source);
    }
    return value;
  }
);

注意:context.source 是新能力,使用前要查兼容。

Map / WeakMap upsert ​

Upsert 方向的能力用于“没有就初始化,有就返回已有值”。具体 API 名称以最终运行时实现为准,例如提案中常见的是 getOrInsert / getOrInsertComputed 这类语义。

示意:

javascript
const map = new Map();
const value = map.getOrInsert?.("count", 0);

在支持前,常见写法仍是:

javascript
if (!map.has(key)) {
  map.set(key, createValue());
}

const value = map.get(key);

Iterator.concat ​

Iterator.concat 来自 Iterator Sequencing 提案,用于把多个迭代器按顺序串起来,适合组合多个惰性数据源。由于该能力非常新,生产使用前必须查运行时支持。

2027+ 继续关注的特性 ​

下面这些特性在 TC39 finished proposals 中 expected publication year 为 2027 或更晚,不应当当作 ES2026 稳定特性使用:

  • Temporal:新的日期时间 API,目标是解决 Date 的历史包袱。
  • Explicit Resource Management:using / await using,用于作用域结束时自动释放资源。
  • Atomics.pause:并发和共享内存相关的底层能力。
  • Joint Iteration:多迭代器联合迭代能力。

11.14 ES6+ 特性选择建议 ​

可以放心作为现代项目基础的特性 ​

  • let / const
  • 箭头函数
  • 模板字符串
  • 解构赋值
  • 默认参数、剩余参数、展开语法
  • class
  • Promise
  • async/await
  • ES Module
  • Map / Set
  • 可选链 ?.
  • 空值合并 ??
  • Array.prototype.at
  • Object.hasOwn

这些特性在现代浏览器、Node.js 和主流构建工具中已经非常常见。

使用前要看兼容性的特性 ​

  • Object.groupBy / Map.groupBy
  • Promise.withResolvers
  • Iterator helpers
  • Set 新方法
  • RegExp.escape
  • Import Attributes / JSON modules
  • ES2026 新特性

项目中使用这些能力前,建议检查:

  1. 目标浏览器版本。
  2. Node.js 版本。
  3. TypeScript 是否认识语法和类型。
  4. Babel/SWC/Oxc 是否能转换。
  5. Polyfill 是否可行。

不要只靠语法糖,要理解背后模型 ​

特性背后要理解
class原型链、构造函数、继承
Promise状态、thenable、微任务
async/awaitPromise、错误传播、并发控制
ESM静态依赖、live binding、循环依赖
Proxy拦截器、Reflect、对象内部方法
Iterator可迭代协议、惰性计算
Set/Map相等性、引用类型 key、插入顺序

11.15 官方资料 ​


12. 面向对象编程 ​

12.1 Class 基础(ES6) ​

javascript
class Person {
  static species = "Homo sapiens";      // 静态属性
  #ssn;                                 // 私有属性(ES2022)

  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() { return `Hi, I'm ${this.name}`; }

  get birthYear() {
    return new Date().getFullYear() - this.age;
  }

  static createAnonymous() {
    return new Person("Anonymous", 0);
  }
}

12.2 继承 ​

javascript
class Animal {
  constructor(name) { this.name = name; }
  speak() { console.log(`${this.name} makes a sound`); }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  speak() { console.log(`${this.name} barks!`); }
}

12.3 Mixin(混入) ​

Mixin 的核心含义是:把一组能力混入到对象、原型或类中,让多个对象复用同一批方法,而不强行建立父子继承关系。

它解决的问题不是“谁是谁的子类”,而是“这个对象是否拥有某种能力”。

例如:

  • Duck 可以游泳,也可以飞。
  • Robot 可以飞,但不是鸟。
  • UserController、OrderController 都需要日志能力,但它们不应该都继承同一个 LoggerBaseController。

这类能力更适合用 Mixin 表达。

12.3.1 最简单的对象混入 ​

javascript
const Swimmable = { swim() { console.log("swimming"); } };
const Flyable = { fly() { console.log("flying"); } };

class Duck {}

Object.assign(Duck.prototype, Swimmable, Flyable);

const duck = new Duck();
duck.swim();
duck.fly();

这里发生的事情是:

text
Duck.prototype
  + swim()
  + fly()

duck -> Duck.prototype -> Object.prototype

也就是说,swim 和 fly 被复制到了 Duck.prototype 上,实例 duck 通过原型链找到这些方法。

这不是多继承。JavaScript 仍然只有一条原型链,只是我们把多个对象的方法复制到了同一个原型对象上。

12.3.2 Mixin 与继承的区别 ​

对比点继承Mixin
关系is-a,表达父子关系can-do,表达能力组合
结构单一父类链路多个能力对象组合
方法来源来自父类原型链通常复制到目标对象或原型
适合场景稳定的分类体系日志、权限、缓存、事件、序列化等横向能力
风险父类变动影响子类命名冲突、来源不清晰

不要为了复用两三个方法就强行设计一个父类。父类应该表达稳定抽象,Mixin 更适合表达可插拔能力。

12.3.3 函数式 Mixin:给对象动态增强能力 ​

对象混入也可以写成函数,这样可以在运行时给不同对象添加能力。

javascript
function withTimestamp(target) {
  target.createdAt = new Date();
  target.getCreatedAt = function() {
    return this.createdAt.toISOString();
  };
  return target;
}

const task = withTimestamp({ title: "学习 Mixin" });

console.log(task.getCreatedAt());

这种方式适合增强普通对象,但要注意:

  • 会直接修改传入对象。
  • 方法如果写成普通函数,会依赖 this。
  • 多个 mixin 修改同名属性时,后写入的会覆盖先写入的。

如果不想修改原对象,可以返回新对象:

javascript
function withLogger(obj) {
  return {
    ...obj,
    log(message) {
      console.log(`[${this.name}] ${message}`);
    }
  };
}

const service = withLogger({ name: "UserService" });
service.log("created");

12.3.4 class Mixin:用类工厂组合能力 ​

在 ES6 class 中,常见写法是“接收一个基类,返回一个增强后的子类”。

javascript
const FlyableMixin = Base => class extends Base {
  fly() {
    console.log(`${this.name} is flying`);
  }
};

const SwimmableMixin = Base => class extends Base {
  swim() {
    console.log(`${this.name} is swimming`);
  }
};

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Duck extends FlyableMixin(SwimmableMixin(Animal)) {}

const duck = new Duck("Donald");
duck.fly();
duck.swim();

这个写法本质上仍然是单继承,只是每一层 Mixin 都返回一个新的子类:

text
Duck
  extends FlyableMixin(...)
    extends SwimmableMixin(...)
      extends Animal

优点是可以使用 super、私有字段、构造函数初始化等 class 能力。缺点是层级变深后调试会变复杂。

12.3.5 项目中常见的 Mixin 能力 ​

能力说明示例
日志能力给多个对象统一添加 log、warn、errorservice、controller
事件能力添加 on、off、emitUI 组件、插件系统
序列化能力添加 toJSON、fromJSONmodel、DTO
权限能力添加 can、hasRole用户对象、后台模块
缓存能力添加 getCache、setCacheAPI client、数据服务

示例:给多个服务添加事件发布能力。

javascript
const EventMixin = {
  on(eventName, handler) {
    this._events ??= new Map();
    const handlers = this._events.get(eventName) ?? [];
    handlers.push(handler);
    this._events.set(eventName, handlers);
  },
  emit(eventName, payload) {
    const handlers = this._events?.get(eventName) ?? [];
    handlers.forEach(handler => handler(payload));
  }
};

class UploadTask {}
Object.assign(UploadTask.prototype, EventMixin);

const task = new UploadTask();
task.on("progress", percent => console.log(percent));
task.emit("progress", 80);

12.3.6 Mixin 的风险 ​

Mixin 很灵活,但它的风险也很明显:

  1. 命名冲突:多个 mixin 都定义 init、log、destroy 时,后者可能覆盖前者。
  2. 来源不明确:看到 this.log() 时,不一定知道它来自哪个 mixin。
  3. 隐式依赖:某个 mixin 可能要求目标对象必须有 this.user、this.config,但代码表面看不出来。
  4. 类型推导困难:纯 JavaScript 中没有静态类型,调用方很难知道混入后对象新增了什么能力。
  5. 破坏封装:直接修改原型或对象,可能影响所有实例。

更稳妥的做法:

javascript
const LOGGER_KEY = Symbol("logger");

const LoggerMixin = {
  [LOGGER_KEY]: true,
  log(message) {
    console.log(message);
  }
};

function applyMixin(target, mixin) {
  for (const key of Reflect.ownKeys(mixin)) {
    if (key in target) {
      throw new Error(`Mixin conflict: ${String(key)}`);
    }
    Object.defineProperty(
      target,
      key,
      Object.getOwnPropertyDescriptor(mixin, key)
    );
  }
}

class Service {}
applyMixin(Service.prototype, LoggerMixin);

这里不用 Object.assign,是因为 Object.assign 只复制可枚举属性,并且会触发 getter;Object.defineProperty 可以更精确地复制属性描述符。

12.3.7 什么时候该用 Mixin ​

适合使用:

  • 多个类需要共享一组小而稳定的能力。
  • 能力是横向的,不属于某个父类抽象。
  • 方法之间依赖少,命名清晰。
  • 团队约定了冲突处理和命名规范。

不适合使用:

  • 只是为了偷懒复用大段业务逻辑。
  • mixin 依赖大量宿主对象属性。
  • 多个 mixin 之间存在复杂调用顺序。
  • 可以用普通函数、组合对象、模块导出解决。

在现代项目中,优先级通常是:

text
普通函数 / 模块函数
  > 组合对象
  > class Mixin
  > 修改原型的 Mixin
  > 深层继承

13. 异步编程 ​

13.1 同步 vs 异步 ​

javascript
console.log("1");
setTimeout(() => console.log("3"), 1000);
console.log("2");
// 输出:1 → 2 → 3

13.2 回调函数 ​

javascript
// 回调地狱
getUser(1, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {});
  });
});

13.3 Promise(ES6) ​

javascript
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve("成功"), 1000);
});

promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("完成"));

// 链式调用解决回调地狱
getUser(1)
  .then(user => getPosts(user.id))
  .then(posts => getComments(posts[0].id))
  .catch(err => console.error(err));

// 并行
Promise.all([p1, p2]);          // 全部成功
Promise.allSettled([p1, p2]);   // 全部完成
Promise.race([p1, p2]);         // 第一个完成
Promise.any([p1, p2]);          // 第一个成功

13.4 async/await(ES8) ​

javascript
async function fetchUser(id) {
  const response = await fetch(`/api/user/${id}`);
  return await response.json();
}

// 错误处理
async function getData() {
  try {
    const user = await fetchUser(1);
    const posts = await getPosts(user.id);
    return { user, posts };
  } catch (error) {
    console.error("失败:", error);
    throw error;
  }
}

// 并行请求
async function getDashboard() {
  const [user, posts] = await Promise.all([
    fetchUser(1), fetchPosts(1)
  ]);
  return { user, posts };
}

13.5 事件循环 ​

javascript
console.log("1");                       // 同步
setTimeout(() => console.log("2"), 0);  // 宏任务
Promise.resolve().then(() => console.log("3")); // 微任务
console.log("4");                       // 同步
// 输出:1 → 4 → 3 → 2

13.5.1 宏任务与微任务的执行顺序 ​

为什么 setTimeout(..., 0) 比 Promise.then 晚执行?因为浏览器主线程执行完一段同步代码(一个宏任务,例如整段 <script>)后,会先清空微任务队列,再去取下一个宏任务。

text
宏任务(一次一个)→ 清空微任务队列 → 渲染(浏览器)→ 下一个宏任务 → ...
队列常见来源
宏任务 Task<script>、setTimeout、setInterval、DOM 事件、I/O
微任务 MicrotaskPromise.then/catch/finally、queueMicrotask、MutationObserver

更完整的机制(调用栈、渲染时机、Node.js 六阶段)见《深入详解篇》第三章「事件循环的微观层面」。


14. 模块化 ​

14.1 ES Modules(现代标准) ​

javascript
// math.js
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }

// main.js
import multiply, { PI, add } from "./math.js";
import * as math from "./math.js";
const module = await import("./math.js"); // 动态导入

14.2 CommonJS(Node.js 传统方式) ​

javascript
// math.js
module.exports = { PI, add };
// main.js
const math = require("./math.js");

14.3 ESM vs CJS ​

特性ES ModulesCommonJS
语法import/exportrequire()/module.exports
加载静态(编译时)动态(运行时)
浏览器原生支持需要打包工具
树摇优化支持不支持

15. DOM 操作与 BOM ​

15.1 选择元素 ​

javascript
// 推荐
document.querySelector("#app");
document.querySelectorAll(".item");

// 旧方式
document.getElementById("app");
document.getElementsByClassName("item");

15.2 操作元素 ​

javascript
// 创建
const div = document.createElement("div");
div.textContent = "Hello";
div.innerHTML = "<strong>Bold</strong>";

// 插入
parent.appendChild(child);
parent.prepend(child);           // DOM API
parent.insertAdjacentHTML("beforeend", "<span>HTML</span>");

// 删除
element.remove();                // DOM API

// 类名
element.classList.add("active");
element.classList.remove("hidden");
element.classList.toggle("expanded");
element.classList.contains("active");

// 样式
element.style.color = "red";
element.style.cssText = "color: red; background: blue;";

15.3 属性操作 ​

javascript
element.getAttribute("data-id");
element.setAttribute("data-id", "123");
element.dataset.id;    // 访问 data-id 属性

15.4 BOM ​

javascript
window.innerWidth / innerHeight;   // 视口尺寸
location.href;                     // 当前 URL
location.reload();                 // 刷新
history.back(); history.forward();
navigator.userAgent;

setTimeout(() => {}, 1000);
setInterval(() => {}, 1000);
requestAnimationFrame(() => {});   // 高性能动画

16. 事件机制 ​

16.1 事件绑定 ​

javascript
element.addEventListener("click", (e) => {
  console.log("点击位置:", e.clientX, e.clientY);
});

element.removeEventListener("click", handler); // 移除

16.2 事件流:捕获 → 目标 → 冒泡 ​

javascript
// 第三个参数 true = 捕获阶段触发
parent.addEventListener("click", () => {}, true);
child.addEventListener("click", () => {});

// 事件委托
document.querySelector("ul").addEventListener("click", (e) => {
  if (e.target.tagName === "LI") {
    console.log("点击了 li");
  }
});

// 阻止默认行为和冒泡
e.preventDefault();
e.stopPropagation();

16.3 常用事件 ​

javascript
// 鼠标:click, dblclick, mouseenter, mouseleave
// 键盘:keydown, keyup
// 表单:submit, change, input, focus, blur
// 文档:DOMContentLoaded, load, resize, scroll
// 触屏:touchstart, touchmove, touchend

17. 错误处理与调试 ​

17.1 try...catch ​

javascript
try {
  riskyOperation();
} catch (error) {
  console.error(error.message);
  console.error(error.stack);
} finally {
  cleanup();
}

// 自定义错误
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

17.2 调试技巧 ​

javascript
console.log / warn / error / table / time / timeEnd / trace;
debugger;  // 代码断点

18. 浏览器存储 ​

18.1 localStorage(持久存储,约 5MB) ​

javascript
localStorage.setItem("key", JSON.stringify({ name: "Alice" }));
const data = JSON.parse(localStorage.getItem("key"));
localStorage.removeItem("key");
localStorage.clear();

18.2 sessionStorage(会话存储,标签页关闭即清除) ​

javascript
sessionStorage.setItem("temp", "临时数据");
javascript
document.cookie = "username=Alice; path=/; max-age=86400";

18.4 IndexedDB(存储大量结构化数据) ​

javascript
const request = indexedDB.open("MyDB", 1);
request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const store = db.createObjectStore("users", { keyPath: "id" });
};

19. 网络请求 ​

19.1 Fetch API(推荐) ​

javascript
// GET
const response = await fetch("/api/users");
const users = await response.json();

// POST
const res = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice" })
});

if (!res.ok) throw new Error(`HTTP ${res.status}`);
const result = await res.json();

// 上传文件
const formData = new FormData();
formData.append("file", fileInput.files[0]);
await fetch("/upload", { method: "POST", body: formData });

19.2 请求封装 ​

javascript
const api = {
  baseURL: "/api",
  async request(endpoint, options = {}) {
    const response = await fetch(`${this.baseURL}${endpoint}`, {
      headers: { "Content-Type": "application/json", ...options.headers },
      ...options
    });
    if (!response.ok) throw new Error("请求失败");
    return response.json();
  },
  get(url) { return this.request(url); },
  post(url, data) { return this.request(url, { method: "POST", body: JSON.stringify(data) }); }
};

20. 常用设计模式 ​

20.1 单例模式 ​

javascript
class Singleton {
  static #instance;
  constructor() {
    if (Singleton.#instance) return Singleton.#instance;
    Singleton.#instance = this;
  }
}

20.2 观察者模式(事件总线) ​

javascript
class EventBus {
  constructor() { this.events = {}; }
  on(event, cb) { (this.events[event] ||= []).push(cb); }
  emit(event, ...args) { this.events[event]?.forEach(cb => cb(...args)); }
}

20.3 工厂模式 ​

javascript
class UserFactory {
  static create(type, name) {
    const classes = { admin: Admin, guest: Guest };
    return new (classes[type])(name);
  }
}

20.4 Proxy 代理 ​

javascript
const handler = {
  get(obj, prop) { return prop in obj ? obj[prop] : "默认值"; }
};
const proxy = new Proxy(target, handler);

21. 性能优化基础 ​

21.1 防抖与节流 ​

javascript
// 防抖:多次触发只执行最后一次
function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// 节流:控制执行频率
function throttle(fn, limit = 300) {
  let inThrottle = false;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

21.2 常见优化策略 ​

  • 使用 DocumentFragment 批量 DOM 操作

  • 图片添加 loading="lazy"

  • 使用 WeakMap/WeakSet 避免内存泄漏

  • 及时取消事件监听

  • 代码分割:const module = await import("./heavy.js")

  • 使用 requestAnimationFrame 做动画


22. 学习路线总结 ​

22.1 推荐学习路线 ​

第一阶段:基础入门
├── 变量、数据类型、运算符
├── 控制流(if/else、循环)
├── 函数基础
└── 数组与对象基础

第二阶段:核心进阶
├── 函数高级(闭包、高阶函数)
├── 数组方法(map/filter/reduce)
├── 对象原型与 this
└── ES6+ 新特性

第三阶段:异步编程
├── 回调函数
├── Promise
├── async/await
└── 事件循环

第四阶段:DOM 与浏览器
├── DOM 操作
├── 事件机制
├── 浏览器存储
└── 网络请求

第五阶段:工程化
├── 模块化
├── 打包工具(Webpack/Vite)
├── 测试(Jest/Vitest)
└── TypeScript

第六阶段:框架与进阶
├── React / Vue / Angular
├── Node.js 后端
├── 性能优化
└── 设计模式

22.2 推荐资源 ​

  • MDN Web Docs:最权威的 JavaScript 文档

  • JavaScript.info:深入的 JS 教程

  • You Don't Know JS:深入 JS 核心机制

    22.3 常见面试题 ​

  1. 闭包是什么?应用场景?

  2. this 的指向规则?

  3. Promise 的三种状态?

  4. 事件循环机制(宏任务 vs 微任务)?

  5. 防抖和节流的区别?

  6. 深拷贝与浅拷贝?

  7. var/let/const 的区别?

  8. == 和 === 的区别?

  9. 原型链与继承?

  10. async/await 的原理?


23. 核心知识点深度补全 ​

本章专门补全高频但容易被写成“一句话介绍”的核心内容。学习时不要只记结论,要能画出对象查找链路、事件循环顺序,并能解释为什么某段代码会得到某个输出。

23.1 Map 与 Object 的区别 ​

Object 和 Map 都可以表达键值对,但目的不同。

对比项ObjectMap
主要用途表达结构化业务对象表达纯键值映射
key 类型字符串、Symbol;其他值会转为属性键任意值,包括对象、函数、NaN
顺序有属性枚举顺序规则按插入顺序迭代
大小Object.keys(obj).lengthmap.size
原型干扰普通对象继承 Object.prototype不存在属性名冲突
动态增删可以,但不是专门为频繁增删设计更适合频繁增删
JSON天然适合 JSON需要转换

对象作为 key 时,普通对象会被转成字符串,Map 保留引用身份。

javascript
const a = { id: 1 };
const b = { id: 1 };

const obj = {};
obj[a] = "A";
obj[b] = "B";
console.log(obj); // { "[object Object]": "B" }

const map = new Map();
map.set(a, "A");
map.set(b, "B");
console.log(map.get(a)); // A
console.log(map.get(b)); // B

Map 的 key 比较使用 SameValueZero 语义,所以 NaN 可以作为稳定 key。

javascript
const map = new Map();
map.set(NaN, "value");
console.log(map.get(NaN)); // value

常用 API:

javascript
const cache = new Map();

cache.set("user:1", { name: "Alice" });
cache.set("user:2", { name: "Bob" });

console.log(cache.get("user:1"));
console.log(cache.has("user:2"));
console.log(cache.size);

for (const [key, value] of cache) {
  console.log(key, value);
}

cache.delete("user:1");
cache.clear();

工程选择:

  • 表达用户、订单、配置这类结构化数据:用 Object。
  • 表达任意 key 到 value 的映射:用 Map。
  • 需要对象作为 key:用 Map 或 WeakMap。
  • 需要传给后端 JSON:普通对象更自然。

23.2 Set 与集合运算 ​

Set 表示不重复值的集合,核心语义是“成员是否存在”,不是“带下标的数组”。

javascript
const ids = new Set([1, 2, 2, 3]);
console.log(ids); // Set(3) { 1, 2, 3 }

ids.add(4);
ids.delete(2);
console.log(ids.has(3)); // true
console.log(ids.size); // 3

数组去重:

javascript
const numbers = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3]

注意:对象去重看引用,不看内容。

javascript
const set = new Set();
set.add({ id: 1 });
set.add({ id: 1 });
console.log(set.size); // 2

按业务字段去重可用 Map:

javascript
const users = [
  { id: 1, name: "Alice" },
  { id: 1, name: "Alice v2" },
  { id: 2, name: "Bob" },
];

const uniqueUsers = [...new Map(users.map(user => [user.id, user])).values()];
console.log(uniqueUsers);

集合运算:

javascript
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);

const union = new Set([...a, ...b]);
const intersection = new Set([...a].filter(item => b.has(item)));
const difference = new Set([...a].filter(item => !b.has(item)));

console.log([...union]);        // [1, 2, 3, 4, 5]
console.log([...intersection]); // [3]
console.log([...difference]);   // [1, 2]

适用场景:

  • 权限码集合。
  • 已选择 ID。
  • 搜索历史去重。
  • 图或树遍历中的 visited 集合。
  • 高频成员判断。

23.3 WeakMap 与 WeakSet ​

WeakMap 的 key 必须是对象,而且 key 是弱引用。如果外部已经没有引用这个对象,垃圾回收可以回收它,WeakMap 不会强行留住它。

javascript
let element = document.querySelector("#app");
const meta = new WeakMap();

meta.set(element, { mountedAt: Date.now() });

element.remove();
element = null;
// 如果没有其他引用,DOM 节点和元数据都可以被回收

为什么 WeakMap 不能遍历、没有 size?因为 key 是否还存在取决于垃圾回收,而垃圾回收时机不由业务代码控制。如果允许遍历,程序就能观察 GC 行为,语义会变得不稳定。

私有状态示例:

javascript
const privateState = new WeakMap();

class Counter {
  constructor() {
    privateState.set(this, { count: 0 });
  }

  increment() {
    privateState.get(this).count += 1;
  }

  get value() {
    return privateState.get(this).count;
  }
}

WeakSet 适合记录对象是否已经处理过:

javascript
const visited = new WeakSet();

function walk(node) {
  if (visited.has(node)) return;
  visited.add(node);
  // 继续处理 node
}

23.4 原型、构造函数与原型链 ​

JavaScript 对象有一个内部链接 [[Prototype]],指向另一个对象或 null。读取属性时,引擎先查自身,找不到就沿着 [[Prototype]] 向上查,这条链就是原型链。

javascript
const animal = {
  eat() {
    console.log("eat");
  },
};

const dog = Object.create(animal);
dog.name = "Lucky";

console.log(dog.name); // 自身属性
dog.eat();             // 从 animal 原型上找到

查找链路:

text
dog -> animal -> Object.prototype -> null

构造函数与 prototype:

javascript
function User(name) {
  this.name = name;
}

User.prototype.sayHi = function() {
  console.log(`Hi, ${this.name}`);
};

const user = new User("Alice");
user.sayHi();

new User("Alice") 大致做四件事:

  1. 创建一个新对象。
  2. 把新对象的 [[Prototype]] 指向 User.prototype。
  3. 用新对象作为 this 执行构造函数。
  4. 如果构造函数没有显式返回对象,就返回这个新对象。

模拟 new:

javascript
function myNew(Constructor, ...args) {
  const instance = Object.create(Constructor.prototype);
  const result = Constructor.apply(instance, args);
  return result !== null && (typeof result === "object" || typeof result === "function")
    ? result
    : instance;
}

prototype 和 __proto__ 不要混:

名称属于谁含义
prototype函数对象作为构造函数创建实例时,实例原型会指向它
__proto__普通对象也可访问访问对象内部 [[Prototype]] 的历史兼容入口
Object.getPrototypeOf(obj)标准 API获取对象原型
Object.setPrototypeOf(obj, proto)标准 API设置对象原型,热路径中慎用

class 本质仍基于原型:

javascript
class User {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    console.log(`Hi, ${this.name}`);
  }
}

const user = new User("Alice");
console.log(Object.getPrototypeOf(user) === User.prototype); // true

23.5 this 与原型链 ​

原型链决定“方法从哪里找到”,this 决定“方法执行时操作谁”。

javascript
const animal = {
  name: "animal",
  sayName() {
    console.log(this.name);
  },
};

const dog = Object.create(animal);
dog.name = "dog";

dog.sayName(); // dog

虽然 sayName 从 animal 上找到,但调用形式是 dog.sayName(),所以 this 是 dog。

丢失 this:

javascript
const fn = dog.sayName;
fn(); // 严格模式下 this 是 undefined

修复:

javascript
const bound = dog.sayName.bind(dog);
bound(); // dog

23.6 事件循环:调用栈、宏任务、微任务 ​

JavaScript 主线程一次只能执行一段同步代码。异步能力来自宿主环境:浏览器提供定时器、网络、DOM 事件;Node.js 提供文件 I/O、网络 I/O、定时器等。

核心结构:

结构作用
调用栈 Call Stack当前正在执行的同步函数调用
宏任务队列 Task Queuescript、setTimeout、setInterval、I/O、UI 事件等
微任务队列 Microtask QueuePromise.then/catch/finally、queueMicrotask、MutationObserver 等
渲染步骤浏览器在合适时机执行样式计算、布局、绘制

一次常见浏览器事件循环:

  1. 执行一个宏任务,例如整段 script。
  2. 调用栈清空。
  3. 清空所有微任务;微任务中继续创建微任务,也继续清空。
  4. 浏览器根据需要渲染。
  5. 取下一个宏任务继续。

经典题:

javascript
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

queueMicrotask(() => {
  console.log("D");
});

console.log("E");

输出:

text
A
E
C
D
B

原因:

  1. A 同步输出。
  2. setTimeout 注册宏任务。
  3. Promise.then 注册微任务。
  4. queueMicrotask 注册微任务。
  5. E 同步输出。
  6. 当前 script 宏任务结束,清空微任务,输出 C、D。
  7. 执行下一个宏任务,输出 B。

23.7 async/await 与微任务 ​

async 函数总是返回 Promise。await 会暂停当前 async 函数,把后续代码安排到异步延续中,通常通过微任务恢复执行。

javascript
async function demo() {
  console.log("2");
  await Promise.resolve();
  console.log("4");
}

console.log("1");
demo();
console.log("3");

输出:

text
1
2
3
4

执行过程:

  1. 输出 1。
  2. 调用 demo,同步执行到 await 前,输出 2。
  3. await Promise.resolve() 后面的代码被安排到微任务中。
  4. 回到外层同步代码,输出 3。
  5. 同步代码结束,微任务恢复 demo,输出 4。

并发请求不要写成不必要的串行:

javascript
// 慢:第二个请求必须等第一个完成
const user = await fetchUser();
const posts = await fetchPosts();

// 快:两个请求同时开始
const [user2, posts2] = await Promise.all([
  fetchUser(),
  fetchPosts(),
]);

需要尽量拿到所有结果时:

javascript
const results = await Promise.allSettled([
  fetchUser(),
  fetchPosts(),
  fetchNotifications(),
]);

23.8 宏任务与微任务来源 ​

类型浏览器常见来源Node.js 常见来源
宏任务script、setTimeout、setInterval、DOM 事件、MessageChannel、网络回调setTimeout、setInterval、setImmediate、I/O 回调
微任务Promise.then、queueMicrotask、MutationObserverPromise.then、queueMicrotask、process.nextTick

process.nextTick 是 Node.js 特有机制,优先级通常高于 Promise 微任务。浏览器没有 process.nextTick。

23.9 微任务饥饿 ​

如果一个微任务不断创建新的微任务,浏览器会一直清空微任务队列,导致渲染和宏任务迟迟得不到执行。

javascript
function loop() {
  queueMicrotask(loop);
}

loop();

这段代码会让页面很难响应。需要分片执行时,可以使用 setTimeout、requestAnimationFrame、requestIdleCallback 或专门调度器。

23.10 学习检查题 ​

  1. Map 为什么可以用对象作为 key,而普通对象不适合?
  2. Set 去重对象数组为什么会失败?如何按 id 去重?
  3. WeakMap 为什么不能遍历?
  4. prototype 和 __proto__ 分别是什么?
  5. new 操作符内部做了什么?
  6. 方法从原型上找到时,this 一定指向原型对象吗?
  7. 为什么 Promise.then 比 setTimeout(..., 0) 更早执行?
  8. await 前后的代码分别在什么时候执行?
  9. 微任务中继续创建微任务会发生什么?
  10. 浏览器事件循环和 Node.js 事件循环有哪些差异?

本文档持续更新中。建议结合 MDN 和官方文档学习最新特性。

基于 VitePress 构建的个人学习文档知识库