Skip to content

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  // 警告:不能修改只读属性: count

Vue 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: 1

Reflect(ES2015)

大白话解释: Reflect 就像"执行原始操作的工具"。当 Proxy 拦截了操作后,用 Reflect 来执行原始的操作。

为什么 Proxy 和 Reflect 要配合使用?

  • Proxy:拦截操作(门卫拦截)
  • Reflect:执行原始操作(门卫放行后执行原始动作)

为什么要用 Reflect 而不是直接操作 target?

  • 正确的 this 指向:Reflect.get 的 receiver 参数能正确处理继承场景
  • 统一的返回值:Reflect 的修改类方法(如 definePropertydeleteProperty)返回布尔值而非抛错,便于统一处理
  • 与 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)

生成器是一种特殊的函数,可以暂停执行和恢复执行。

什么是生成器?

普通函数一旦开始执行,就会一直运行到结束。生成器函数不同:它可以在执行过程中"暂停",等需要的时候再"恢复"执行。

什么时候用生成器?

  1. 处理大数据集:不需要一次性把所有数据加载到内存,可以按需生成
  2. 异步流程控制:在 async/await 出现之前,Generator + co 是处理异步的主流方案
  3. 状态机:游戏中的回合、表单的步骤等需要"记住状态"的场景
  4. 自定义迭代协议:实现自己的 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 JSON

parse

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

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准