本文目标:系统整理 ES6 及后续版本的核心特性,面向面试复习,兼顾知识体系完整性。
风格:原理讲解 + 代码示例 + 面试题标注。
定位:ES6 知识的“索引 + 系统整理”中枢,与已有文章交叉链接,避免重复内容。
标注约定:
面试题: 表示高频面试问题相关阅读: 表示可跳转旧文深入面试频率: 表示复习优先级ES6 全称 ECMAScript 6,是 ECMAScript 标准的第 6 版。2015 年正式发布后,标准委员会决定改为按年份命名,因此:
但日常交流中,很多人说的“ES6+”并不是严格指 ES2015 之后的每一个版本,而是泛指:
ES2015 及之后所有现代 JavaScript 新特性的统称。
| 版本 | 代表特性 |
|---|---|
| ES2015 / ES6 | let / const、箭头函数、模板字符串、解构、默认参数、rest、扩展运算符、Class、模块化、Promise、Symbol、Set、Map、Iterator、Generator、Proxy、Reflect |
| ES2016 | 指数运算符 **、Array.prototype.includes |
| ES2017 | async/await、Object.values/entries、padStart/padEnd、Object.getOwnPropertyDescriptors |
| ES2018 | Promise.prototype.finally、对象扩展运算符 {...obj}、异步迭代 |
| ES2019 | flat/flatMap、Object.fromEntries、trimStart/trimEnd |
| ES2020 | 可选链 ?.、空值合并 ??、BigInt、Promise.allSettled、globalThis、String.prototype.matchAll |
| ES2021 | 逻辑赋值运算符 ??= / &&= / ||=、数字分隔符 1_000_000 |
| ES2022 | Array.prototype.at、类私有字段 #field、顶层 await、静态块 |
面试频率:⭐⭐
快速带过即可,重点是知道版本关系与特性归属。
面试频率:⭐⭐⭐⭐⭐ 重点写
| 对比项 | var | let | const |
|---|---|---|---|
| 作用域 | 全局 / 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升,初始化为 undefined | 提升,但存在 TDZ | 提升,但存在 TDZ |
| 暂时性死区 | 无 | 有 | 有 |
| 重复声明 | 允许 | 同一作用域不允许 | 同一作用域不允许 |
| 必须初始化 | 否 | 否 | 是 |
| 是否挂到全局对象 | 浏览器全局 var 会挂到 window | 不会 | 不会 |
| 可否修改 | 可 | 可 | 绑定不可重新赋值 |
很多资料只说“var 会提升,let/const 不会”,这个说法并不准确。更准确的说法是:
所有声明都会被提升,区别在于初始化时机和能否访问。
当 JavaScript 引擎进入一个执行上下文(全局或函数)时,会经历两个阶段:
在创建阶段:
var 声明的变量被登记后立即初始化为 undefinedlet/const 声明的变量被登记后不初始化,处于“未初始化”状态js
|
1 2 3 4 5 |
console.log(a); // undefined console.log(fn); // [Function: fn] var a = 1; function fn() {} |
js
|
1 2 |
console.log(b); // ReferenceError: Cannot access 'b' before initialization let b = 2; |
let/const 的变量其实也在环境里“存在”,只是不能访问,这个“存在但不可访问”的区间就叫 暂时性死区(TDZ)。
TDZ 是为了修复 var 的历史遗留问题。var 允许在声明前访问,得到 undefined,这会导致 bug 很难被发现:js
|
1 2 3 4 |
function f() { console.log(value); // undefined,但开发者可能期望报错 var value = 1; } |
TDZ 让这种代码直接抛出 ReferenceError,提前暴露问题。
块级作用域不只是“{} 内声明外面访问不到”,它解决了两个经典问题。
js
|
1 2 3 4 |
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 3 3 3 |
var 是函数作用域,整个循环只有 一个 i,三个回调共享它,循环结束后 i 变成 3。js
|
1 2 3 4 |
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 0 1 2 |
let 在每次迭代时创建一个新的绑定,可以理解为:js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 伪代码:let 循环的等价形式 { let i = 0; setTimeout(() => console.log(i), 0); } { let i = 1; setTimeout(() => console.log(i), 0); } { let i = 2; setTimeout(() => console.log(i), 0); } |
js
|
1 2 3 4 5 6 7 8 9 |
var name = 'outer'; function f() { console.log(name); // undefined,而不是 'outer' if (false) { var name = 'inner'; } } f(); |
因为 var name 提升到函数顶部,遮蔽了外层的 name。如果用 let:js
|
1 2 3 4 5 6 7 8 9 |
let name = 'outer'; function f() { console.log(name); // 'outer' if (false) { let name = 'inner'; } } f(); |
const 保证的是:
变量绑定指向的内存地址不可变,而不是值本身不可变。
js
|
1 2 3 |
const obj = { name: 'Tom' }; obj.name = 'Jerry'; // 可以 // obj = {}; // TypeError: Assignment to constant variable. |
从内存角度看:js
|
1 2 3 4 |
const obj = { name: 'Tom' }; // obj 指向一个内存地址 0x100 // obj.name = 'Jerry' 修改的是 0x100 处的对象内容 // obj = {} 是让 obj 指向新的地址,这是被禁止的 |
需要 Object.freeze(),但它是浅冻结:js
|
1 2 3 |
const obj = Object.freeze({ a: 1, nested: { b: 2 } }); obj.a = 2; // 严格模式报错,非严格模式静默失败 obj.nested.b = 3; // 仍然可以修改,因为 nested 没有被冻结 |
深冻结需要递归:js
|
1 2 3 4 5 6 7 8 9 |
function deepFreeze(obj) { Object.getOwnPropertyNames(obj).forEach(name => { const value = obj[name]; if (value && typeof value === 'object') { deepFreeze(value); } }); return Object.freeze(obj); } |
js
|
1 2 3 4 5 |
var a = 1; let b = 2; console.log(window.a); // 1 console.log(window.b); // undefined |
var 会挂到 window 对象上let/const 存在于全局词法环境,不挂到 window这在实际开发中有影响,比如:js
|
1 2 |
var undefined = 1; // 老代码可能出问题 let undefined = 1; // SyntaxError |
for 循环中 let 到底创建了几个变量?取决于循环的迭代次数。每次迭代都会创建一个新的块级绑定,但循环头部的声明只执行一次。
let 和 const 哪个性能更好?在 V8 等现代引擎中,两者性能几乎相同。const 在语义上更明确,推荐优先使用,能避免意外重新赋值。
const 声明后可以修改属性吗?可以删除属性吗?可以修改属性,也可以删除属性,只要不改变 const 变量本身的指向。js
|
1 2 3 |
const obj = { a: 1 }; delete obj.a; // 可以 console.log(obj); // {} |
面试频率:⭐⭐⭐⭐
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const arr = [1, 2, 3]; const [a, b, c] = arr; console.log(a, b, c); // 1 2 3 // 默认值:只有 undefined 才触发 const [x = 10, y = 20] = [1]; console.log(x, y); // 1 20 const [m = 10] = [null]; console.log(m); // null,null 不触发默认值 // 嵌套解构 const [p, [q, r]] = [1, [2, 3]]; console.log(p, q, r); // 1 2 3 // 与扩展运算符配合 const [first, ...rest] = [1, 2, 3, 4]; console.log(first, rest); // 1 [2, 3, 4] // 交换变量 let n1 = 1, n2 = 2; [n1, n2] = [n2, n1]; console.log(n1, n2); // 2 1 |
[a, b] = [b, a] 能交换变量?数组解构会先求值右侧,构造一个新数组 [b, a],然后依次赋值给左侧。等价于:js
|
1 2 3 |
const temp = [b, a]; a = temp[0]; b = temp[1]; |
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const user = { name: 'Tom', age: 18, address: { city: 'Beijing' } }; const { name, age } = user; // 重命名 const { name: userName } = user; console.log(userName); // Tom // 默认值 const { gender = 'male' } = user; // 嵌套解构 const { address: { city } } = user; console.log(city); // Beijing // rest 提取剩余属性 const { name: n, ...others } = user; console.log(others); // { age: 18, address: {...} } |
注意:对象解构赋值给已声明变量时,需要用括号包裹,否则 { 会被解析为块语句。js
|
1 2 3 |
let a; ({ a } = { a: 1 }); // 正确 // { a } = { a: 1 }; // SyntaxError |
解构赋值对基本类型是值拷贝,对引用类型是引用拷贝,所以是浅拷贝。js
|
1 2 3 4 |
const obj = { nested: { x: 1 } }; const { nested } = obj; nested.x = 2; console.log(obj.nested.x); // 2 |
js
|
1 2 3 4 5 6 |
function createUser({ name = 'anonymous', age = 0 } = {}) { return { name, age }; } createUser(); // { name: 'anonymous', age: 0 } createUser({ name: 'Tom' }); // { name: 'Tom', age: 0 } |
{ a } = {}?因为如果调用时不传参数,形参为 undefined,对 undefined 解构会报错:js
|
1 2 |
function f({ a }) {} f(); // TypeError: Cannot destructure property 'a' of undefined |
加上 = {} 后,不传参时形参默认是 {},解构成功,a 取默认值。
js
|
1 2 3 4 5 6 7 8 9 |
// 陷阱一:解构 null/undefined const { a } = null; // TypeError const [b] = undefined; // TypeError // 陷阱二:函数返回值解构时未判空 function getUser() { return null; } const { name } = getUser() || {}; // 安全写法 |
面试频率:⭐⭐⭐⭐⭐ 重点写
相关阅读:箭头函数this指向可链接到《JS 中 this 指针问题》
js
|
1 2 3 4 5 6 7 8 9 10 |
const add = (a, b) => a + b; const obj = { name: 'Tom', say() { setTimeout(() => { console.log(this.name); // 箭头函数继承 say 的 this,即 obj }, 0); } }; |
| 对比项 | 普通函数 | 箭头函数 |
|---|---|---|
this | 调用时动态决定 | 继承外层作用域 |
arguments | 有自己的 arguments | 没有,使用外层 |
| 构造函数 | 可以 new | 不能 new |
prototype | 有 | 没有 |
yield | 可用于生成器 | 不能作为生成器 |
super | 视上下文 | 继承外层 |
new.target | 有 | 无 |
普通函数的 this 有四条绑定规则,优先级从高到低:
new 绑定:new Foo() 中 this 指向新对象call / apply / bindobj.fn() 中 this 指向 objwindow,严格模式 undefined箭头函数不适用这四条规则,它的 this 在定义时就确定了。js
|
1 2 3 4 5 6 7 |
const obj = { name: 'Tom', say: () => { console.log(this.name); // 这里的 this 是定义时的外层,通常是 window 或 undefined } }; obj.say(); // undefined |
不能。call/apply/bind 对箭头函数的 this 无效。js
|
1 2 |
const fn = () => console.log(this); fn.call({ a: 1 }); // 仍然输出外层 this |
因为箭头函数没有 [[Construct]] 内部方法,也没有 prototype 属性。js
|
1 2 3 |
const Person = () => {}; console.log(Person.prototype); // undefined new Person(); // TypeError: Person is not a constructor |
thisarguments 的函数prototype 的函数methods(需要 this 指向组件实例)yield 的生成器函数js
|
1 2 3 4 |
function greet(name = 'world') { console.log(`Hello, ${name}`); } greet(); // Hello, world |
默认参数是惰性求值的,每次调用都会重新计算。js
|
1 2 3 4 5 6 7 8 |
function getDefault() { console.log('evaluated'); return 1; } function f(a = getDefault()) {} f(); // evaluated f(); // evaluated |
默认参数会形成自己的作用域,参数之间可以相互引用,但后面的参数可以引用前面的参数,反之不行。js
|
1 2 3 4 |
function f(a = 1, b = a + 1) { return [a, b]; } f(); // [1, 2] |
js
|
1 2 3 4 |
function f(a = b, b = 1) { return [a, b]; } f(); // ReferenceError: Cannot access 'b' before initialization |
js
|
1 2 3 4 |
function sum(...nums) { return nums.reduce((acc, cur) => acc + cur, 0); } sum(1, 2, 3); // 6 |
rest 与 arguments 区别:
| 对比项 | arguments | rest |
|---|---|---|
| 类型 | 类数组 | 真数组 |
| 能否使用数组方法 | 不能直接使用 | 可以 |
| 箭头函数中 | 没有 | 有,取决于外层 |
| 是否包含所有参数 | 包含 | 只包含未命名参数 |
| 是否可被解构 | 不推荐 | 可以 |
不是。它们语法相同(都是 ...),但使用场景相反:
js
|
1 2 3 4 5 |
// rest:收集 function f(...args) {} // 扩展:展开 f(...[1, 2, 3]); |
面试频率:⭐⭐⭐⭐
相关阅读:数组扩展可链接到《数组去重的几种方法》扩展版
js
|
1 2 3 4 5 6 7 8 9 10 11 |
const name = 'Tom'; const age = 18; const user = { name, age, say() { console.log(this.name); }, [`prop_${age}`]: true }; |
js
|
1 2 3 4 5 6 |
const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { a: 3, c: 4 }; Object.assign(target, source1, source2); console.log(target); // { a: 3, b: 2, c: 4 } |
特点:
{...obj} 有什么区别?两者都是浅拷贝,主要区别:
Object.assign 会触发目标对象的 setter,扩展运算符不会Object.assign 可以合并多个源对象,扩展运算符通常用于字面量null/undefined 更安全js
|
1 2 |
Object.assign({}, null); // {} { ...null }; // {} |
js
|
1 2 3 4 5 |
const obj = { a: 1, b: 2 }; Object.keys(obj); // ['a', 'b'] Object.values(obj); // [1, 2] Object.entries(obj); // [['a', 1], ['b', 2]] |
注意:都不包含 Symbol 键,也不包含不可枚举属性。
js
|
1 2 3 4 5 6 7 8 |
const obj = { get name() { return 'Tom'; } }; const desc = Object.getOwnPropertyDescriptors(obj); console.log(desc.name.get); |
常用于配合 Object.defineProperties 实现保留 getter/setter 的浅拷贝:js
|
1 2 3 4 5 6 |
function clone(obj) { return Object.defineProperties( {}, Object.getOwnPropertyDescriptors(obj) ); } |
js
|
1 2 3 4 5 6 |
Array.from('abc'); // ['a', 'b', 'c'] Array.from({ length: 3 }, (_, i) => i); // [0, 1, 2] Array.from(new Set([1, 2, 2])); // [1, 2] Array.of(3); // [3] new Array(3); // [empty × 3] |
[...arr] 有什么区别?Array.from 可以转换类数组对象(有 length 和索引)和可迭代对象js
|
1 2 3 |
const arrayLike = { 0: 'a', 1: 'b', length: 2 }; Array.from(arrayLike); // ['a', 'b'] // [...arrayLike]; // TypeError: arrayLike is not iterable |
js
|
1 2 3 |
const arr = [1, 2, 3, 4]; arr.find(x => x > 2); // 3 arr.findIndex(x => x > 2); // 2 |
与 filter 区别:find 返回第一个匹配元素,filter 返回所有匹配元素组成的数组。
js
|
1 2 |
[NaN].indexOf(NaN); // -1 [NaN].includes(NaN); // true |
includes 使用 SameValueZero 比较,能识别 NaN。indexOf 使用严格相等,不能识别 NaN。
js
|
1 2 3 4 |
[1, [2, [3]]].flat(); // [1, 2, [3]] [1, [2, [3]]].flat(Infinity); // [1, 2, 3] [1, 2].flatMap(x => [x, x * 2]); // [1, 2, 2, 4] |
flatMap 等价于先 map 再 flat(1)。
js
|
1 2 3 4 5 6 7 8 9 10 |
function flat(arr, depth = 1) { return arr.reduce((acc, cur) => { if (Array.isArray(cur) && depth > 0) { acc.push(...flat(cur, depth - 1)); } else { acc.push(cur); } return acc; }, []); } |
面试频率:⭐⭐⭐
js
|
1 2 3 4 5 |
const name = 'Tom'; const age = 18; const str = `My name is ${name}, I am ${age} years old.`; |
支持多行、变量嵌入、表达式嵌入。
js
|
1 2 3 4 5 6 7 |
function tag(strings, ...values) { console.log(strings); // ['Hello ', '!'] console.log(values); // ['Tom'] return strings[0] + values[0] + strings[1]; } tag`Hello ${'Tom'}!`; |
标签模板的典型应用:防止 XSS 攻击、国际化。js
|
1 2 3 4 5 6 7 8 9 10 11 |
function safeHtml(strings, ...values) { const escaped = values.map(v => String(v).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])) ); return strings.reduce((acc, s, i) => acc + s + (escaped[i] || ''), '' ); } |
js
|
1 2 3 4 5 6 |
'abc'.includes('b'); // true 'abc'.startsWith('a'); // true 'abc'.endsWith('c'); // true 'ha'.repeat(3); // 'hahaha' '1'.padStart(3, '0'); // '001' '1'.padEnd(3, '0'); // '100' |
js
|
1 |
String.raw`C:\Users\name`; // 'C:\\Users\\name' |
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
0b1010; // 10 0o12; // 10 Number.isFinite(1); // true Number.isFinite('1'); // false isFinite('1'); // true,会先转换 Number.isNaN(NaN); // true Number.isNaN('NaN'); // false isNaN('NaN'); // true,会先转换 Number.parseInt('12px'); // 12 Number.parseFloat('3.14abc'); // 3.14 Number.isInteger(3); // true Number.isInteger(3.0); // true Number.EPSILON; // 2.220446049250313e-16 Number.MAX_SAFE_INTEGER; // 9007199254740991 Number.MIN_SAFE_INTEGER; // -9007199254740991 2 ** 3; // 8 2 ** 3 ** 2; // 512,右结合 |
浮点数在 IEEE 754 标准下无法精确表示 0.1 和 0.2,相加后产生误差。js
|
1 2 |
0.1 + 0.2 === 0.3; // false Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true |
面试频率:⭐⭐⭐⭐⭐ 重点写
Symbol 是原始数据类型,主要用途是解决属性名冲突。js
|
1 2 3 |
const s1 = Symbol('id'); const s2 = Symbol('id'); console.log(s1 === s2); // false |
即使描述相同,每个 Symbol 也是唯一的。
js
|
1 2 3 |
const obj = { [Symbol('name')]: 'Tom' }; |
Symbol 属性不可枚举:js
|
1 2 3 4 5 6 7 |
const sym = Symbol('key'); const obj = { [sym]: 1, a: 2 }; Object.keys(obj); // ['a'] for (const k in obj) {} // 只遍历 'a' JSON.stringify(obj); // '{"a":2}' Object.getOwnPropertySymbols(obj); // [sym] |
因为 Symbol 的设计目标就是作为“隐藏属性”或“元属性”,不希望被普通遍历逻辑意外访问,避免和业务属性冲突。
js
|
1 2 3 4 |
const a = Symbol.for('app'); const b = Symbol.for('app'); console.log(a === b); // true Symbol.keyFor(a); // 'app' |
Symbol.for 会在全局注册表中查找,没有就创建,有就返回同一个。
| Symbol | 用途 |
|---|---|
Symbol.iterator | 定义默认迭代器 |
Symbol.asyncIterator | 定义异步迭代器 |
Symbol.toPrimitive | 定义对象转原始值行为 |
Symbol.toStringTag | 定义 Object.prototype.toString 返回值 |
Symbol.hasInstance | 定义 instanceof 行为 |
js
|
1 2 3 4 5 6 7 8 9 10 |
const obj = { [Symbol.toPrimitive](hint) { if (hint === 'number') return 42; if (hint === 'string') return 'hello'; return true; } }; console.log(+obj); // 42 console.log(`${obj}`); // 'hello' |
不能。Symbol 适合做“元属性”和“避免冲突的私有标记”,但序列化(JSON.stringify)、Object.keys、for...in 都不包含 Symbol,不适合做业务数据的普通键。
js
|
1 2 3 4 5 6 7 8 9 10 |
const set = new Set([1, 2, 2, 3]); console.log(set); // Set(3) {1, 2, 3} set.add(4); set.has(2); // true set.delete(1); console.log(set.size); // 3 set.forEach(v => console.log(v)); for (const v of set) {} |
Set 内部使用 SameValueZero 算法比较值:
NaN 等于 NaN+0 等于 -0js
|
1 2 |
new Set([NaN, NaN]); // Set(1) {NaN} new Set([{}, {}]); // Set(2) {{}, {}} |
js
|
1 |
const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3] |
js
|
1 2 |
const arr = [{ id: 1 }, { id: 1 }, { id: 2 }]; const unique = [...new Map(arr.map(item => [item.id, item])).values()]; |
sizejs
|
1 2 3 4 |
const ws = new WeakSet(); const obj = {}; ws.add(obj); ws.has(obj); // true |
典型应用:标记对象是否被处理过,而不阻止其被回收。
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const map = new Map(); const key = { id: 1 }; map.set(key, 'value'); map.get(key); // 'value' map.has(key); // true map.delete(key); map.set('name', 'Tom'); map.size; // 1 for (const [k, v] of map) {} map.forEach((v, k) => {}); |
| 对比项 | Object | Map |
|---|---|---|
| 键类型 | 字符串 / Symbol | 任意类型 |
| 顺序 | 不完全可靠 | 插入顺序 |
| size | 手动计算 | map.size |
| 频繁增删 | 一般 | 更优 |
| 默认原型 | 有原型链 | 干净 |
| 序列化 | 直接支持 JSON | 需要手动转换 |
Map 内部使用类似哈希表的结构,键的比较使用 SameValueZero,对象键通过引用比较。普通对象的键会被强制转成字符串,对象会变成 '[object Object]'。js
|
1 2 3 4 |
const obj = {}; obj[{ a: 1 }] = 'x'; obj[{ b: 2 }] = 'y'; console.log(obj); // { '[object Object]': 'y' } |
sizejs
|
1 2 3 |
const wm = new WeakMap(); const dom = document.querySelector('#app'); wm.set(dom, { data: 1 }); |
js
|
1 2 3 4 5 6 7 8 9 10 |
const privateData = new WeakMap(); class Person { constructor(name) { privateData.set(this, { name }); } getName() { return privateData.get(this).name; } } |
| 数据结构 | 是否可遍历 | 键类型限制 | 引用强度 | 使用场景 |
|---|---|---|---|---|
| Set | 是 | 任意值 | 强引用 | 去重、集合运算 |
| WeakSet | 否 | 对象 | 弱引用 | 临时标记对象 |
| Map | 是 | 任意值 | 强引用 | 复杂键值对 |
| WeakMap | 否 | 对象 | 弱引用 | 私有数据、缓存 |
因为它是弱引用,键可能随时被垃圾回收,遍历结果不确定。如果允许遍历,可能导致不可预期的行为。
面试频率:⭐⭐⭐⭐
可迭代协议:对象必须实现 Symbol.iterator 方法,返回一个迭代器。js
|
1 2 3 4 5 6 7 |
const arr = [1, 2, 3]; const iterator = arr[Symbol.iterator](); iterator.next(); // { value: 1, done: false } iterator.next(); // { value: 2, done: false } iterator.next(); // { value: 3, done: false } iterator.next(); // { value: undefined, done: true } |
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const myIterable = { data: [1, 2, 3], [Symbol.iterator]() { let index = 0; const data = this.data; return { next() { if (index < data.length) { return { value: data[index++], done: false }; } return { value: undefined, done: true }; } }; } }; for (const v of myIterable) { console.log(v); // 1 2 3 } |
| 对比项 | for...of | for...in |
|---|---|---|
| 遍历内容 | 可迭代对象的值 | 可枚举属性名 |
| 是否包含原型链 | 否 | 是 |
| 适合数组 | 是 | 否 |
| 适合对象 | 默认不可迭代 | 是 |
| 能否中断 | 支持 break/continue | 支持 break/continue |
js
|
1 2 3 4 5 |
Array.prototype.extra = 'x'; const arr = [1, 2, 3]; for (const k in arr) { console.log(k); // '0' '1' '2' 'extra' } |
js
|
1 2 3 4 5 6 7 8 9 10 |
function* gen() { yield 1; yield 2; return 3; } const it = gen(); it.next(); // { value: 1, done: false } it.next(); // { value: 2, done: false } it.next(); // { value: 3, done: true } |
Generator 函数执行到 yield 会暂停,调用 next() 恢复。js
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function* gen() { console.log('start'); const x = yield 1; console.log('x:', x); const y = yield 2; console.log('y:', y); } const it = gen(); it.next(); // start, { value: 1, done: false } it.next(10); // x: 10, { value: 2, done: false } it.next(20); // y: 20, { value: undefined, done: true } |
因为第一次 next() 是启动生成器,执行到第一个 yield 暂停。此时还没有 yield 表达式在等待接收值,所以传参被忽略。
js
|
1 2 3 4 5 6 7 8 9 10 11 |
function* a() { yield 1; yield 2; } function* b() { yield* a(); yield 3; } console.log([...b()]); // [1, 2, 3] |
Generator + Promise + 执行器是 async/await 的前身。js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function* fetchData() { const res = yield fetch('/api'); const data = yield res.json(); return data; } // 手动执行器 function run(gen) { const it = gen(); function next(value) { const { value: v, done } = it.next(value); if (done) return Promise.resolve(v); return Promise.resolve(v).then(next); } return next(); } run(fetchData).then(data => console.log(data)); |
js
|
1 2 3 4 5 6 |
function* idGenerator() { let id = 0; while (true) { yield id++; } } |
面试频率:⭐⭐⭐⭐
相关阅读:ES5 继承对比链接到《JS 面向对象》
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Person { constructor(name) { this.name = name; } say() { console.log(this.name); } static create(name) { return new Person(name); } get upperName() { return this.name.toUpperCase(); } set upperName(value) { this.name = value.toLowerCase(); } } |
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Counter { #count = 0; increment() { this.#count++; } get value() { return this.#count; } } const c = new Counter(); c.increment(); console.log(c.value); // 1 // console.log(c.#count); // SyntaxError |
| 对比项 | ES5 构造函数 | Class |
|---|---|---|
| 调用方式 | 可以不用 new | 必须用 new |
| 严格模式 | 非严格 | 内部默认严格模式 |
| 方法可枚举 | 是 | 否 |
| 变量提升 | 函数声明提升 | 不提升到可调用 |
| 继承 | 手动设置原型链 | extends / super |
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a sound`); } static type() { return 'Animal'; } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed = breed; } speak() { super.speak(); console.log('Wang!'); } } |
因为子类的 this 是由父类构造函数创建的。在 super() 之前,子类没有自己的 this,访问 this 会报错。js
|
1 2 3 4 5 6 |
class Dog extends Animal { constructor(name) { console.log(this); // ReferenceError super(name); } } |
super 在构造函数中指向父类构造函数([[Prototype]] 的 constructor),在方法中指向父类的原型对象。js
|
1 2 3 4 5 6 |
class Dog extends Animal { speak() { // super.speak() 等价于 Animal.prototype.speak.call(this); } } |
js
|
1 2 |
class Dog extends Animal {} Dog.type(); // 'Animal' |
因为 Dog.__proto__ === Animal,所以静态方法也被继承。
仍然是原型链。extends 做了两件事:
Dog.prototype.__proto__ = Animal.prototype(实例方法继承)Dog.__proto__ = Animal(静态方法继承)面试频率:⭐⭐⭐⭐
相关阅读:可与《跨域的几种方法》交叉链接;若内容过多可独立成篇
js
|
1 2 3 4 5 6 7 8 |
// math.js export const add = (a, b) => a + b; export default function multiply(a, b) { return a * b; } // main.js import multiply, { add } from './math.js'; |
重命名:js
|
1 2 |
import { add as sum } from './math.js'; export { add as sum } from './math.js'; |
聚合导出:js
|
1 2 |
export * from './math.js'; export { add } from './math.js'; |
js
|
1 2 3 |
import('./math.js').then(module => { console.log(module.add(1, 2)); }); |
条件加载、懒加载:js
|
1 2 3 |
if (needMath) { const { add } = await import('./math.js'); } |
并行加载:js
|
1 2 3 4 |
const [a, b] = await Promise.all([ import('./a.js'), import('./b.js') ]); |
| 对比项 | ES Module | CommonJS |
|---|---|---|
| 加载时机 | 静态编译时 | 运行时 |
| 导出值 | 值引用 / 活绑定 | 值拷贝 |
this | 顶层 undefined | module.exports |
| 循环依赖 | 活绑定,处理更好 | 可能拿到不完整导出 |
| 使用场景 | 浏览器 / 现代构建 | Node 传统模块 |
js
|
1 2 3 4 5 6 7 8 9 10 11 |
// counter.js export let count = 0; export function increment() { count++; } // main.js import { count, increment } from './counter.js'; console.log(count); // 0 increment(); console.log(count); // 1,值同步更新 |
CommonJS 中:js
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// counter.js let count = 0; module.exports = { count, increment() { count++; } }; // main.js const { count, increment } = require('./counter.js'); console.log(count); // 0 increment(); console.log(count); // 0,值不会同步 |
不能。导入绑定是只读的,但如果是对象,可以修改对象内部属性。js
|
1 2 3 |
import { obj } from './module.js'; obj.a = 1; // 可以 // obj = {}; // TypeError |
面试频率:⭐⭐⭐⭐⭐ 重点写
Promise 三种状态:
pendingfulfilledrejected状态一旦改变不可逆。js
|
1 2 3 4 5 6 7 |
const p = new Promise((resolve, reject) => { resolve(1); }); p.then(value => console.log(value)) .catch(err => console.error(err)) .finally(() => console.log('done')); |
链式调用本质:每次 then 返回一个新的 Promise。
js
|
1 2 3 4 5 6 7 8 9 10 11 |
Promise.resolve(1) .then(v => { console.log(v); // 1 return v + 1; }) .then(v => { console.log(v); // 2 throw new Error('err'); }) .catch(e => console.log(e.message)) // err .then(() => console.log('after catch')); // 会执行 |
then 中返回普通值,会被包装成 resolved Promisethen 中抛出错误,会进入下一个 catchcatch 后如果没有再抛错,后续 then 会继续执行相关阅读:详细实现见《手写 Promise A+ 规范的 Promise 代码》,此处只做使用层面快速回顾并链接。
| 方法 | 行为 | 成功条件 | 失败条件 |
|---|---|---|---|
Promise.all | 全部成功才成功 | 所有 fulfilled | 任意一个 rejected |
Promise.race | 第一个 settled | 第一个 fulfilled | 第一个 rejected |
Promise.allSettled | 等待所有完成 | 永远 fulfilled | 不会 reject |
Promise.any | 第一个成功 | 任意一个 fulfilled | 全部 rejected,抛 AggregateError |
js
|
1 2 3 4 |
Promise.all([p1, p2]); Promise.race([p1, p2]); Promise.allSettled([p1, p2]); Promise.any([p1, p2]); |
会。Promise.all 只是控制返回的 Promise 状态,不会取消其他 Promise。其他 Promise 仍然会继续执行,只是结果被忽略。
js
|
1 2 3 4 5 6 7 8 9 |
const results = await Promise.allSettled([ Promise.resolve(1), Promise.reject('err') ]); // [ // { status: 'fulfilled', value: 1 }, // { status: 'rejected', reason: 'err' } // ] |
手写实现放《手写题合集》。
js
|
1 2 3 4 5 6 7 8 |
async function getData() { try { const res = await fetch('/api'); return await res.json(); } catch (err) { console.error(err); } } |
要点:
async 函数返回值始终是 Promiseawait 后面跟非 Promise 值,会被包装成 Promisetry/catch 或 .catch()Promise.all + await 替代串行 awaitjs
|
1 2 3 4 5 |
async function f() { return 1; } f().then(v => console.log(v)); // 1 |
等价于:js
|
1 2 3 |
function f() { return Promise.resolve(1); } |
如果 async 函数中抛出错误,返回的 Promise 会变成 rejected:js
|
1 2 3 4 5 |
async function f() { throw new Error('err'); } f().catch(e => console.log(e.message)); // err |
会被 Promise.resolve() 包装,但仍然会进入微任务队列。js
|
1 2 3 4 5 6 7 8 9 10 11 12 |
async function f() { console.log(1); await 123; console.log(2); } f(); console.log(3); // 1 // 3 // 2 |
串行:js
|
1 2 |
const a = await getA(); // 等待 1s const b = await getB(); // 再等待 1s,总共 2s |
并发:js
|
1 |
const [a, b] = await Promise.all([getA(), getB()]); // 总共 1s |
async/await底层是 Promise 的语法糖,执行顺序详见《JS 事件循环机制》的扩展章节。
示例:js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); async1(); console.log('script end'); // script start // async1 start // async2 // script end // async1 end |
执行过程:
console.log('script start')async1(),同步执行到 await async2()async2(),输出 async2,返回 resolved Promiseawait 后面的代码 console.log('async1 end') 进入微任务队列console.log('script end')async1 end进入微任务队列,在当前同步代码执行完后、下一个宏任务之前执行。
面试频率:⭐⭐⭐
js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const target = { name: 'Tom' }; const proxy = new Proxy(target, { get(target, prop) { console.log('get', prop); return target[prop]; }, set(target, prop, value) { console.log('set', prop, value); target[prop] = value; return true; } }); proxy.name; // get name proxy.age = 18; // set age 18 |
常用陷阱:
getsethasdeletePropertyapplyconstructownKeysjs
|
1 2 3 4 5 6 7 8 9 |
const validator = new Proxy({}, { set(target, prop, value) { if (prop === 'age' && typeof value !== 'number') { throw new TypeError('age must be a number'); } target[prop] = value; return true; } }); |
Reflect 让 Object 操作变成函数行为。js
|
1 2 3 4 5 6 |
const obj = { a: 1 }; Reflect.get(obj, 'a'); // 1 Reflect.set(obj, 'b', 2); Reflect.has(obj, 'a'); // true Reflect.deleteProperty(obj, 'b'); |
js
|
1 2 3 4 5 6 7 8 |
const proxy = new Proxy(target, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); }, set(target, prop, value, receiver) { return Reflect.set(target, prop, value, receiver); } }); |
| 对比项 | Proxy | Object.defineProperty |
|---|---|---|
| 代理范围 | 整个对象 | 单个属性 |
| 新增属性 | 可拦截 | 需要手动处理 |
| 数组 | 可拦截 | 需要重写方法 |
| 拦截操作 | 13 种 | 有限 |
| 兼容性 | ES6+ | ES5 |
Vue 2 用 defineProperty,Vue 3 用 Proxy。
receiver 是代理对象或继承代理对象的对象。在 Reflect.get 中传入 receiver 可以保证 getter 中的 this 指向正确。js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const target = { get name() { return this._name; }, _name: 'Tom' }; const proxy = new Proxy(target, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } }); console.log(proxy.name); // 'Tom' |
面试频率:⭐⭐⭐⭐
可选链?.和空值合并??建议单独展开讲与||的区别。
| 特性 | 版本 | 简要说明 |
|---|---|---|
** 指数运算符 | ES2016 | 2 ** 3 = 8 |
Array.prototype.includes | ES2016 | 替代 indexOf 判断包含 |
Object.values() / entries() | ES2017 | 对象遍历新方法 |
padStart() / padEnd() | ES2017 | 字符串填充 |
Object.getOwnPropertyDescriptors() | ES2017 | 获取完整属性描述符 |
async/await | ES2017 | 异步编程终极方案 |
Promise.prototype.finally | ES2018 | 无论成功失败都执行 |
对象扩展运算符 { ...obj } | ES2018 | 对象浅拷贝 / 合并 |
flat() / flatMap() | ES2019 | 数组扁平化 |
Object.fromEntries() | ES2019 | 键值对数组转对象 |
可选链 ?. | ES2020 | 安全访问嵌套属性 |
空值合并 ?? | ES2020 | 仅对 null/undefined 取默认值 |
BigInt | ES2020 | 大整数类型 |
Promise.allSettled() | ES2020 | 等待所有 Promise 完成 |
globalThis | ES2020 | 统一全局对象引用 |
String.prototype.matchAll | ES2020 | 全局匹配所有结果 |
逻辑赋值运算符 ??= / &&= / ||= | ES2021 | 逻辑运算与赋值结合 |
数字分隔符 1_000_000 | ES2021 | 提高数值可读性 |
Array.prototype.at() | ES2022 | 支持负索引 |
类私有字段 #field | ES2022 | 真正的私有属性 |
顶层 await | ES2022 | 模块顶层直接使用 await |
?.js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const user = { address: { city: 'Beijing' } }; user.address?.city; // 'Beijing' user.contact?.phone; // undefined // 方法调用 user.say?.(); // 如果 say 存在才调用 // 动态属性 user.address?.['city']; // 'Beijing' |
??js
|
1 2 3 4 5 6 |
const a = 0; const b = null; a || 10; // 10,因为 0 是假值 a ?? 10; // 0,只有 null/undefined 才取默认值 b ?? 10; // 10 |
区别:
|| 对 0、''、false、NaN 也会取默认值?? 只对 null 和 undefined 取默认值?. 和 && 的区别?js
|
1 2 |
obj?.a; // obj 为 null/undefined 时返回 undefined obj && obj.a; // 也能短路,但 obj 为 0、'' 时也会短路 |
?. 更精确,不会因为假值误判。
用于表示超过 Number.MAX_SAFE_INTEGER 的整数。js
|
1 2 3 4 5 |
const big = 9007199254740993n; console.log(big + 1n); // 9007199254740994n // 不能与 Number 混合运算 // big + 1; // TypeError |
不同环境全局对象不同:
windowglobalselfglobalThis 统一了引用方式。js
|
1 |
globalThis.setTimeout(() => {}, 0); |
第二、四、七、十一部分:
let/const + TDZ第三、五、八、九部分:
第六、十、十二、十三部分:
ES6+ 不是零散语法点的堆砌,而是一套现代 JavaScript 工程体系。面试复习时建议按“变量 → 函数 → 数据结构 → 异步 → 元编程 → 模块化”的主线串联,再用后续版本特性补充细节。本文可作为索引,遇到具体实现时再跳转到对应旧文深入。