toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
20 stars 1 forks source link

Proxy 与 Reflect #334

Open toFrankie opened 5 months ago

toFrankie commented 5 months ago

元编程

元编程(meta-programming)一般分为两类,一是在编译时生成代码,二是在运行时修改代码行为。

Just like metadata is data about data, metaprogramming is writing programs that manipulate programs. It's a common perception that metaprograms are the programs that generate other programs. But the paradigm is even broader. All of the programs designed to read, analyze, transform, or modify themselves are examples of metaprogramming. Metaprogramming in Python

怎么理解元编程?

Proxy

在 JavaScript 中,Proxy 属于元编程的一种。

简介

如果你问我多大,通过 person.age 访问得到 20

const person = {
 name: 'Frankie',
 age: 20,
}

但这届年轻人,总是说「别问,问就是 18」,那么我会创建一个替身:

const substitute = new Proxy(person, {
  get(target, property) {
    if (property === 'age') {
      return 18
    }
    return target[property]
  },
})

这样,再问我年龄时,你问的其实是 substitute,此时 substitute.age 是 18。尽管我真实年龄 person.age 是 20。

Proxy 通常用于修改某些操作的默认行为。比如,别人访问我的年龄,讲道理应该返回真实年龄(默认行为),但由于某些原因(心情不爽),就告诉你我 18。

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

基础语法

const proxy = new Proxy(target, handler)

其中 handler 有以下方法:

所有方法都是可选的。如果某个方法未定义,将会保留源对象的默认行为。

一个无操作转发代理:

const person = {}
const proxy = new Proxy(person, {})

proxy.name = 'Frankie'

console.log(person.name) // 'Frankie'

get/set 方法

用于拦截对象的读取、赋值。

const person = {
  name: 'Frankie',
  age: 20,
}

const handler = {
  get(target, property, receiver) {
    console.log(`Getting ${property}`)
    return Reflect.get(target, property, receiver)
  },

  set(target, property, value, receiver) {
    console.log(`Setting ${property}`)
    return Reflect.set(target, property, value, receiver)
  },
}

const proxy = new Proxy(person, handler)

当读取 proxy.name 或赋值 proxy.name = 'foo' 就会对应触发 getset 方法。

参数:

返回值:

约束:

receiver 不是代理实例本身的反例:

const empty = {}

const proxy = new Proxy(
  {},
  {
    get(target, property, receiver) {
      console.log(receiver === proxy) // ?
      console.log(receiver === empty) // ?
      return Reflect.get(target, property, receiver)
    },
  }
)

Object.setPrototypeOf(empty, proxy)

empty.foo

当读取 empty.foo 时,因本身没有 foo 属性,则从原型链 proxy 上找,触发 get 方法,此时打印结果分别是 falsetrue。也就是说,此时的 receiverempty 对象,而非 proxy 实例。

其他方法

除了最常用的拦截属性读写操作之外,还可以拦截以下操作:

handler 方法 拦截操作
get() 针对属性读取的拦截。
set() 针对属性赋值的拦截。
has() 针对 in 操作符的拦截。
apply() 针对函数调用的拦截。
construct() 针对 new 操作符的构造函数调用的拦截。
defineProperty() 针对 Object.defineProperty() 操作的拦截。
deleteProperty() 针对 delete 操作符删除属性的拦截。
getOwnPropertyDescriptor() 针对 Object.getOwnPropertyDescriptor() 操作的拦截。
getPrototypeOf() 针对 Object.getPrototypeOf()Object.prototype.__proto__Object.prototype.isPrototypeOf()instanceof 操作的拦截。
setPrototypeOf() 针对 Object.setPrototypeOf() 操作的拦截。
ownKeys() 针对 Reflect.ownKeys() 操作的拦截。
isExtensible() 针对 Object.isExtensible() 操作的拦截。
preventExtensions() 针对 Object.preventExtensions() 操作的拦截。

以上 handler 所有方法,都会对应拦截 Reflect 的同名方法。

应用场景

防止访问私有属性。

const person = {
  name: 'Frankie',
  _phone: '12345678910',
}

const user = new Proxy(person, {
  get(target, property, receiver) {
    if (property.startsWith('_')) return undefined
    return Reflect.get(target, property, receiver)
  },
})

console.log(user._phone) // 'Frankie'
console.log(user._phone) // undefined

我们知道,在 JavaScript 中访问一些不存在的属性会返回 undefined,那么借助 Proxy 可以在访问未知/不存在的属性时添加一些 Warning。

数组负值索引:

Negative Array Index in Javascript

为什么 Vue 使用 Proxy 代替 Object.defineProperty?

关于 Object.defineProperty() 缺点:

这些问题在 Proxy 上都有较好且完整的支持。

但 Proxy 兼容性没那么好。它无法 polyfill。

Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled.

源码:

Proxy 性能

Reflect

Reflect 是一个内置「对象」。它不是函数,自然也不能当作普通函数或使用 new 关键字调用。

Object.prototype.toString.call(Reflect) // '[object Reflect]'

它跟 Proxy 的 handler 有着同名的方法:

Proxy 可以与 Reflect 搭配使用,前者负责拦截对象的操作,后者负责原有的默认行为。

设计 Reflect 的目的:

References