Proxy 和 Reflect
Proxy
Proxy对象可以包装另一个对象并拦截其读取/写入属性和其他操作。
js1let proxy = new Proxy(target, handler)
target—— 是要包装的对象,可以使任何东西,包括函数handler—— 代理配置:带有拦截操作(捕捉器)的方法的对象。比如get捕捉器用于读取target的属性,set捕捉器用于写入target的属性。
如果handler是空对象,即里面没有任何捕捉器,那么就直接对 target 进行处理。反之则通过捕捉器让 Proxy 对target进行处理。
例如,下面是一个没有捕捉器的代码:
js1let target = {} 2let proxy = new Proxy(target, {}) // 空的 handler 对象,没有捕捉器 3proxy.name = 'qiuyanxi' //对 proxy 写入内容,会直接影响到 target 4console.log(target.name) // qiuyanxi 5console.log(proxy.name) // qiuyanxi 6 7for (let key in proxy) console.log(key) // name
由于没有捕捉器,所有对proxy的操作都会直接转发给target
- 写入操作
proxy.name=会将值直接写入target - 读取操作
proxy.name会直接读取target.name - 对
proxy迭代会从target处返回对应的值
如果没有任何捕捉器,那么proxy就是一个target的透明包装器(wrapper)

如果要做更多操作,那么我们需要添加捕捉器。
捕捉器在里面的功能相当于拦截,我们可以用它们来拦截底层方法。
底层方法是位于JavaScript 的对象底层的方法,也就是内部方法,我们不能直接通过方法名调用它们。
内部方法描述了最底层的工作方式,例如[[Get]]用于读取属性,[[Set]]用于写入属性等。
这些最底层的内部方法都能够被 Proxy 捕捉器所拦截。
以下是每一种内部方法所对应的捕捉器:
| 内部方法 | Handler 方法 | 何时触发 |
|---|---|---|
[[Get]] | get | 读取属性 |
[[Set]] | set | 写入属性 |
[[HasProperty]] | has | in 操作符 |
[[Delete]] | deleteProperty | delete 操作符 |
[[Call]] | apply | 函数调用 |
[[Construct]] | construct | new 操作符 |
[[GetPrototypeOf]] | getPrototypeOf | Object.getPrototypeOf |
[[SetPrototypeOf]] | setPrototypeOf | Object.setPrototypeOf |
[[IsExtensible]] | isExtensible | Object.isExtensible |
[[PreventExtensions]] | preventExtensions | Object.preventExtensions |
[[DefineOwnProperty]] | defineProperty | Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] | getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries |
[[OwnPropertyKeys]] | ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries |
get 捕捉器
最常见的捕捉器用于读取/写入属性。
如果我们想要拦截读取操作,handler就应该具备get(target,property,receiver)方法。
读取属性时会触发这个方法,参数是这样的:
- target —— 目标对象,该对象被作为第一个参数传递给
new Proxy - Property —— 目标属性名
- receiver —— 如果目标属性是一个
getter属性,则receiver就是本次读取属性所在的this对象。通常,这就是proxy对象本身。(如果我们从 proxy 继承,则是从 proxy 继承的对象)
现在我们可以用get来实现一个对象的默认值。
一般来说,数组不存在的项都是返回 undefined,如果我们想要默认返回 0,则可以这么做:
js1let target = [1, 2, 3] 2let proxy = new Proxy(target, { 3 get(target, property, receiver) { 4 if (property in target) { 5 return target[property] 6 } 7 return 0 8 }, 9}) 10console.log(proxy[0]) //1 11console.log(proxy[4]) //0 没有这个属性
这种方法很有用,假设我们现在有一本词典:
js1let dictionary = { 2 hello: 'English', 3 你好: 'Chinese', 4}
我们可以将它变成它的代理,然后再拦截对它的读取操作
js1dictionary = new Proxy(dictionary, { 2 // 拦截对它的操作 3 get(target, property, receiver) { 4 //如果有这个词,就返回对应的语言 5 if (property in target) { 6 return target[property] 7 } 8 return property // 如果没有,就返回这个词 9 }, 10}) 11console.log(dictionary['hello']) // English 12console.log(dictionary['哈利路亚']) // 哈利路亚
变量名从原来的 target 指向了代理,这就意味着代理覆盖了变量。
js1dictionary = new Proxy(dictionary, ...);
代理应该在所有地方都完全替代目标对象。目标对象被代理后,任何人都不应该再引用目标对象。否则很容易搞砸。
set 捕捉器
JavaScript 的数组里面可以放任何的值,对象、数字、字符串等,假设我需要设置一个专门用于添加数字的数组,如果添加了其他类型的值就会抛出一个错误,就可以使用 set 捕捉器拦截。
set捕捉器参数如下:
js1set(target, property, value, receiver)
- target —— 目标对象
- property —— 目标属性名称
- value —— 目标属性的值
- receiver —— 与
get捕捉器类似,仅与 setter 访问器属性相关。
如果写入成功,则应该返回true,否则返回false(触发 TypeError)
js1let numbers = [] 2numbers = new Proxy(numbers, { 3 set(target, prop, value, receiver) { 4 if (typeof value === 'number') { 5 target[prop] = value 6 return true 7 } 8 return false 9 }, 10}) 11console.log((numbers[0] = 0)) // 添加成功 12console.log(numbers.push(1)) // 添加成功 13console.log((numbers[2] = 'test')) // TypeError
值得注意的是,使用数组的内置方法push也是有效的。代理对象 proxy 并不会破坏任何东西。我们不必重写push或者unshift等添加元素的数组方法(Vue2 是这么做的),就可以在其中添加检查,因为在内部它们使用代理所拦截的[[Set]]操作。
一定要返回
true。对于 set 操作,它必须在成功时返回 true,如果我们忘记这样做,或者返回 falsy 值,则该操作触发 TypeError。这是因为从底层设计来说,我们必须维持不变量。
不变量
JavaScript 强制执行某些不变量 — 内部方法和捕捉器必须满足的条件。
其中大多数用于返回值:
[[Set]]如果值已成功写入,则必须返回true,否则返回false。
[[Delete]]如果已成功删除该值,则必须返回true,否则返回false。应用于代理(proxy)对象的
[[GetPrototypeOf]],必须返回与应用于被代理对象的[[GetPrototypeOf]]相同的值。换句话说,读取代理对象的原型必须始终返回被代理对象的原型。...
捕捉器可以拦截这些操作,但是必须遵循这些规则以确保语言功能的正确和一致的行为。
使用ownKeys和getOwnPropertyDescriptor迭代
Object.keys,for..in 循环和大多数其他遍历对象属性的方法都使用内部方法 [[OwnPropertyKeys]](由 ownKeys 捕捉器拦截) 来获取属性列表。
这些方法在细节上有所不同:
Object.getOwnPropertyNames(obj)返回非 Symbol 键。Object.getOwnPropertySymbols(obj)返回 Symbol 键。Object.keys/values()返回带有enumerable标志的非 Symbol 键/值。for..in循环遍历所有带有enumerable标志的非 Symbol 键,以及原型对象的键。
我们可以使用ownKeys捕捉器来拦截for..in对user的遍历,并使用Object.keys和Object.values来跳过下划线为__开头的属性。
js1let user = { name: 'qiuyanxi', age: 20, _height: 180 } 2user = new Proxy(user, { 3 // 过滤掉开头带_的属性 4 ownKeys(target) { 5 return Object.keys(target).filter((item) => !item.startsWith('_')) 6 }, 7}) 8for (let key in user) { 9 console.log(key) // name age 10}
如果我们返回对象中不存在的键,Object.keys并不会列出这些键:
js1let user = {} 2 3user = new Proxy(user, { 4 ownKeys(target) { 5 return ['a', 'b', 'c'] 6 }, 7}) 8 9alert(Object.keys(user)) // <empty>
这是因为Object.keys仅返回带有enumerable标志的属性。为了检查它,该方法会首先对每个属性调用内部方法[[getOwnProperty]]来获取描述符(descriptor)。
但是上面的例子中并没有属性,那就意味着其属性描述符为空,没有enumerable标志,因此它被忽略了。
如果我们希望在这种情况下返回,那么我们可以使用getOwnPropertyDescriptor捕捉器拦截对[[getOwnProperty]]的调用,并返回enumberable:true的描述符。
js1let user = {} 2 3user = new Proxy(user, { 4 ownKeys(target) { 5 // 一旦要获取属性列表就会被调用 6 return ['a', 'b', 'c'] 7 }, 8 9 getOwnPropertyDescriptor(target, prop) { 10 // 被每个属性调用 11 return { 12 enumerable: true, 13 configurable: true, 14 /* ...其他标志,可能是 "value:..." */ 15 } 16 }, 17}) 18 19alert(Object.keys(user)) // a, b, c
deleteProperty 捕捉器
当我们想要删除某个属性时,可以通过deleteProperty来拦截删除操作。
比如我们不希望删除 name,则可以这么拦截:
js1let user = { name: 'qiuyanxi', age: '20' } 2user = new Proxy(user, { 3 deleteProperty(target, prop) { 4 if (prop === 'name') { 5 return false 6 } 7 delete target[prop] 8 return true 9 }, 10}) 11delete user.age // success 12console.log(user.age) // undefined 13delete user.name // TypeError
使用多个捕捉器来实现受保护属性
普遍约定下,以下划线__开头的属性和方法是内部的,我们不应该从对象外部访问它。
下面我们使用代理来禁止对__开头属性的任何访问:
- 不能读取该属性
- 不能修改该属性
- 不能删除该属性
- 不能遍历该属性
js1let user = { name: 'qiuyanxi', age: '20', _password: '***' } 2user = new Proxy(user, { 3 // 拦截读取属性 4 get(target, prop) { 5 if (prop.startsWith('_')) { 6 throw new Error(`you can not visit ${prop} `) 7 } 8 return target[prop] 9 }, 10 // 拦截写入属性 11 set(target, prop, value) { 12 if (prop.startsWith('_')) { 13 return false 14 } 15 target[prop] = value 16 return true 17 }, 18 // 拦截删除属性 19 deleteProperty(target, prop) { 20 if (prop.startsWith('_')) { 21 return false 22 } 23 delete target[prop] 24 return true 25 }, 26 // 拦截读取属性列表 27 ownKeys(target) { 28 return Object.keys(target).filter((key) => !key.startsWith('_')) 29 }, 30}) 31 32// "get" 不允许读取 _password 33try { 34 user._password 35} catch (error) { 36 alert(error.message) // you can not visit _password 37} 38 39// "set" 不允许写入 _password 40try { 41 user._password = '123456' 42} catch (error) { 43 alert(error.message) // 'set' on proxy: trap returned falsish for property '_password' 44} 45 46// "deleteProperty" 不允许删除 _password 47try { 48 delete user._password 49} catch (error) { 50 alert(error.message) // 'deleteProperty' on proxy: trap returned falsish for property '_password' 51} 52 53// "ownKeys" 将 _password 过滤出去 54alert(Object.keys(user)) // name age
拦截读取属性时的注意点
在读取属性时,有一个注意点:当属性是函数时,需要做特殊处理。比如,user 的_password属性虽然是受到保护的,但是我们依然想要通过某个方法来访问到它:
js1let user = { 2 name: 'qiuyanxi', 3 age: '20', 4 _password: '***', 5 getPassword: function () { 6 return this._password 7 }, 8}
如果此时代码依然是这样的:
js1user = new Proxy(user, { 2 get(target, prop) { 3 if (prop.startsWith('_')) { 4 throw new Error(`you can not visit ${prop} `) 5 } 6 return target[prop] 7 }, 8})
那当访问 user.getPassword()时,此时 this 为代理对象 ,而不是原对象。this._password会触发 get拦截器,我们会得到error。
解决的方法是将原对象绑定上去:
diff1user = new Proxy(user, { 2 get(target, prop) { 3 if (prop.startsWith("_")) { 4 throw new Error(`you can not visit ${prop} `); 5 } 6+ if(typeof target[prop]==='function'){return target[prop].bind(target)} 7 return target[prop]; 8 } 9});
这样一来,this的指向就绑定在原对象上面,对this.getPassword的调用不会触发捕捉器。
这种方案通常可行,但是并不理想,因为一个方法可能将未被代理的对象传递到任何地方,这时候我们就会陷入困惑,原始对象和被代理对象会变得模糊不清。同时,一个对象可能被代理多次,并且如果我们将未包装的对象传递给方法,则可能会产生意想不到的后果。
has 捕捉器
使用 in操作符可以检查属性是否存在于对象当中,如果想要拦截,则需要使用 has 捕捉器。
has 捕捉器会拦截 in 调用。
has(target,property)
- Target ——目标对象
- property —— 属性名
js1let range = { 2 start: 1, 3 end: 10, 4} 5 6range = new Proxy(range, { 7 has(target, prop) { 8 return prop >= target.start && prop <= target.end 9 }, 10}) 11 12alert(5 in range) // true 13alert(50 in range) // false
上面的代码是通过in操作符来检查一个数字是否在range范围内。
apply 包装函数
apply(target, thisArg, args) 捕捉器可以捕捉到函数的调用情况。
假设现在有一个函数是这样的:
js1function sayHi(args) { 2 console.log(`${args} ${this.name} `) 3} 4sayHi.call({ name: 'qiuyanxi' }, 'hi') // hi qiuyanxi
此时sayHi被调用后 this 为{name:'qiuyanxi'},args为hi。如果我们希望能够使用同样的方式,但是缺修改掉 this,使其变成{name:'Leo'}应该怎么做?
使用apply可以拦截函数调用,并且对其做一些修改:
js1function sayHi(args) { 2 console.log(`${args} ${this.name} `) 3} 4sayHi = new Proxy(sayHi, { 5 apply(target, thisArg, args) { 6 console.log('thisArg', thisArg) // { name: "qiuyanxi" } 7 console.log('args', args) // ['hi'] 8 target.call({ name: 'Leo' }, args) 9 }, 10}) 11sayHi.call({ name: 'qiuyanxi' }, 'hi') // hi Leo
使用apply捕捉器,我们就可以改变函数调用时的this指向。
下面还有一个例子,我们写一个 delay 函数,让传入的函数每次调用时,都延迟 3s
js1function delay(f, ms) { 2 return function () { 3 return setTimeout(() => { 4 f.apply(this, arguments) 5 }, ms) 6 } 7} 8function sayHi(user) { 9 console.log(`Hi ${user}`) 10} 11console.log(sayHi.length) // 1 12sayHi = delay(sayHi, 3000) 13console.log(sayHi.length) // 0
通过这种包装后,虽然sayHi('qiuyanxi')能够正常调用,但是有了这么一层包装器后,sayHi的一些原始属性就消失了。
那么我们可以使用 proxy来做包装
js1function delay(f, ms) { 2 return new Proxy(f, { 3 apply(target, thisArg, args) { 4 setTimeout(() => { 5 target.apply(thisArg, args) 6 }, ms) 7 }, 8 }) 9} 10 11function sayHi(user) { 12 console.log(`hi ${user}`) 13} 14 15sayHi = delay(sayHi, 3000) 16sayHi.length // 1 17sayHi('qiuyanxi') // hi qiuyanxi
现在代理上的所有操作都会转发到原始函数上。于是sayHi.length也是正确的结果。
Reflect
Reflect是一个内建对象,它相当于内部方法的简单包装,允许我们显示调用[[Get]]和[[Set]]等内部方法。
比如,当使用obj[prop]读取属性时,其实是调用了[[Get]]这个内部方法。我们也可以使用Reflect.get(obj,prop)来直接调用这个内部方法而不使用obj[prop]读取。
js1let user = { name: 'qiuyanxi' } 2Reflect.get(user, 'name') // qiuyanxi
同时也可以不使用obj[prop]=xxx这样的方式设置属性,而改用Reflect.set(obj,prop,value)写入属性
js1let user = {} 2Reflect.set(user, 'name', 'qiuyanxi') // {name: "qiuyanxi"} 3console.log(user)
以下是执行相同操作和 Reflect 调用的示例:
| 操作 | Reflect 调用 | 内部方法 |
|---|---|---|
obj[prop] | Reflect.get(obj, prop) | [[Get]] |
obj[prop] = value | Reflect.set(obj, prop, value) | [[Set]] |
delete obj[prop] | Reflect.deleteProperty(obj, prop) | [[Delete]] |
new F(value) | Reflect.construct(F, value) | [[Construct]] |
| … | … | … |
Reflect允许我们将操作符(new、delete)作为函数(Reflect.construct、Reflect.deleteProperty)来调用。
对于每个可被 Proxy捕获的内部方法,在Reflect中都有一个对应的方法,其名称和参数跟Proxy捕捉器相同。
当使用Proxy时,我们可以结合Reflect来将操作转发给原始对象。
js1let user = { name: 'qiuyanxi' } 2user = new Proxy(user, { 3 get(target, prop) { 4 alert(`get ${prop}`) 5 return Reflect.get(target, prop) 6 }, 7 set(target, prop, value, receiver) { 8 alert(`set ${prop}=${value}`) 9 return Reflect.set(target, prop, value) 10 }, 11}) 12console.log(user.name) 13user.name = 'Leo'
- Reflect.get 可以读取一个对象属性
- Reflect.set 写入一个对象的属性,如果成功则返回
true,否则则返回false
这样,一切都很简单:如果一个捕捉器想要将调用转发给对象,则只需使用相同的参数调用 Reflect.<method> 就足够了。
虽然不使用Reflect同样也可以,但是建议配合Proxy使用。
下面是一个体现使用Reflect的好处的简单例子:
js1let user = { 2 _name: 'qiuyanxi', 3 get name() { 4 return this._name 5 }, 6} 7let proxyUser = new Proxy(user, { 8 get(target, prop, receiver) { 9 return target[prop] // * 10 }, 11}) 12let admin = Object.create(proxyUser) 13admin._name = 'admin' 14// 期望得到输出 admin 15console.log(admin.name) // qiuyanxi
在第*行,我们直接用target[prop]的方式来返回目标对象的属性值。
读取 admin.name 应该返回 "admin",而不是 "qiuyanxi"!
如果我们移除代理,那么一切都会按预期进行。
问题实际上出在代理中,在 (*) 行。
-
当我们读取
admin.name时,由于admin对象自身没有对应的的属性,搜索将转到其原型。 -
原型是
userProxy。 -
从代理读取
name属性时,get捕捉器会被触发,并从原始对象返回target[prop]属性,在(*)行。当调用
target[prop]时,若prop是一个 getter,它将在this=target上下文中运行其代码。因此,结果是来自原始对象target的this._name,即来自user。
为了解决这种情况,我们需要 get 捕捉器的第三个参数 receiver。它保证将正确的 this 传递给 getter。在我们的例子中是 admin。
如何把上下文传递给 getter?对于一个常规函数,我们可以使用 call/apply,但这是一个 getter,它不能“被调用”,只能被访问。
所以这时候我们需要使用Reflect了。
js1let user = { 2 _name: 'qiuyanxi', 3 get name() { 4 return this._name 5 }, 6} 7let userProxy = new Proxy(user, { 8 get(...rest) { 9 return Reflect.get(...rest) 10 }, 11}) 12let admin = Object.create(userProxy) 13admin._name = 'admin' 14console.log(admin.name) // 得到正确的答案了:admin
现在 receiver 保留了对正确 this 的引用(即 admin),该引用是在 (*) 行中被通过 Reflect.get 传递给 getter 的。
Reflect 调用的命名与捕捉器的命名完全相同,并且接受相同的参数。它们是以这种方式专门设计的。
因此,return Reflect... 提供了一个安全的方式,可以轻松地转发操作,并确保我们不会忘记与此相关的任何内容。
Proxy 的局限
Proxy可以在底层修改或者调整现有对象的某些行为,但并不是全部。
-
内建对象:内部插槽(Internal slot)
许多内建对象,例如
Map,Set,Date,Promise等,都使用了所谓的“内部插槽”。它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,
Map将项目(item)存储在[[MapData]]中。内建方法可以直接访问它们,而不通过[[Get]]/[[Set]]内部方法。所以Proxy无法拦截它们。类似这样的内建对象被代理后,由于代理对象没有这些内部插槽,因此内建方法会调用失败。
js1let map = new Map() 2 3let proxy = new Proxy(map, {}) 4 5proxy.set('test', 1) // Error在内部,一个
Map将所有数据都存在[[MapData]]内部插槽中。代理对象没有这样的插槽。内建方法Map.prototype.set方法试图访问内部属性this.[[MapData]],但由于this=proxy,在proxy中无法找到它,因此上面的代码会失败。这儿有一种解决方法:
js1let map = new Map() 2 3let proxy = new Proxy(map, { 4 get(target, prop, receiver) { 5 let value = Reflect.get(...arguments) 6 return typeof value == 'function' ? value.bind(target) : value 7 }, 8}) 9 10proxy.set('test', 1) 11alert(proxy.get('test')) // 1(工作了!)现在它正常工作了,因为
get捕捉器将函数属性(例如map.set)绑定到了目标对象(map)本身。与前面的示例不同,
proxy.set(...)内部this的值并不是proxy,而是原始的map。因此,当set捕捉器的内部实现尝试访问this.[[MapData]]内部插槽时,它会成功。 -
私有字段
类的私有字段也会发生类似的情况。
例如,
getName()方法访问私有的#name属性,并在代理后中断(break):js1class User { 2 #name = 'Guest' 3 4 getName() { 5 return this.#name 6 } 7} 8 9let user = new User() 10 11user = new Proxy(user, {}) 12 13alert(user.getName()) // Error原因是私有字段是通过内部插槽实现的。JavaScript 在访问它们时不使用
[[Get]]/[[Set]]。在调用
getName()时,this的值是代理后的user,它没有带有私有字段的插槽。再次,带有
bind方法的解决方案使它恢复正常:js1class User { 2 #name = 'Guest' 3 4 getName() { 5 return this.#name 6 } 7} 8 9let user = new User() 10 11user = new Proxy(user, { 12 get(target, prop, receiver) { 13 let value = Reflect.get(...arguments) 14 return typeof value == 'function' ? value.bind(target) : value 15 }, 16}) 17 18alert(user.getName()) // Guest使用
bind的解决方案也有缺点:它将原始对象暴露给该方法,可能使其进一步传递并破坏其他代理功能。 -
Proxy != target
代理和原始对象是不同的对象。
如果我们使用原始对象作为键,然后对其进行代理,之后却无法找到代理了:
js1let allUsers = new Set() 2 3class User { 4 constructor(name) { 5 this.name = name 6 allUsers.add(this) 7 } 8} 9 10let user = new User('John') 11 12alert(allUsers.has(user)) // true 13 14user = new Proxy(user, {}) 15 16alert(allUsers.has(user)) // false如我们所见,进行代理后,我们在
allUsers中找不到user,因为代理是一个不同的对象。 -
无法拦截===相等性检查
Proxy 可以拦截许多操作符,例如
new(使用construct),in(使用has),delete(使用deleteProperty)等。但是没有办法拦截对于对象的严格相等性检查。一个对象只严格等于其自身,没有其他值。
可撤销的 Proxy
一个可撤销的代理是可以被禁用的代理。
比如,我们现在想关闭对资源的访问。我们可以包装成一个可撤销的代理,没有任何捕捉器。这样的代理会将操作转发给对象,并且我们可以随时禁用。
js1let { proxy, revoke } = Proxy.revocable(target, handler)
第一步,定义原始对象
js1let user = { name: 'qiuyanxi' }
第二步,包装成可撤销的代理
js1let { proxy, revoke } = Proxy.revocable(user, {})
第三步,想要撤销时调用revoke
js1proxy.name // 'qiuyanxi' 2revoke() // 撤销代理 3proxy.name // Error
对 revoke() 的调用会从代理中删除对目标对象的所有内部引用,因此它们之间再无连接。
由于 proxy 和 revoke 是分开的,因此我们可以传递 proxy,而不传递 revoke。
如果我们希望能保存revoke,有两种方式:
-
保存在
proxy中使用
proxy.revoke = revoke将revoke绑定到proxy中 -
创建一个
WeakMap,用proxy作为键,revoke作为值js1let revokes = new WeakMap() 2 3let object = { 4 data: 'Valuable data', 5} 6 7let { proxy, revoke } = Proxy.revocable(object, {}) 8 9revokes.set(proxy, revoke) 10 11// ...我们代码中的其他位置... 12revoke = revokes.get(proxy) 13revoke() 14 15alert(proxy.data) // Error(revoked)此处我们使用
WeakMap而不是Map,因为它不会阻止垃圾回收。如果一个代理对象变得“不可访问”(例如,没有变量再引用它),则WeakMap允许将其与它的revoke一起从内存中清除,因为我们不再需要它了。
Proxy 在开发中的应用
-
访问 array[-1]
JavaScript 不支持通过负值索引来访问数组元素,但是很多语言都实现了:
js1let array = [1, 2, 3] 2array[-1] // 3 3array[-2] // 2 4array[-3] // 1我们也可以通过代理来实现这个功能:
js1let array = [1, 2, 3] 2 3array = new Proxy(array, { 4 get(target, prop, receiver) { 5 if (parseInt(prop) < 0) { 6 prop = target.length + parseInt(prop) 7 } 8 console.log(prop) 9 10 return Reflect.get(target, prop, receiver) 11 }, 12}) 13 14alert(array[-1]) // 3 15alert(array[-2]) // 2 -
实现可观察的 Observer
创建一个
makeObservable(target),这个函数可以返回一个代理使得对象可以被观察js1let user = {} 2user = makeObservable(user) 3 4user.observe((key, value) => { 5 alert(`SET ${key}=${value}`) 6}) 7user.observe((key, value) => { 8 console.log(`SET ${key}=${value} `) 9}) 10 11user.name = 'John' // alerts: SET name=John log: SET name=Johnjs1function makeObservable(target) { 2 let observes = [] 3 target.observe = function (observeFn) { 4 observes.push(observeFn) 5 } 6 return new Proxy(target, { 7 set(target, prop, value, receiver) { 8 for (let observe of observes) { 9 observe(prop, value) 10 } 11 return Reflect.set(target, prop, value, receiver) 12 }, 13 }) 14}
总结
Proxy是对象的包装器,将代理上的操作转发到对象,并且能够捕获到底层的一些操作。
它可以包装任何类型的对象,包括类和函数。语法为:
js1let proxy = new Proxy(target, { 2 /* trap */ 3})
我们可以捕获:
- 读取(get)
- 写入(set)
- 删除(deleteProperty)
- 函数调用(apply)
- new 操作(construct)
- ...
这使我们能够创建“虚拟”属性和方法,实现默认值,可观察对象,函数装饰器等。
Reflect是用来补充Proxy的,每个proxy捕捉器都有相同参数的Reflect调用,我们应该用它将调用转发给目标对象。
Proxy 的局限:
- 内建对象具有“内部插槽”,对这些对象的访问无法被代理。请参阅上文中的解决方法。
- 私有类字段的问题同“内部插槽”,因为它们也是用内部插槽实现的。因此,也必须用目标对象作为 this
- 对象的严格性检查
===无法被拦截 - 性能:proxy 的属性拦截使得最简单的代理访问属性所需的时间都会延长几倍。但大多数情况下都是可接受的。