Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy 与 Reflect #334

Open
toFrankie opened this issue Apr 5, 2024 · 0 comments
Open

Proxy 与 Reflect #334

toFrankie opened this issue Apr 5, 2024 · 0 comments
Labels
2024 2024 年撰写 JS 与 JavaScript、ECMAScript 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章

Comments

@toFrankie
Copy link
Owner

toFrankie commented Apr 5, 2024

元编程

元编程(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)
  • target - 被代理的对象(下文称为源对象)。可以是任意类型的对象,比如数组、函数、另一个代理对象等。
  • handler - 一个含有特定方法的对象。

其中 handler 有以下方法:

  • handler.get()
  • handler.set()
  • handler.has()
  • handler.apply()
  • handler.construct()
  • handler.defineProperty()
  • handler.deleteProperty()
  • handler.getOwnPropertyDescriptor()
  • handler.getPrototypeOf()
  • handler.setPrototypeOf()
  • handler.ownKeys()
  • handler.isExtensible()
  • handler.preventExtensions()

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

一个无操作转发代理:

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 方法。

参数:

  • target - 源对象。
  • property - 被读取/赋值的属性名。
  • value - 将被赋值的值(仅 set 方法有)。
  • receiver - 最初接收赋值的对象。通常是代理实例本身。

返回值:

  • get() 方法可返回任意值。
  • set() 方法返回布尔值,true 表示属性设置成功。

约束:

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() 缺点:

  • 对于属性众多、嵌套更深的对象,需要遍历、深层监听,可能会带来性能问题。
  • 无法监听到对象属性的新增/删除,需要额外添加新的 API 实现,比如 setdelete
  • 无法监听数组 API,加之数组长度可能很大,如果使用对象那种遍历、深层监听的方式,性能更加糟糕了,所以重写了 pushpop 等方式。

这些问题在 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 有着同名的方法:

  • Reflect.get()
  • Reflect.set()
  • Reflect.has()
  • Reflect.apply()
  • Reflect.construct()
  • Reflect.defineProperty()
  • Reflect.deleteProperty()
  • Reflect.getOwnPropertyDescriptor()
  • Reflect.getPrototypeOf()
  • Reflect.setPrototypeOf()
  • Reflect.ownKeys()
  • Reflect.isExtensible()
  • Reflect.preventExtensions()

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

设计 Reflect 的目的:

  • 未来 JS 新语法可能只部署到 Reflect 上。类似于可迭代的 Map 一样,假设未来要新增一种数据结构,也会基于可迭代的趋势去设计。
  • 修改某些 Object 方法的返回结果,变得更合理。比如 Reflect.defineProperty() 如果无法定义属性,就会返回 false,而不像 Object.defineProperty() 在无法定义属性时抛出错误。
  • 统一为函数行为,比如原来的 delete obj[key] 删除属性,现在则是 Reflect.deleteProperty(obj, key)
  • 设计与 Proxy handler 一致,都有对应的方法。

References

@toFrankie toFrankie added 2024 2024 年撰写 JS 与 JavaScript、ECMAScript 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章 labels Apr 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2024 2024 年撰写 JS 与 JavaScript、ECMAScript 相关的文章 前端 与 JavaScript、ECMAScript、Web 前端相关的文章
Projects
None yet
Development

No branches or pull requests

1 participant