JavaScript 进阶特性
Proxy/Reflect、迭代器与生成器、错误处理、JSON 等进阶知识。
Proxy(ES2015)
大白话解释: Proxy 就像给对象"装了一个门卫"。所有对对象的操作(读取、修改、删除、检查)都要经过这个门卫,门卫可以拦截这些操作,做你想做的事情(比如记录日志、验证数据、触发更新)。
Proxy vs Reflect 的关系:
- Proxy:拦截操作(门卫拦截)
- Reflect:执行原始操作(门卫放行后执行原始动作)
Proxy 能拦截什么?
- 读取属性(get)
- 设置属性(set)
- 删除属性(deleteProperty)
- 检查属性(has)
- 函数调用(apply)
- new 操作(construct)
- 等等共 13 种操作
什么时候用 Proxy?
- Vue 3 响应式:拦截数据变化,触发视图更新
- 数据验证:设置属性时验证值是否合法
- 日志记录:记录所有对对象的操作
- 默认值:访问不存在的属性时返回默认值
Proxy 可以拦截对象的基本操作(读取、设置、删除等),是 Vue 3 响应式的底层机制。
基本用法
js
const target = { name: '张三', age: 25 }
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`删除 ${String(key)}`)
return Reflect.deleteProperty(target, key)
},
})
proxy.name // 日志:读取 name → '张三'
proxy.age = 26 // 日志:设置 age = 26
delete proxy.age // 日志:删除 age可拦截的操作(共 13 种)
js
const handler = {
// 属性操作
get(target, key, receiver) {}, // 读取属性
set(target, key, value, receiver) {}, // 设置属性
has(target, key) {}, // in 操作符
deleteProperty(target, key) {}, // delete 操作符
// 函数调用
apply(target, thisArg, args) {}, // 函数调用
construct(target, args) {}, // new 操作符
// 对象操作
getPrototypeOf(target) {}, // Object.getPrototypeOf
setPrototypeOf(target, proto) {}, // Object.setPrototypeOf
isExtensible(target) {}, // Object.isExtensible
preventExtensions(target) {}, // Object.preventExtensions
// 属性描述符
getOwnPropertyDescriptor(target, key) {},
defineProperty(target, key, desc) {},
// 遍历
ownKeys(target) {}, // Object.keys / for...in
}常见用途
数据验证
js
const schema = {
name: { type: 'string', required: true },
age: { type: 'number', min: 0, max: 150 },
}
function createValidated(obj, schema) {
return new Proxy(obj, {
set(target, key, value) {
const rule = schema[key]
if (!rule) return Reflect.set(target, key, value) // 没有规则的属性直接放行
if (typeof value !== rule.type) {
throw new TypeError(`${key} 必须是 ${rule.type}`)
}
if (rule.min !== undefined && value < rule.min) {
throw new RangeError(`${key} 不能小于 ${rule.min}`)
}
if (rule.max !== undefined && value > rule.max) {
throw new RangeError(`${key} 不能大于 ${rule.max}`)
}
return Reflect.set(target, key, value)
},
})
}
const user = createValidated({}, schema)
user.name = '张三' // ✅
user.age = 25 // ✅
// user.age = -1 // ❌ RangeError: age 不能小于 0
// user.age = '25' // ❌ TypeError: age 必须是 number默认值
js
const defaults = { theme: 'light', lang: 'zh-CN', pageSize: 20 }
const config = new Proxy(defaults, {
get(target, key) {
return key in target ? target[key] : '未设置'
},
})
console.log(config.theme) // 'light'
console.log(config.unknown) // '未设置'只读对象
js
function readonly(obj) {
return new Proxy(obj, {
set(target, key) {
console.warn(`不能修改只读属性: ${String(key)}`)
return false
},
deleteProperty() {
console.warn('不能删除只读对象的属性')
return false
},
})
}
const state = readonly({ count: 0, name: 'test' })
state.count = 1 // 警告:不能修改只读属性: countVue 3 响应式简化原理
js
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 deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (deps) deps.forEach((fn) => fn())
}
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 result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
// 使用
const state2 = reactive({ count: 0, user: { name: '张三' } })
effect(() => {
console.log('count is:', state2.count)
})
state2.count = 1 // 自动触发:count is: 1Reflect(ES2015)
大白话解释: Reflect 就像"执行原始操作的工具"。当 Proxy 拦截了操作后,用 Reflect 来执行原始的操作。
为什么 Proxy 和 Reflect 要配合使用?
- Proxy:拦截操作(门卫拦截)
- Reflect:执行原始操作(门卫放行后执行原始动作)
为什么要用 Reflect 而不是直接操作 target?
- 正确的 this 指向:Reflect.get 的 receiver 参数能正确处理继承场景
- 统一的返回值:Reflect 的修改类方法(如
defineProperty、deleteProperty)返回布尔值而非抛错,便于统一处理 - 与 Proxy trap 一一对应:代码更清晰
Reflect 是一个内置对象,提供与 Proxy trap 一一对应的静态方法。
为什么要用 Reflect
js
const source = { name: '张三' }
const proxy = new Proxy(source, {
// ❌ 不推荐:直接操作 source
get(target, key) {
return target[key]
},
// ✅ 推荐:使用 Reflect
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
})
// receiver 的作用:正确处理 this 指向
const parent = new Proxy({ value: 1 }, {
get(target, key, receiver) {
console.log('parent get', key)
return Reflect.get(target, key, receiver)
},
})
const child = Object.create(parent)
child.value // receiver 是 child,this 指向正确常用方法
js
// 与 Proxy trap 一一对应
Reflect.get(target, key, receiver)
Reflect.set(target, key, value, receiver)
Reflect.has(target, key) // 等价于 key in target
Reflect.deleteProperty(target, key) // 等价于 delete target[key]
Reflect.ownKeys(target) // 获取所有键(含 Symbol)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, proto)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.defineProperty(target, key, desc)
Reflect.getOwnPropertyDescriptor(target, key)
Reflect.apply(func, thisArg, args)
Reflect.construct(Constructor, args)优势:统一的返回值
js
// Object 方法没有统一的返回值和错误处理
try {
Object.defineProperty(source, 'a', { value: 1 })
} catch (e) {
// 抛错
}
// Reflect 返回布尔值,不会抛错
const success = Reflect.defineProperty(source, 'a', { value: 1 })
console.log(success) // true/false迭代器与可迭代协议
大白话解释: 迭代器就像"自动售货机"。你按一下按钮(调用 next()),它就吐出一个商品(返回 { value, done }),直到商品卖完(done: true)。可迭代协议就是"这台机器支持按按钮"的标准接口(实现了 Symbol.iterator 方法)。
什么时候用迭代器?
- 自定义遍历逻辑:比如遍历树结构、链表、数据库查询结果
- 惰性计算:按需生成值,不一次性加载所有数据
- 配合 for...of / 展开运算符 / 解构赋值使用
可迭代协议(Iterable)
实现了 Symbol.iterator 方法的对象就是可迭代对象。
js
// 数组是可迭代对象
const arr = [1, 2, 3]
const iter = arr[Symbol.iterator]()
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { value: undefined, done: true }自定义可迭代对象
js
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from
const last = this.to
return {
next() {
if (current <= last) {
return { value: current++, done: false }
}
return { value: undefined, done: true }
},
}
},
}
// 现在可以用 for...of
for (const num of range) {
console.log(num) // 1, 2, 3, 4, 5
}
// 展开运算符也生效
console.log([...range]) // [1, 2, 3, 4, 5]for...of vs for...in
js
const arr = [10, 20, 30]
arr.custom = 'hello'
// for...of —— 遍历可迭代对象的值
for (const val of arr) {
console.log(val) // 10, 20, 30(不含 custom)
}
// for...in —— 遍历对象的可枚举键
for (const key in arr) {
console.log(key) // '0', '1', '2', 'custom'
}生成器(Generator)
生成器是一种特殊的函数,可以暂停执行和恢复执行。
什么是生成器?
普通函数一旦开始执行,就会一直运行到结束。生成器函数不同:它可以在执行过程中"暂停",等需要的时候再"恢复"执行。
什么时候用生成器?
- 处理大数据集:不需要一次性把所有数据加载到内存,可以按需生成
- 异步流程控制:在 async/await 出现之前,Generator + co 是处理异步的主流方案
- 状态机:游戏中的回合、表单的步骤等需要"记住状态"的场景
- 自定义迭代协议:实现自己的
for...of可遍历对象
基本语法
js
function* count() {
yield 1
yield 2
yield 3
}
const gen = count()
console.log(gen.next()) // { value: 1, done: false }
console.log(gen.next()) // { value: 2, done: false }
console.log(gen.next()) // { value: 3, done: false }
console.log(gen.next()) // { value: undefined, done: true }
// 生成器是可迭代对象
for (const n of count()) {
console.log(n) // 1, 2, 3
}
console.log([...count()]) // [1, 2, 3]无限序列
js
function* fibonacci() {
let a = 0, b = 1
while (true) {
yield a
;[a, b] = [b, a + b]
}
}
const fib = fibonacci()
fib.next().value // 0
fib.next().value // 1
fib.next().value // 1
fib.next().value // 2
fib.next().value // 3
fib.next().value // 5
// 取前 10 个
function take(iterable, n) {
const result = []
for (const val of iterable) {
result.push(val)
if (result.length >= n) break
}
return result
}
take(fibonacci(), 10) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]yield* 委托
js
function* concat(iter1, iter2) {
yield* iter1
yield* iter2
}
const result = concat([1, 2], [3, 4])
console.log([...result]) // [1, 2, 3, 4]
// 递归生成器
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item)
} else {
yield item
}
}
}
console.log([...flatten([1, [2, [3, [4]]]])]) // [1, 2, 3, 4]双向通信
js
function* dialog() {
const name = yield '你叫什么名字?'
const age = yield `你好 ${name},你多大了?`
return `${name},${age}岁`
}
const gen = dialog()
console.log(gen.next().value) // '你叫什么名字?'
console.log(gen.next('张三').value) // '你好 张三,你多大了?'
console.log(gen.next(25).value) // '张三,25岁'异步生成器
js
async function* fetchPages(urls) {
for (const url of urls) {
const res = await fetch(url)
yield await res.json()
}
}
// 使用 for-await-of
async function main() {
const urls = ['/api/page1', '/api/page2', '/api/page3']
for await (const data of fetchPages(urls)) {
console.log(data)
}
}错误处理
try / catch / finally
js
try {
const data = JSON.parse(input)
processData(data)
} catch (error) {
console.error('解析失败:', error.message)
} finally {
// 无论成功失败都执行
loading.value = false
}错误类型
js
// 内置错误类型
new Error('通用错误')
new TypeError('类型错误')
new RangeError('范围错误')
new ReferenceError('引用错误')
new SyntaxError('语法错误')
new URIError('URI 错误')
new EvalError('Eval 错误')
// 错误属性
const err = new Error('出错了')
err.message // '出错了'
err.name // 'Error'
err.stack // 错误堆栈信息自定义错误
js
class AppError extends Error {
constructor(message, code, details) {
super(message)
this.name = 'AppError'
this.code = code
this.details = details
}
}
class ValidationError extends AppError {
constructor(field, message) {
super(message, 'VALIDATION_ERROR', { field })
this.name = 'ValidationError'
}
}
// 使用
throw new ValidationError('email', '邮箱格式不正确')Error.cause(ES2022)
js
// 链式错误:保留原始错误信息
try {
const data = await fetchUserData(userId)
} catch (err) {
throw new Error('获取用户数据失败', { cause: err })
}
// 捕获时可以访问原始错误
try {
await loadPage()
} catch (err) {
console.error(err.message) // '获取用户数据失败'
console.error(err.cause.message) // 原始错误信息
}异步错误处理
js
// async/await + try/catch
async function fetchData() {
try {
const res = await fetch('/api/data')
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
return await res.json()
} catch (err) {
console.error('请求失败:', err)
throw err // 重新抛出或返回默认值
}
}
// Promise 链
fetch('/api/data')
.then((res) => res.json())
.catch((err) => {
console.error('请求失败:', err)
return defaultValue
})
.finally(() => {
loading = false
})
// 全局错误捕获
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 拒绝:', event.reason)
})
window.addEventListener('error', (event) => {
console.error('未捕获的错误:', event.error)
})JSON
stringify
js
const obj = { name: '张三', age: 25, active: true }
JSON.stringify(obj) // '{"name":"张三","age":25,"active":true}'
JSON.stringify(obj, null, 2) // 格式化(2 空格缩进)
// replacer(过滤/转换)
JSON.stringify(obj, ['name', 'age']) // '{"name":"张三","age":25}'
JSON.stringify(obj, (key, value) => {
if (key === 'age') return undefined // 排除 age
return value
})
// toJSON 方法
const date = {
time: new Date(),
toJSON() {
return this.time.toISOString()
},
}
JSON.stringify(date) // '{"time":"2024-01-15T00:00:00.000Z"}'不可序列化的值
js
JSON.stringify(undefined) // undefined(不是字符串)
JSON.stringify(function () {}) // undefined
JSON.stringify(Symbol()) // undefined
JSON.stringify(NaN) // 'null'
JSON.stringify(Infinity) // 'null'
JSON.stringify(new Date()) // '"2024-01-15T00:00:00.000Z"'(调用 toJSON)
// 循环引用会报错
const obj = {}
obj.self = obj
// JSON.stringify(obj) // ❌ TypeError: Converting circular structure to JSONparse
js
JSON.parse('{"name":"张三","age":25}')
// { name: '张三', age: 25 }
// reviver(转换函数)
JSON.parse('{"date":"2024-01-15T00:00:00.000Z"}', (key, value) => {
if (key === 'date') return new Date(value)
return value
})
// { date: Date 对象 }JSON 深拷贝的局限
js
// JSON 深拷贝(有限制)
const copy = JSON.parse(JSON.stringify(obj))
// ❌ 不能处理的类型:
// - undefined → 丢失
// - 函数 → 丢失
// - Symbol → 丢失
// - RegExp → 变成 '{}'
// - Date → 变成字符串
// - Map/Set → 变成 '{}'
// - 循环引用 → 报错
// - BigInt → 报错
// ✅ 推荐用 structuredClone(Web API,Chrome 98+ 支持)
const deepCopy = structuredClone(obj)
// 支持:Date, RegExp, Map, Set, ArrayBuffer, Blob, File, 循环引用
// 不支持:函数, Symbol