外观
JavaScript 系统学习文档
从零开始,系统掌握 JavaScript 核心知识 版本:ES6+ 为主,2026-07-09 已补充 ES2015 到 ES2026 的主要新增特性。具体兼容性请以 MDN、Can I use、Node.js/V8/浏览器版本为准。
目录
1. JavaScript 简介与历史
1.1 什么是 JavaScript
JavaScript(简称 JS)是一种轻量级的、解释型或即时编译型的编程语言。它是 Web 开发的三大核心技术之一(HTML 定义内容、CSS 定义样式、JavaScript 定义行为)。
1.2 发展简史
| 时间 | 事件 |
|---|---|
| 1995 | Brendan Eich 在 Netscape 创造了 JavaScript(最初叫 Mocha → LiveScript → JavaScript) |
| 1997 | ECMAScript 1 标准发布 |
| 2009 | ES5 发布,带来严格模式、JSON 支持等 |
| 2015 | ES6/ES2015 发布——现代 JavaScript 的里程碑 |
| 2016-现在 | 每年发布一个新版本(ES2016 之后持续演进,本文补充到 ES2026 主要特性) |
| 2009 | Node.js 诞生,JavaScript 可以运行在服务器端 |
| 2012 | TypeScript 首次公开发布(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 文件:
bashnode index.js或者进入交互模式:
bashnode > 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_CASE MAX_SIZE,API_URL常量 3.5 输入与输出
javascriptconsole.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 基本数据类型
| 类型 | 示例 | 说明 |
|---|---|---|
| Number | 42, 3.14, Infinity, NaN | 所有数字,不区分整数/浮点 |
| String | "hello", 'world', `template` | 字符串 |
| Boolean | true, false | 布尔值 |
| Null | null | 空值(主动设置) |
| Undefined | undefined | 未定义 |
| Symbol | Symbol("desc") | 唯一标识符(ES6) |
| BigInt | 999n, 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 位)。这带来三个必须记住的结论:
- 小数运算可能不精确:
0.1 + 0.2不等于0.3,因为十进制小数在二进制里无法精确表示。 - 整数有安全范围:超过
Number.MAX_SAFE_INTEGER(2^53 - 1=9007199254740991)后,整数会丢失精度。 - 有特殊值:
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) |
|---|---|---|---|---|
| 结果 | true | false | true | true |
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)
// 建议:总是显式传第二个参数 radix4.4.3 == 的隐式转换算法(为什么建议用 ===)
== 在类型不同时会按规则转换,规则多且反直觉。核心顺序:
- 类型相同 → 直接按
===比较。 null == undefined→true(两者互相相等,但不等于任何其他值)。number == string→ 把字符串转成数字再比。boolean == 其他→ 先把布尔转成数字(true→ 1、false→ 0)。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); // nullstructuredClone(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 → 85.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); // true5.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)); // 37.3 作用域与闭包
javascript
// 闭包:函数可以访问外部作用域的变量
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 27.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)); // 107.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); // 1208.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 → null10. 字符串与正则表达式
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 | 多行模式,^/$ 匹配每行首尾 |
s | dotAll,. 也匹配换行(ES2018) |
u | Unicode 模式 |
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+ 年份总览
| 年份 | 版本 | 代表特性 |
|---|---|---|
| 2015 | ES2015 / ES6 | let/const、箭头函数、模板字符串、解构、class、module、Promise、Generator、Map/Set、Symbol、Proxy、Reflect |
| 2016 | ES2016 | Array.prototype.includes、指数运算符 ** |
| 2017 | ES2017 | async/await、Object.values/entries、字符串 padding、Object.getOwnPropertyDescriptors、尾逗号参数 |
| 2018 | ES2018 | 对象 rest/spread、异步迭代、Promise.prototype.finally、正则增强 |
| 2019 | ES2019 | Array.prototype.flat/flatMap、Object.fromEntries、String.prototype.trimStart/trimEnd、可选 catch 绑定、稳定排序 |
| 2020 | ES2020 | BigInt、可选链 ?.、空值合并 ??、Promise.allSettled、动态 import()、globalThis、import.meta |
| 2021 | ES2021 | 逻辑赋值、数字分隔符、String.prototype.replaceAll、Promise.any、AggregateError、WeakRef、FinalizationRegistry |
| 2022 | ES2022 | 顶层 await、class fields、私有字段/方法、静态初始化块、Array.prototype.at、Object.hasOwn、Error cause |
| 2023 | ES2023 | Change Array by Copy、findLast/findLastIndex、Hashbang Grammar、Symbols as WeakMap keys |
| 2024 | ES2024 | Object.groupBy、Map.groupBy、Promise.withResolvers、ArrayBuffer.prototype.transfer、正则 v flag、String.prototype.isWellFormed/toWellFormed |
| 2025 | ES2025 | Set 方法、Iterator helpers、RegExp.escape、Import Attributes、JSON modules、Promise.try、Float16Array、正则修饰符等 |
| 2026 | ES2026 | Array.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"); // trueError 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.atObject.hasOwn
这些特性在现代浏览器、Node.js 和主流构建工具中已经非常常见。
使用前要看兼容性的特性
Object.groupBy/Map.groupByPromise.withResolvers- Iterator helpers
- Set 新方法
RegExp.escape- Import Attributes / JSON modules
- ES2026 新特性
项目中使用这些能力前,建议检查:
- 目标浏览器版本。
- Node.js 版本。
- TypeScript 是否认识语法和类型。
- Babel/SWC/Oxc 是否能转换。
- Polyfill 是否可行。
不要只靠语法糖,要理解背后模型
| 特性 | 背后要理解 |
|---|---|
class | 原型链、构造函数、继承 |
Promise | 状态、thenable、微任务 |
async/await | Promise、错误传播、并发控制 |
| ESM | 静态依赖、live binding、循环依赖 |
| Proxy | 拦截器、Reflect、对象内部方法 |
| Iterator | 可迭代协议、惰性计算 |
| Set/Map | 相等性、引用类型 key、插入顺序 |
11.15 官方资料
- TC39 finished proposals:https://github.com/tc39/proposals/blob/main/finished-proposals.md
- ECMAScript 规范:https://tc39.es/ecma262/
- MDN JavaScript 参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
- MDN JavaScript 指南:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide
- MDN 可选链:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
- MDN 空值合并:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
- MDN Promise:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- MDN Modules:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- MDN Classes:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
- MDN Object.groupBy:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy
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、error | service、controller |
| 事件能力 | 添加 on、off、emit | UI 组件、插件系统 |
| 序列化能力 | 添加 toJSON、fromJSON | model、DTO |
| 权限能力 | 添加 can、hasRole | 用户对象、后台模块 |
| 缓存能力 | 添加 getCache、setCache | API 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 很灵活,但它的风险也很明显:
- 命名冲突:多个 mixin 都定义
init、log、destroy时,后者可能覆盖前者。 - 来源不明确:看到
this.log()时,不一定知道它来自哪个 mixin。 - 隐式依赖:某个 mixin 可能要求目标对象必须有
this.user、this.config,但代码表面看不出来。 - 类型推导困难:纯 JavaScript 中没有静态类型,调用方很难知道混入后对象新增了什么能力。
- 破坏封装:直接修改原型或对象,可能影响所有实例。
更稳妥的做法:
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 → 313.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 → 213.5.1 宏任务与微任务的执行顺序
为什么 setTimeout(..., 0) 比 Promise.then 晚执行?因为浏览器主线程执行完一段同步代码(一个宏任务,例如整段 <script>)后,会先清空微任务队列,再去取下一个宏任务。
text
宏任务(一次一个)→ 清空微任务队列 → 渲染(浏览器)→ 下一个宏任务 → ...| 队列 | 常见来源 |
|---|---|
| 宏任务 Task | <script>、setTimeout、setInterval、DOM 事件、I/O |
| 微任务 Microtask | Promise.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 Modules | CommonJS |
|---|---|---|
| 语法 | import/export | require()/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, touchend17. 错误处理与调试
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", "临时数据");18.3 Cookie(约 4KB,自动发送到服务器)
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 常见面试题
闭包是什么?应用场景?
this 的指向规则?
Promise 的三种状态?
事件循环机制(宏任务 vs 微任务)?
防抖和节流的区别?
深拷贝与浅拷贝?
var/let/const 的区别?
== 和 === 的区别?
原型链与继承?
async/await 的原理?
23. 核心知识点深度补全
本章专门补全高频但容易被写成“一句话介绍”的核心内容。学习时不要只记结论,要能画出对象查找链路、事件循环顺序,并能解释为什么某段代码会得到某个输出。
23.1 Map 与 Object 的区别
Object 和 Map 都可以表达键值对,但目的不同。
| 对比项 | Object | Map |
|---|---|---|
| 主要用途 | 表达结构化业务对象 | 表达纯键值映射 |
| key 类型 | 字符串、Symbol;其他值会转为属性键 | 任意值,包括对象、函数、NaN |
| 顺序 | 有属性枚举顺序规则 | 按插入顺序迭代 |
| 大小 | Object.keys(obj).length | map.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)); // BMap 的 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") 大致做四件事:
- 创建一个新对象。
- 把新对象的
[[Prototype]]指向User.prototype。 - 用新对象作为
this执行构造函数。 - 如果构造函数没有显式返回对象,就返回这个新对象。
模拟 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); // true23.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(); // dog23.6 事件循环:调用栈、宏任务、微任务
JavaScript 主线程一次只能执行一段同步代码。异步能力来自宿主环境:浏览器提供定时器、网络、DOM 事件;Node.js 提供文件 I/O、网络 I/O、定时器等。
核心结构:
| 结构 | 作用 |
|---|---|
| 调用栈 Call Stack | 当前正在执行的同步函数调用 |
| 宏任务队列 Task Queue | script、setTimeout、setInterval、I/O、UI 事件等 |
| 微任务队列 Microtask Queue | Promise.then/catch/finally、queueMicrotask、MutationObserver 等 |
| 渲染步骤 | 浏览器在合适时机执行样式计算、布局、绘制 |
一次常见浏览器事件循环:
- 执行一个宏任务,例如整段 script。
- 调用栈清空。
- 清空所有微任务;微任务中继续创建微任务,也继续清空。
- 浏览器根据需要渲染。
- 取下一个宏任务继续。
经典题:
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原因:
A同步输出。setTimeout注册宏任务。Promise.then注册微任务。queueMicrotask注册微任务。E同步输出。- 当前 script 宏任务结束,清空微任务,输出
C、D。 - 执行下一个宏任务,输出
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。 - 调用
demo,同步执行到await前,输出2。 await Promise.resolve()后面的代码被安排到微任务中。- 回到外层同步代码,输出
3。 - 同步代码结束,微任务恢复
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、MutationObserver | Promise.then、queueMicrotask、process.nextTick |
process.nextTick 是 Node.js 特有机制,优先级通常高于 Promise 微任务。浏览器没有 process.nextTick。
23.9 微任务饥饿
如果一个微任务不断创建新的微任务,浏览器会一直清空微任务队列,导致渲染和宏任务迟迟得不到执行。
javascript
function loop() {
queueMicrotask(loop);
}
loop();这段代码会让页面很难响应。需要分片执行时,可以使用 setTimeout、requestAnimationFrame、requestIdleCallback 或专门调度器。
23.10 学习检查题
Map为什么可以用对象作为 key,而普通对象不适合?Set去重对象数组为什么会失败?如何按id去重?WeakMap为什么不能遍历?prototype和__proto__分别是什么?new操作符内部做了什么?- 方法从原型上找到时,
this一定指向原型对象吗? - 为什么
Promise.then比setTimeout(..., 0)更早执行? await前后的代码分别在什么时候执行?- 微任务中继续创建微任务会发生什么?
- 浏览器事件循环和 Node.js 事件循环有哪些差异?
本文档持续更新中。建议结合 MDN 和官方文档学习最新特性。