Proxy Pattern

JavaScript 代理模式的实现与应用场景

问题

什么是代理模式?如何在 JavaScript 中实现代理模式?

解答

代理模式是一种结构型设计模式,通过创建一个代理对象来控制对原始对象的访问。ES6 提供了原生的 Proxy 对象来实现这一模式。

基本语法

const proxy = new Proxy(target, handler)
// target: 被代理的对象
// handler: 定义拦截行为的对象

数据验证

// 创建一个带验证的用户对象
const userValidator = {
  set(target, prop, value) {
    if (prop === 'age') {
      if (typeof value !== 'number') {
        throw new TypeError('age 必须是数字')
      }
      if (value < 0 || value > 150) {
        throw new RangeError('age 必须在 0-150 之间')
      }
    }
    target[prop] = value
    return true
  }
}

const user = new Proxy({}, userValidator)

user.name = 'Tom'  // 正常
user.age = 25      // 正常
// user.age = -1   // RangeError: age 必须在 0-150 之间
// user.age = 'abc' // TypeError: age 必须是数字

属性访问控制

// 私有属性保护
const privateHandler = {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error(`无法访问私有属性 ${prop}`)
    }
    return target[prop]
  },
  set(target, prop, value) {
    if (prop.startsWith('_')) {
      throw new Error(`无法修改私有属性 ${prop}`)
    }
    target[prop] = value
    return true
  }
}

const obj = new Proxy({ _secret: '123', name: 'test' }, privateHandler)

console.log(obj.name)    // 'test'
// console.log(obj._secret) // Error: 无法访问私有属性 _secret

函数调用拦截

// 记录函数调用日志
function createLoggedFunction(fn) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      console.log(`调用 ${target.name},参数:`, args)
      const result = target.apply(thisArg, args)
      console.log(`返回值:`, result)
      return result
    }
  })
}

function add(a, b) {
  return a + b
}

const loggedAdd = createLoggedFunction(add)
loggedAdd(1, 2)
// 调用 add,参数: [1, 2]
// 返回值: 3

缓存代理

// 缓存计算结果
function createCachedFunction(fn) {
  const cache = new Map()
  
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args)
      
      if (cache.has(key)) {
        console.log('从缓存获取')
        return cache.get(key)
      }
      
      const result = target.apply(thisArg, args)
      cache.set(key, result)
      return result
    }
  })
}

function expensiveCalculation(n) {
  console.log('执行计算...')
  return n * n
}

const cachedCalc = createCachedFunction(expensiveCalculation)

cachedCalc(5) // 执行计算... 25
cachedCalc(5) // 从缓存获取 25
cachedCalc(3) // 执行计算... 9

响应式数据(Vue 3 原理)

// 简化版响应式实现
function reactive(obj) {
  return new Proxy(obj, {
    get(target, prop) {
      console.log(`读取 ${prop}`)
      // 实际实现中这里会收集依赖
      return target[prop]
    },
    set(target, prop, value) {
      console.log(`设置 ${prop} = ${value}`)
      target[prop] = value
      // 实际实现中这里会触发更新
      return true
    }
  })
}

const state = reactive({ count: 0 })
state.count      // 读取 count
state.count = 1  // 设置 count = 1

常用 handler 方法

const handler = {
  get(target, prop, receiver) {},        // 读取属性
  set(target, prop, value, receiver) {}, // 设置属性
  has(target, prop) {},                  // in 操作符
  deleteProperty(target, prop) {},       // delete 操作符
  apply(target, thisArg, args) {},       // 函数调用
  construct(target, args) {},            // new 操作符
  getPrototypeOf(target) {},             // Object.getPrototypeOf
  setPrototypeOf(target, proto) {},      // Object.setPrototypeOf
  ownKeys(target) {},                    // Object.keys 等
  defineProperty(target, prop, desc) {}, // Object.defineProperty
}

关键点

  • Proxy 可以拦截对象的 13 种操作,比 Object.defineProperty 更强大
  • handler 中的方法称为”陷阱”(trap),用于自定义拦截行为
  • Vue 3 使用 Proxy 替代 Vue 2 的 Object.defineProperty 实现响应式
  • Proxy 可以代理数组,能拦截 pushpop 等方法
  • Reflect 对象通常与 Proxy 配合使用,提供默认行为