ES6+ 核心特性深度解析
解构赋值
解构赋值是 ES6 引入的一种从数组或对象中提取值、对变量进行赋值的语法糖。其本质是模式匹配——只要等号两边的模式相同,左边的变量就会被赋予右边对应的值。引擎在编译阶段会将解构语法转换为一系列赋值语句,因此它是零运行时开销的语法转换。
对象解构
对象解构基于属性名进行匹配,与顺序无关。
const person = { name: 'Alice', age: 30, job: 'Engineer' }
const { name, age } = person
const { name: userName, age: userAge } = person
const { name, ...rest } = person深层原理:对象解构在规范中通过 RequireObjectCoercible 确保右侧值不是 null 或 undefined,然后对每个解构属性调用 GetV 获取值。这意味着原始类型也可以被解构,因为它们会被临时装箱为包装对象:
const { length } = 'hello'
const { toFixed } = 42数组解构
数组解构基于迭代器协议进行匹配——它会对右侧值调用 Symbol.iterator 方法,然后按顺序取值。
const [a, b, c] = [1, 2, 3]
const [first, , third] = [1, 2, 3]
const [head, ...tail] = [1, 2, 3, 4]由于数组解构依赖迭代器协议,任何实现了 Symbol.iterator 的对象都可以被数组解构:
const [a, b, c] = 'abc'
const [x, y] = new Set([10, 20])
function* gen() {
yield 1
yield 2
yield 3
}
const [p, q, r] = gen()嵌套解构
解构可以嵌套到任意深度,对象与数组可自由组合。
const data = {
users: [
{ id: 1, profile: { name: 'Alice', scores: [90, 85, 92] } },
{ id: 2, profile: { name: 'Bob', scores: [78, 88, 95] } }
]
}
const {
users: [
{
profile: {
name: firstName,
scores: [bestScore]
}
}
]
} = data默认值
当解构的值严格等于 undefined 时,默认值生效。这里的关键是严格等于 undefined——null 不会触发默认值。
const { a = 1, b = 2 } = { a: 10, b: undefined }
const { x = 1 } = { x: null }
const [m = 'default', n = 'default'] = [undefined, null]默认值可以是表达式,且是惰性求值的——只有在需要时才会执行:
function expensive() {
console.log('computed')
return 42
}
const { val = expensive() } = { val: 100 }
const { val2 = expensive() } = {}函数参数解构
解构在函数参数中极为常见,特别适合处理配置对象:
function createUser({ name, age = 18, role = 'user' } = {}) {
return { name, age, role }
}
createUser({ name: 'Alice' })
createUser({ name: 'Bob', age: 25, role: 'admin' })
createUser()展开运算符
展开运算符 ... 在 ES6 中以两种形态出现:展开(Spread) 和 剩余(Rest)。两者语法相同,但语义相反——Spread 是将可迭代对象"展开",Rest 是将多个元素"收集"为一个。
数组展开
数组展开基于迭代器协议,将可迭代对象的每个元素展开到新数组中。
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const merged = [...arr1, ...arr2]
const copy = [...arr1]展开操作创建的是浅拷贝——嵌套的引用类型仍然共享同一引用:
const original = [{ a: 1 }, { b: 2 }]
const cloned = [...original]
cloned[0].a = 999对象展开
对象展开(ES2018)将源对象的自有可枚举属性逐一复制到目标对象中。
const defaults = { theme: 'light', lang: 'en', debug: false }
const userConfig = { theme: 'dark', lang: 'zh' }
const config = { ...defaults, ...userConfig }
const obj = { a: 1, b: 2, c: 3 }
const { a, ...remaining } = obj对象展开的属性覆盖遵循后者胜出原则,这一机制在源码层面等价于 Object.assign({}, ...sources),但有一个关键区别:展开运算符不会触发目标对象上的 setter。
const base = { x: 1 }
const extended = { ...base, x: 2, y: 3 }函数参数中的展开与剩余
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0)
}
sum(1, 2, 3, 4)
const args = [1, 2, 3]
Math.max(...args)Rest 参数在规范中取代了 arguments 对象。与 arguments 的本质区别在于:Rest 参数是真正的 Array 实例,拥有所有数组方法;而 arguments 是一个类数组对象(Array-like),并且在非严格模式下与命名参数存在"别名绑定"的怪异行为。
Promise 与 async/await
此处仅作概要梳理,详细内容请参阅 Promise 专题。
Promise 核心概念
Promise 是对异步操作最终完成(或失败)的表示。它有三种状态:pending、fulfilled、rejected,状态一旦变更不可逆转(settled)。
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000)
})
p.then(val => console.log(val))
.catch(err => console.error(err))
.finally(() => console.log('cleanup'))Promise 的微任务调度机制是其核心——.then 回调被推入微任务队列(microtask queue),在当前宏任务结束后、下一个宏任务开始前执行。
async/await
async/await 是 Generator + Promise 的语法糖,让异步代码以同步形式书写。async 函数始终返回 Promise,await 会暂停函数执行直到 Promise settled。
async function fetchData(url) {
try {
const response = await fetch(url)
const data = await response.json()
return data
} catch (error) {
throw new Error(`Fetch failed: ${error.message}`)
}
}并发控制
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
])
const result = await Promise.allSettled([
fetchCritical(),
fetchOptional()
])
const fastest = await Promise.race([
fetchFromCDN1(),
fetchFromCDN2()
])
const firstSuccess = await Promise.any([
fetchFromPrimary(),
fetchFromFallback()
])Proxy 与 Reflect
Proxy 是 ES6 引入的元编程(Meta-programming) 能力,允许你定义对象基本操作的自定义行为。在 ECMAScript 规范中,对象的基本操作被定义为一组"内部方法"(Internal Methods),Proxy 正是对这些内部方法的拦截层。
基本用法
Proxy 接受两个参数:target(目标对象)和 handler(拦截器对象),handler 中的方法被称为 trap。
const target = { name: 'Alice', age: 30 }
const handler = {
get(target, property, receiver) {
console.log(`Reading ${property}`)
return Reflect.get(target, property, receiver)
},
set(target, property, value, receiver) {
console.log(`Writing ${property} = ${value}`)
return Reflect.set(target, property, value, receiver)
}
}
const proxy = new Proxy(target, handler)
proxy.name
proxy.age = 31可拦截操作详解
Proxy 支持 13 种 trap,覆盖了对象的所有内部方法:
| Trap | 触发时机 | 对应内部方法 |
|---|---|---|
get | 读取属性 | [[Get]] |
set | 设置属性 | [[Set]] |
has | in 操作符 | [[HasProperty]] |
deleteProperty | delete 操作符 | [[Delete]] |
ownKeys | Object.keys() / for...in | [[OwnPropertyKeys]] |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | [[GetOwnProperty]] |
defineProperty | Object.defineProperty() | [[DefineOwnProperty]] |
getPrototypeOf | Object.getPrototypeOf() | [[GetPrototypeOf]] |
setPrototypeOf | Object.setPrototypeOf() | [[SetPrototypeOf]] |
isExtensible | Object.isExtensible() | [[IsExtensible]] |
preventExtensions | Object.preventExtensions() | [[PreventExtensions]] |
apply | 函数调用 | [[Call]] |
construct | new 操作符 | [[Construct]] |
const handler = {
has(target, key) {
if (key.startsWith('_')) {
return false
}
return Reflect.has(target, key)
},
deleteProperty(target, key) {
if (key.startsWith('_')) {
throw new Error(`Cannot delete private property "${key}"`)
}
return Reflect.deleteProperty(target, key)
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => !String(key).startsWith('_'))
}
}
const obj = new Proxy({ _secret: 42, name: 'public' }, handler)
'_secret' in obj
Object.keys(obj)Reflect 的设计意义
Reflect 并非简单的工具函数集合,它的设计目标有三个层面:
- 提供与 Proxy trap 一一对应的默认行为,让你在 trap 中方便地调用"原始操作"。
- 将命令式操作函数化:
delete obj.key→Reflect.deleteProperty(obj, key),key in obj→Reflect.has(obj, key)。 - 返回布尔值代替抛异常:
Object.defineProperty失败时抛异常,Reflect.defineProperty返回false。
Reflect.has({ a: 1 }, 'a')
Reflect.ownKeys({ a: 1, [Symbol('b')]: 2 })
const success = Reflect.defineProperty({}, 'x', { value: 1 })实际用例:响应式系统
Vue 3 的响应式系统正是基于 Proxy 构建的。以下是核心思路的简化实现:
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
const result = Reflect.get(target, key, receiver)
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
const state = reactive({ count: 0, nested: { value: 1 } })
effect(() => {
console.log(`count is: ${state.count}`)
})
state.count++这里使用 Reflect.get 而非直接 target[key] 的关键原因在于 receiver 参数的传递。当目标对象有 getter 且通过原型链继承时,receiver 确保 this 指向正确的代理对象,从而让依赖追踪不会遗漏。
可撤销代理
const { proxy, revoke } = Proxy.revocable({ data: 'sensitive' }, {
get(target, key) {
return Reflect.get(target, key)
}
})
proxy.data
revoke()Symbol
Symbol 是 ES6 引入的第七种原始类型,每个 Symbol 值都是唯一且不可变的。它的核心价值在于提供"绝对不会冲突的属性键",以及作为语言级别的"协议标识符"。
唯一性
const s1 = Symbol('description')
const s2 = Symbol('description')
s1 === s2
typeof s1
const key = Symbol('myKey')
const obj = { [key]: 'value' }
obj[key]Symbol 属性不会出现在 for...in、Object.keys()、JSON.stringify() 中,只能通过 Object.getOwnPropertySymbols() 或 Reflect.ownKeys() 获取。这一特性使其成为实现"半私有"属性的利器。
Symbol.for 与全局注册表
Symbol.for(key) 通过全局 Symbol 注册表实现跨域、跨 iframe 的 Symbol 共享:
const s1 = Symbol.for('app.id')
const s2 = Symbol.for('app.id')
s1 === s2
Symbol.keyFor(s1)
Symbol.keyFor(Symbol('local'))Well-known Symbols
ECMAScript 规范定义了一组 Well-known Symbols,用于定制语言内置行为。它们是 JavaScript 的"内部协议":
class Money {
constructor(amount, currency) {
this.amount = amount
this.currency = currency
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount
if (hint === 'string') return `${this.amount} ${this.currency}`
return this.amount
}
}
const price = new Money(100, 'USD')
+price
`${price}`
price + 50class TypedArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance) && instance.every(item => typeof item === 'number')
}
}
[1, 2, 3] instanceof TypedArray
[1, '2', 3] instanceof TypedArrayclass SafeArray extends Array {
static get [Symbol.species]() {
return Array
}
}
const safe = new SafeArray(1, 2, 3)
const mapped = safe.map(x => x * 2)
mapped instanceof SafeArray
mapped instanceof ArraySymbol.iterator
Symbol.iterator 是可迭代协议的核心,它将在下一节详细展开。
Iterator 与 for...of
可迭代协议
JavaScript 定义了两个协议来实现迭代:
- 可迭代协议(Iterable Protocol):对象实现
Symbol.iterator方法,该方法返回一个迭代器。 - 迭代器协议(Iterator Protocol):对象实现
next()方法,返回{ value, done }形状的结果。
for...of 循环、展开运算符、数组解构、Promise.all 等语法特性都依赖可迭代协议。
const arr = [10, 20, 30]
const iterator = arr[Symbol.iterator]()
iterator.next()
iterator.next()
iterator.next()
iterator.next()自定义迭代器
class Range {
constructor(start, end, step = 1) {
this.start = start
this.end = end
this.step = step
}
[Symbol.iterator]() {
let current = this.start
const { end, step } = this
return {
next() {
if (current <= end) {
const value = current
current += step
return { value, done: false }
}
return { value: undefined, done: true }
},
[Symbol.iterator]() {
return this
}
}
}
}
const range = new Range(1, 5)
for (const n of range) {
console.log(n)
}
[...new Range(0, 10, 2)]
const [a, b, c] = new Range(100, 200, 50)无限迭代器
迭代器可以是无限的——只需永远不返回 done: true。这时必须由消费方(如 for...of 中的 break)来终止迭代:
function naturals(start = 0) {
return {
[Symbol.iterator]() {
let n = start
return {
next() {
return { value: n++, done: false }
}
}
}
}
}
for (const n of naturals()) {
if (n > 5) break
console.log(n)
}for...of 与 for...in 的本质区别
for...in 遍历对象的可枚举字符串属性键(包含原型链),for...of 消费迭代器产出的值。二者在语义层面完全不同:
const arr = [10, 20, 30]
for (const key in arr) {
console.log(key, typeof key)
}
for (const val of arr) {
console.log(val, typeof val)
}Generator
Generator 函数(function*)是 ES6 引入的一种特殊函数,它能够暂停和恢复执行。调用 Generator 函数不会立即执行函数体,而是返回一个 Generator 对象,该对象同时实现了迭代器协议和可迭代协议。
基本用法与 yield
function* countdown(n) {
while (n > 0) {
yield n
n--
}
}
const gen = countdown(3)
gen.next()
gen.next()
gen.next()
gen.next()Generator 函数执行到 yield 时暂停,下次调用 next() 时从暂停处恢复执行。内部通过保存执行上下文(包括局部变量、PC 指针等)实现挂起/恢复,本质上是协程(Coroutine) 的一种实现。
yield* 委托
yield* 将迭代委托给另一个可迭代对象:
function* concat(...iterables) {
for (const iterable of iterables) {
yield* iterable
}
}
[...concat([1, 2], [3, 4], 'ab')]yield* 的返回值是被委托 Generator 的 return 值:
function* inner() {
yield 'a'
yield 'b'
return 'inner done'
}
function* outer() {
const result = yield* inner()
yield result
}
[...outer()]双向通信
Generator 最强大的特性之一是双向数据通道——next(value) 传入的参数会成为上一个 yield 表达式的返回值:
function* conversation() {
const name = yield 'What is your name?'
const age = yield `Hello ${name}! How old are you?`
return `${name} is ${age} years old.`
}
const talk = conversation()
talk.next()
talk.next('Alice')
talk.next(30)这种双向通信能力使 Generator 可以实现复杂的控制流。throw() 和 return() 方法补充了错误注入和提前终止的能力:
function* resilient() {
try {
const a = yield 1
const b = yield 2
return a + b
} catch (e) {
yield `Error caught: ${e.message}`
} finally {
console.log('cleanup')
}
}
const gen2 = resilient()
gen2.next()
gen2.throw(new Error('oops'))
gen2.next()异步 Generator
ES2018 引入了异步 Generator(async function*),融合了异步迭代与 Generator 的暂停能力:
async function* fetchPages(baseUrl, totalPages) {
for (let page = 1; page <= totalPages; page++) {
const response = await fetch(`${baseUrl}?page=${page}`)
const data = await response.json()
yield data
}
}
async function processAllPages() {
for await (const pageData of fetchPages('/api/items', 5)) {
console.log(`Got ${pageData.items.length} items`)
}
}异步 Generator 返回的对象实现了异步可迭代协议——Symbol.asyncIterator 方法返回异步迭代器,其 next() 返回 Promise<{ value, done }>。
class EventStream {
constructor(element, eventName) {
this.element = element
this.eventName = eventName
}
[Symbol.asyncIterator]() {
const queue = []
let resolve = null
this.element.addEventListener(this.eventName, (event) => {
if (resolve) {
resolve({ value: event, done: false })
resolve = null
} else {
queue.push(event)
}
})
return {
next() {
if (queue.length > 0) {
return Promise.resolve({ value: queue.shift(), done: false })
}
return new Promise(r => { resolve = r })
}
}
}
}Map / Set / WeakMap / WeakSet
ES6 引入的四种集合类型弥补了 JavaScript 长期以来只能用普通对象模拟映射和集合的缺陷。
Map
Map 是真正的键值对集合,与对象的本质区别在于:键可以是任意类型(包括对象、函数、NaN),且保持插入顺序。
const map = new Map()
const objKey = { id: 1 }
const fnKey = () => {}
map.set(objKey, 'object value')
map.set(fnKey, 'function value')
map.set(NaN, 'NaN value')
map.get(objKey)
map.get(NaN)
map.has(fnKey)
map.sizeMap 的键比较使用 SameValueZero 算法,它与 === 的唯一区别是 NaN === NaN 为 true。
const map2 = new Map([
['key1', 'value1'],
['key2', 'value2']
])
for (const [key, value] of map2) {
console.log(key, value)
}
const arr = [...map2.entries()]
const keys = [...map2.keys()]
const values = [...map2.values()]Set
Set 是值的集合,自动去重。同样使用 SameValueZero 算法判断唯一性。
const set = new Set([1, 2, 3, 2, 1])
set.size
set.add(4)
set.has(3)
set.delete(2)
const unique = [...new Set([1, 1, 2, 2, 3, 3])]
const a = new Set([1, 2, 3, 4])
const b = new Set([3, 4, 5, 6])
const union = new Set([...a, ...b])
const intersection = new Set([...a].filter(x => b.has(x)))
const difference = new Set([...a].filter(x => !b.has(x)))WeakMap
WeakMap 的键必须是对象或 Symbol(非注册 Symbol),且键是弱引用——当键对象没有其他引用时,垃圾回收器可以回收键和对应的值。
const wm = new WeakMap()
let element = document.createElement('div')
wm.set(element, { clicks: 0 })
wm.get(element)
element = nullWeakMap 不可迭代、没有 size 属性,这是因为其内容取决于垃圾回收的时机,是不确定的。典型应用场景:
const privateData = new WeakMap()
class Person {
constructor(name, age) {
privateData.set(this, { name, age })
}
getName() {
return privateData.get(this).name
}
getAge() {
return privateData.get(this).age
}
}
const p = new Person('Alice', 30)
p.getName()
privateData.get(p)const cache = new WeakMap()
function computeExpensive(obj) {
if (cache.has(obj)) {
return cache.get(obj)
}
const result = JSON.stringify(obj).length * Math.random()
cache.set(obj, result)
return result
}WeakSet
WeakSet 与 WeakMap 类似,存储对象的弱引用集合,常用于"标记"对象:
const visited = new WeakSet()
function processNode(node) {
if (visited.has(node)) return
visited.add(node)
console.log(`Processing: ${node.id}`)
}可选链 ?. 与空值合并 ??
可选链操作符
可选链 ?.(ES2020)在访问深层嵌套属性时,如果链上某个引用为 null 或 undefined,表达式会短路并返回 undefined,而非抛出 TypeError。
const user = {
profile: {
address: {
city: 'Beijing'
}
}
}
const city = user?.profile?.address?.city
const zip = user?.profile?.address?.zip
const country = user?.settings?.region?.country
const arr = [1, [2, [3]]]
arr?.[1]?.[1]?.[0]
const obj = {
greet() { return 'hello' }
}
obj.greet?.()
obj.missing?.()可选链的规范行为是:当 ?. 左侧的值为 null 或 undefined 时,整个链的剩余部分不再求值(短路求值),直接返回 undefined。这意味着 ?. 后面的属性访问、方法调用、计算属性等都不会执行。
空值合并操作符
??(ES2020)在左侧为 null 或 undefined 时返回右侧值。与 || 的关键区别在于:|| 对所有 falsy 值(0、''、false、NaN)都会取右侧值,而 ?? 仅对 null 和 undefined 生效。
const count = 0
count || 10
count ?? 10
const text = ''
text || 'default'
text ?? 'default'
const isActive = false
isActive || true
isActive ?? true组合使用
const config = {
server: {
port: 0,
host: ''
}
}
const port = config?.server?.port ?? 8080
const host = config?.server?.host ?? 'localhost'
const timeout = config?.server?.timeout ?? 3000逻辑赋值运算符
ES2021 引入的逻辑赋值运算符与 ?? 形成完整的生态:
let a = null
a ??= 'default'
let b = 0
b ||= 42
let c = 1
c &&= 2a ??= b 等价于 a ?? (a = b),注意它是短路的——如果 a 不是 nullish,赋值操作不会发生(不会触发 setter)。
BigInt
BigInt(ES2020)是 JavaScript 的第八种原始类型,用于表示任意精度的整数。它的引入解决了 Number 类型在超过 Number.MAX_SAFE_INTEGER(2^53 - 1)后精度丢失的问题。
const big = 9007199254740993n
const also = BigInt('9007199254740993')
9007199254740992n === 9007199254740993n
Number(9007199254740993)运算规则
BigInt 支持所有算术运算符,但不能与 Number 混合运算:
10n + 20n
2n ** 100n
100n / 3n
typeof 42nBigInt 在除法中会截断小数部分(类似整数除法),这与 Number 的浮点除法行为不同。
比较与条件
42n === 42
42n == 42
0n ? 'truthy' : 'falsy'
1n ? 'truthy' : 'falsy'
const sorted = [3n, 1, 4n, 1n, 5, 9n, 2n].sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})BigInt 不能用于 Math 对象的方法,不能与 JSON.stringify 直接序列化(会抛错),需要自定义序列化逻辑。
globalThis
globalThis(ES2020)提供了一种跨环境获取全局对象的标准方式。在此之前,不同环境中全局对象的访问方式不一致:
| 环境 | 全局对象 |
|---|---|
| 浏览器 | window / self / frames |
| Web Worker | self |
| Node.js | global |
globalThis.setTimeout === setTimeout
globalThis.myGlobal = 'accessible everywhere'
const getGlobal = () => {
if (typeof globalThis !== 'undefined') return globalThis
if (typeof window !== 'undefined') return window
if (typeof global !== 'undefined') return global
if (typeof self !== 'undefined') return self
throw new Error('Unable to locate global object')
}globalThis 的规范定义确保它在所有 JavaScript 环境中都是可写、可配置的全局属性,指向当前环境的全局对象。它的引入让编写跨平台(浏览器 / Node.js / Deno / Worker)的代码变得更加一致。
function isNodeEnvironment() {
return typeof globalThis.process !== 'undefined'
&& typeof globalThis.process.versions?.node !== 'undefined'
}
function isBrowserEnvironment() {
return typeof globalThis.window !== 'undefined'
&& typeof globalThis.document !== 'undefined'
}知识脉络总结
ES6+ 的特性并非孤立存在,它们形成了一张紧密关联的知识网络:
- 迭代器协议是数组解构、展开运算符、
for...of、Promise.all等众多语法特性的底层基础。 - Generator 是迭代器协议的最佳实践方式,也是
async/await的前身(async 函数本质上是自动执行的 Generator)。 - Proxy/Reflect 为元编程打开了大门,是现代响应式框架(Vue 3)和各种工具库的基石。
- Symbol 作为语言级协议标识符,连接了迭代器(
Symbol.iterator)、类型转换(Symbol.toPrimitive)等核心机制。 - WeakMap/WeakSet 与垃圾回收机制深度耦合,是实现"不泄漏的关联数据"的唯一正确方案。
- 可选链与空值合并看似简单,却深刻影响了代码的防御性编程范式。
理解这些特性之间的关联,比单独记忆每个 API 更加重要。