代理(Proxy)与反射(Reflect)
ES6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。
代理
代理可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理施予的行为。
代理是使用 Proxy
构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出 TypeError。
创建空代理
最简单的代理是空代理,即除了作为一个抽象的目标对象,什么也不做。默认情况下,在代理对象上执行的所有操作都会无障碍地传播到目标对象。因此,在任何可以使用目标对象的地方,都可以通过同样的方式来使用与之关联的代理对象。
// 目标对象
const target = {
id: 'target',
}
// 处理程序对象
const handler = {}
// 创建一个空代理
const proxy = new Proxy(target, handler)
// id 属性会访问同一个值
console.log(target.id) // target
console.log(proxy.id) // target
// 给目标属性赋值会反映在两个对象上,因为两个对象访问的是同一个值
target.id = 'foo'
console.log(target.id) // foo
console.log(proxy.id) // foo
// 给代理属性赋值会反映在两个对象上,因为这个赋值会转移到目标对象
proxy.id = 'bar'
console.log(target.id) // bar
console.log(proxy.id) // bar
// hasOwnProperty()方法在两个地方都会应用到目标对象
console.log(target.hasOwnProperty('id')) // true
console.log(proxy.hasOwnProperty('id')) // true
// Proxy.prototype 是 undefined,因此不能使用 instanceof 操作符
console.log(target instanceof Proxy) // TypeError: Function has non-object prototype 9 'undefined' in instanceof check
console.log(proxy instanceof Proxy) // TypeError: Function has non-object prototype 'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy) // false
捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
下面是代理支持的拦截操作,一共 13 种。
1. get(target, property, receiver)
get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。
const myTarget = {}
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
return 'hello'
},
})
proxy.foo // hello
返回值无限制。
proxy.property
proxy[property]
Object.create(proxy)[property]
target: 目标对象
property: 要访问的属性
receiver: 代理对象或继承代理对象的对象。
如果 target.property 不可写且不可配置,则处理程序返回的值必须与 target.property 匹配。
如果 target.property 不可配置且[[Get]]特性为 undefined,处理程序的返回值也必须是 undefined。
2. set(target, property, value, receiver)
set()捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。
const myTarget = {}
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
target[property] = 'hello ' + value
return true
},
})
proxy.foo = 'world'
proxy.foo // "hello world"
返回 true 表示成功。
返回 false 表示失败,严格模式下会抛出 TypeError。
proxy.property = value
proxy[property] = value
Object.create(proxy)[property] = value
target: 目标对象。
property: 引用的目标对象上的属性名。
value: 要赋给属性的值。
receiver: 接收最初赋值的对象。
如果 target.property 不可写且不可配置,则不能修改目标属性的值。
如果 target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性的值。
在严格模式下,处理程序中返回 false 会抛出TypeError。
3. has(target, property)
has()捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。
const myTarget = {
name: 'song',
}
const proxy = new Proxy(myTarget, {
has(target, property) {
return Reflect.has(target, property)
},
})
'name' in proxy // true
'age' in proxy // false
has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
property in proxy
property in Object.create(proxy)
with(proxy) {(property)}
target:目标对象。
property:引用的目标对象上的属性。
如果 target.property 存在且不可配置,则处理程序必须返回 true。
如果 target.property 存在且目标对象不可扩展,则处理程序必须返回 true。
4. defineProperty(target, property, descriptor)
defineProperty()捕获器会在 Object.defineProperty()中被调用。对应的反射 API 方法为 Reflect.defineProperty()。
const myTarget = {}
const proxy = new Proxy(myTarget, {
defineProperty(target, property, descriptor) {
return Reflect.defineProperty(target, property, descriptor)
},
})
Object.defineProperty(proxy, 'foo', { value: 'bar' })
proxy.foo // "bar"
myTarget.foo // "bar"
defineProperty()必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。
Object.defineProperty(proxy, property, descriptor)
target: 目标对象。
property: 引用的目标对象上的属性。
descriptor: 包含可选的 enumerable、configurable、writable、value、get 和 set
定义的对象。
如果目标对象不可扩展,则无法定义属性。
如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性。
如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性。
5. getOwnPropertyDescriptor(target, property)
getOwnPropertyDescriptor()捕获器会在 Object.getOwnPropertyDescriptor()中被调用。对应的反射 API 方法为 Reflect.getOwnPropertyDescriptor()。
const myTarget = {
name: 'song',
}
const proxy = new Proxy(myTarget, {
getOwnPropertyDescriptor(target, property) {
return Reflect.getOwnPropertyDescriptor(target, property)
},
})
Object.getOwnPropertyDescriptor(proxy, 'name') // {value: "song", writable: true, enumerable: true, configurable: true}
getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回 undefined。
Object.getOwnPropertyDescriptor(proxy, property)
target:目标对象。
property:引用的目标对象上的属性。
如果自有的 target.property 存在且不可配置,则处理程序必须返回一个表示该属性存在的 对象。
如果自有的 target.property 存在且可配置,则处理程序必须返回表示该属性可配置的对象。
如果自有的 target.property 存在且 target 不可扩展,则处理程序必须返回一个表示该属性存 在的对象。
如果 target.property 不存在且 target 不可扩展,则处理程序必须返回 undefined 表示该属 性不存在。
如果 target.property 不存在,则处理程序不能返回表示该属性可配置的对象。
6. deleteProperty(target, property)
deleteProperty()捕获器会在 delete 操作符中被调用。对应的反射 API 方法为 Reflect.deleteProperty()。
const myTarget = {
name: 'song',
}
const proxy = new Proxy(myTarget, {
deleteProperty(target, property) {
return Reflect.deleteProperty(target, property)
},
})
delete proxy.name
console.log(myTarget) // {}
deleteProperty()必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。
delete proxy.property
delete proxy[property]
target: 目标对象。
property: 引用的目标对象上的属性。
如果自有的 target.property 存在且不可配置,则处理程序不能删除这个属性。
7. ownKeys(target)
ownKeys()捕获器会在拦截对象自身属性的读取操作,如 Object.keys() 及类似方法中被调用。对应的反射 API 方法为 Reflect.ownKeys()。
const myTarget = {
name: 'song',
age: 29,
}
const proxy = new Proxy(myTarget, {
ownKeys(target) {
return Reflect.ownKeys(target)
},
})
Object.keys(proxy) // ["name", "age"]
ownKeys()必须返回包含字符串或符号的可枚举对象。
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
target: 目标对象。
返回的可枚举对象必须包含 target 的所有不可配置的自有属性。
如果 target 不可扩展,则返回可枚举对象必须准确地包含自有属性键。
8. getPrototypeOf(target)
getPrototypeOf()捕获器主要用来拦截获取对象原型,会在 Object.getPrototypeOf()中被调用。对应的反射 API 方法为 Reflect.getPrototypeOf()。
function Person(name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
const myTarget = new Person('song', 29)
const proxy = new Proxy(myTarget, {
getPrototypeOf(target) {
return Reflect.getPrototypeOf(target)
},
})
Object.getPrototypeOf(proxy) === Person.prototype // true
getPrototypeOf()必须返回对象或 null。
Object.getPrototypeOf(proxy)
proxy.__proto__
Object.prototype.isPrototypeOf(proxy)
proxy instanceof Object
target: 目标对象。
如果 target 不可扩展,则 Object.getPrototypeOf(proxy) 唯一有效的返回值就是 Object.getPrototypeOf(target) 的返回值。
9. setPrototypeOf(target, prototype)
setPrototypeOf()捕获器会在 Object.setPrototypeOf()中被调用。对应的反射 API 方法为 Reflect.setPrototypeOf()。
const myTarget = {}
const proxy = new Proxy(myTarget, {
setPrototypeOf(target, prototype) {
return Reflect.setPrototypeOf(target, prototype)
},
})
console.log(myTarget.prototype) // undefined
Object.setPrototypeOf(proxy, Object)
console.log(myTarget.prototype) // [object Object]
setPrototypeOf()必须返回布尔值,表示原型赋值是否成功。返回非布尔值会被转型为布尔值。
Object.setPrototypeOf(proxy)
target: 目标对象。
prototype:target 的替代原型,如果是顶级原型则为 null。
如果 target 不可扩展,则唯一有效的 prototype 参数就是 Object.getPrototypeOf(target) 的返回值。
10. isExtensible(target)
isExtensible()捕获器会在 Object.isExtensible()中被调用。对应的反射 API 方法为 Reflect.isExtensible()。
const myTarget = {}
const proxy = new Proxy(myTarget, {
isExtensible(target) {
return Reflect.isExtensible(target)
},
})
Object.isExtensible(proxy) // true
isExtensible()必须返回布尔值,表示 target 是否可扩展。返回非布尔值会被转型为布尔值。
Object.isExtensible(proxy)
target: 目标对象。
如果 target 可扩展,则处理程序必须返回 true。
如果 target 不可扩展,则处理程序必须返回 false。
11. preventExtensions(target)
preventExtensions()捕获器会在 Object.preventExtensions()中被调用。对应的反射 API 方法为 Reflect.preventExtensions()。
const myTarget = {}
const proxy = new Proxy(myTarget, {
preventExtensions(target) {
return Reflect.preventExtensions(...arguments)
},
})
console.log(Object.isExtensible(myTarget)) // true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(myTarget)) // false
preventExtensions()必须返回布尔值,表示 target 是否已经不可扩展。返回非布尔值会被转型为布尔值。
Object.preventExtensions(proxy)
target: 目标对象。
如果 Object.isExtensible(proxy)是 false,则处理程序必须返回 true。
12. apply(target, thisArg, argumentsList)
apply()捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply()。
const myTarget = () => {
console.log('myTarget')
}
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList) {
return Reflect.apply(...arguments)
},
})
proxy() // myTarget
返回值无限制。
proxy(...argumentsList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, ...argumentsList)
target: 目标对象。
thisArg: 调用函数时的 this 参数。
argumentsList: 调用函数时的参数列表
target 必须是一个函数对象。
13. construct(target, argumentsList, newTarget)
construct()捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()。
const myTarget = function (name) {
this.name = name
}
const proxy = new Proxy(myTarget, {
construct(target, argumentsList, newTarget) {
return Reflect.construct(...arguments)
},
})
new proxy('song').name // song
construct()必须返回一个对象。
new proxy(...argumentsList)
target: 目标构造函数。
argumentsList: 传给目标构造函数的参数列表。
newTarget: 最初被调用的构造函数。
target 必须可以用作构造函数。
代理的问题与不足
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。
1. 代理中的 this
在 Proxy 代理的情况下,目标对象内部的 this
关键字会指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy)
},
}
const handler = {}
const proxy = new Proxy(target, handler)
target.m() // false
proxy.m() // true
如上,一旦 proxy 代理 target,target.m() 内部的 this 就是指向 proxy,而不是 target。所以,虽然 proxy 没有做任何拦截,target.m() 和 proxy.m() 返回不一样的结果。
下面是一个通过 WeakMap 保存私有变量的例子。
const _name = new WeakMap()
class Person {
constructor(name) {
_name.set(this, name)
}
get name() {
return _name.get(this)
}
}
由于这个实现依赖 Person 实例的对象标识,在这个实例被代理的情况下就会出问题:
const person = new Person('song')
console.log(person.name) // song
const personInstanceProxy = new Proxy(person, {})
console.log(personInstanceProxy.name) // undefined
这是因为 Person 实例一开始使用目标对象作为 WeakMap 的键,代理对象却尝试从自身取得这个实例。要解决这个问题,就需要重新配置代理,把代理 Person 实例改为代理 Person 类本身。之后再创建代理的实例就会以代理实例作为 WeakMap 的键了:
const PersonClassProxy = new Proxy(Person, {})
const proxyPerson = new PersonClassProxy('wang')
console.log(proxyPerson.name)
2. 代理与内部槽位
代理与内置引用类型(比如 Array)的实例通常可以很好地协同,但有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错。
一个典型的例子就是 Date 类型。根据 ECMAScript 规范,Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:
const target = new Date()
const proxy = new Proxy(target, {})
console.log(proxy instanceof Date) // true
target.getDate() // 27
proxy.getDate() // TypeError: 'this' is not a Date object
反射
Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。
上面代理中处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。
Reflect 对象的设计目的有以下几个:
- 将 Object 对象的一些明显属于语言内部的方法,放到 Reflect 对象上。
比如 Object.defineProperty。
- 修改某些 Object 方法的返回结果,让其变得更合理。
比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。
jstry { Object.defineProperty(target, property, attributes) // success } catch (e) { // fail }
jsif (Reflect.defineProperty(target, property, attributes)) { // success } else { // fail }
- 让 Object 操作都变成函数行为。
某些 Object 操作是命令式的,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name) 和 Reflect.deleteProperty(obj, name) 让它们变成了函数行为。
js'assign' in Object // true
jsReflect.has(Object, 'assign') // true
- Reflect 对象的方法与 Proxy 对象的方法一一对应。
只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,总可以在 Reflect 上获取默认行为。
jsconst myTarget = {} const proxy = new Proxy(myTarget, { set(target, property, value) { const success = Reflect.set(target, property, value) if (success) { console.log('property ' + property + ' set to ' + value) } return success }, }) proxy.name = 'song' // property name set to song
上面代码中,Proxy 方法拦截 myTarget 对象的属性赋值行为。它采用 Reflect.set 方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
静态方法
Reflect 对象提供了以下静态方法,这些方法与 proxy handler 方法的命名相同。
1. Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于 target[name]
。
2. Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个 Boolean
,如果更新成功,则返回 true。
3. Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和 in 运算符
的功能完全相同。
4. Reflect.defineProperty(target, propertyKey, attributes)
和 Object.defineProperty()
类似。如果设置成功就会返回 true
。
5. Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于 Object.getOwnPropertyDescriptor()
。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined
。
6. Reflect.deleteProperty(target, propertyKey)
作为函数的 delete
操作符,相当于执行 delete target[name]
。
7. Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys()
, 但不会受 enumerable
影响)。
8. Reflect.getPrototypeOf(target)
类似于 Object.getPrototypeOf()
。
9. Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数。返回一个 Boolean
,如果更新成功,则返回 true
。
10. Reflect.isExtensible(target)
类似于 Object.isExtensible()
。
11. Reflect.preventExtensions(target)
类似于 Object.preventExtensions()
。返回一个 Boolean
。
12. Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply()
功能类似。
13. Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行 new
操作,相当于执行 new target(...args)
。
扩展
这些功能已经存在了,为什么还需要用 Reflect 实现一次?其实还是受到函数式编程思想的影响。
函数式编程
在 JS 中,函数式编程(Functional Programming,简称 FP)是一种编程范式,它侧重于使用函数来创建代码逻辑。这种编程风格强调函数的使用、不可变性和避免副作用。它与命令式编程(通过明确的步骤和状态改变来实现)相对,主要特点包括以下几个方面:
1. 函数是一等公民
在函数式编程中,函数被视为“一等公民”,这意味着函数可以像其他变量一样传递和操作。例如,函数可以作为参数传递给另一个函数,也可以作为另一个函数的返回值。
const add = (x, y) => x + y
const square = (x) => x * x
const operate = (fn, a, b) => fn(a, b)
console.log(operate(add, 2, 3)) // 5
2. 纯函数
纯函数是指在相同输入下总是返回相同输出,并且没有任何可观察的副作用的函数。纯函数使代码更具可预测性和可测试性。
const pureFunction = (x, y) => x + y
3. 不变性
在函数式编程中,数据是不可变的。即一旦创建,数据不会改变。相反,操作数据时总是返回一个新的数据副本。这避免了意外的副作用。
const arr = [1, 2, 3]
const newArr = arr.map((x) => x * 2)
console.log(arr) // [1, 2, 3]
console.log(newArr) // [2, 4, 6]
4. 高阶函数
高阶函数是指可以接受其他函数作为参数或返回一个函数的函数。这种特性使得函数组合和抽象变得非常强大。
const filter = (predicate, arr) => arr.filter(predicate)
const isEven = (x) => x % 2 === 0
console.log(filter(isEven, [1, 2, 3, 4])) // [2, 4]
5. 函数组合
函数组合是将多个小函数组合成一个大的函数。它可以通过管道或函数组合来实现,使得代码更具模块化和可重用性。
const compose = (f, g) => (x) => f(g(x))
const addOne = (x) => x + 1
const double = (x) => x * 2
const addOneThenDouble = compose(double, addOne)
console.log(addOneThenDouble(2)) // 6
6. 声明式编程
函数式编程强调“声明式”而非“命令式”编程。也就是说,你告诉计算机“是什么”,而不是“怎么做”。例如,使用 map、filter 和 reduce 等方法来处理数组,而不是使用循环。
const numbers = [1, 2, 3, 4]
const doubled = numbers.map((x) => x * 2)
console.log(doubled) // [2, 4, 6, 8]
JS 的函数式编程提供了一种优雅的方式来处理复杂的操作,通过使用纯函数
、高阶函数
和不可变数据
,可以写出更具可读性、可测试性和可维护性的代码。这种编程范式已经广泛应用于现代 JS 开发中,特别是在处理复杂的状态管理和数据流时(例如在 React 中)。