通过写 on-change 库理解 JavaScript Proxy
JavaScript 中的 Proxy 是 ES6 的新语法,可以利用这个强大的特性来优雅的解决各种问题。这篇文章里通过写一个 on-change 库从而理解 Proxy。所以 on-change 这个库是用来做什么用的?这是一个为了观察 object 或 array 改变的库。来看一个简单的例子:
const onChange = require('on-change'); const object = { foo: false, a: { b: [{ c: false }] } }; let i = 0; const logger = () => console.log('Object changed:', ++i); const watchedObject = onChange(object, logger); watchedObject.foo = true; //=> 'Object changed: 1' watchedObject.a.b[0].c = true; //=> 'Object changed: 2'
有几个需要注意的点:
- onChange 是一个接收两个参数的方法:需要观察的对象和当对象改变时运行的回调函数,最后返回改变后的对象。
- 在代码第 17 行,当定义 foo 为 true 时,回调函数 logger 被执行。
- 在代码第 20 行,当定义一个多嵌套的对象时,即便是数组,回调函数 logger 会被执行。这说明 onchange 是递归检查执行,所以修改多嵌套里的属性也会触发回调。
所以现在看看应该如何使用 JS 中 Proxy 语法来重新创建这个 on-change 工具库,首先需要学习 Proxy 的基础语法使用。
什么是 Proxy ?
思考以下这段代码
const someObject = { prop1: 'Awesome' }; console.log(someObject.prop1); // Awesome console.log(someObject.prop2); // undefined
someObject.prop1
输出 Awesome,someObject.prop2
输出 undefined 因为 prop2 这个属性不存在。假设当访问一个不存在的值时就返回其默认值,在这里访问 someObject.prop2
应该返回 噢上帝,你访问的属性不存在
而不是 undefined。怎样如何实现而无需在 someObject
修改或添加新属性?
此刻 Proxy 登场。在牛津英文词典中 proxy 这词代表的意思是代表其他人的权利。这正是 JS 中的 Proxy。Proxy 是 ES6 中的一部分,通过 Proxy 可以创建一个代理,用于替代另一个对象(目标),这个代理对目标对象进行了虚拟。Proxy 允许我们对 object 进行拦截一些操作(如设置值或删除值)的执行。当访问一个对象属性时,发生了如下图所示的事:
当使用 Proxy 时,流程发生了一些变化:
从上图可看出,Proxy 位置在 Object 与 Program 中间调解值的改变。Proxy 会检查 Object 的属性值,如果不存在那也可以返回响应。现在来看看如何在 JS 中创建,不过在此之前应该先熟悉以下术语:
- target 目标对象
- traps 陷阱,意为将要拦截的操作。例如,在对象里访问一个属性称为 get traps,设置一个属性值称为 set traps,删除一个属性值称为 deleteProperty trap。还有许多 traps 可查阅这里
- handler 处理器 一个包含所有 traps 的对象,包含它的描述信息
创建 Proxy 对象
首先要做的就是创建一个需要被观察的 object:
const originalObject = { firstName: 'Arfat', lastName: 'Salman' };
现在需要考虑什么对象操作是需要被 traps 拦截的。这里首先添加了一个 get traps,这个 trap 将会在 handler 中注册:
const handler = { get(target, property, receiver) { console.log(`GET ${property}`); return target[property]; } };
有几个需要注意的点:
- handler 是一个普通的对象
- traps 函数是 handler 的一部分。traps 的名字是固定不变且被预先定义的
- get trap 接收三个参数:target, property, receiver
- target 将接收属性的对象(即代理的目标对象)
- property 需要读取的属性的键值
- receiver 操作发生的对象(通常是代理对象)
现在我们使用 Proxy 构造函数结合 handler 和 originalObject
const proxiedObject = new Proxy(originalObject, handler);
整段代码如下
const originalObject = { firstName: 'Arfat', lastName: 'Salman' }; const handler = { get(target, property, receiver) { console.log(`GET ${property}`); return target[property]; } }; const proxiedObject = new Proxy(originalObject, handler);
如果浏览器支持 Proxy 语法,那么可以运行在 console 中,或直接使用命令行 node 运行。如果直接打印出 proxiedObject.firstName 属性那么将会输出
console.log(proxiedObject.firstName);
//=> GET firstName
//=> Arfat
现在来修改 handler 来处理不存在属性的情况
const handler = { get(target, property, receiver) { console.log(`GET ${property}`) if (property in receiver) return target[property] return '噢上帝,你访问的属性不存在' } };
当 property 属性存在 receiver 对象时则说明该属性是存在于对象里的,否则返回提示。现在当你访问 proxiedObject.thisPropertDoesNotExist 将会返回
console.log(proxiedObject.thisPropertDoesNotExist); // => GET thisPropertyDoesNotExist // => 噢上帝,你访问的属性不存在
这次将不会再返回 undefined 而是自定义的返回消息。需要注意的是,如果没有定义任何 traps,则正常的传值就像 proxy 不存在。
重新写一个 on-change 库
理解了 proxy 基础语法和如何工作后,我们将重写 on-change 这个库。正如文章开头部分所描述,onChange 是一个接收两个参数的函数:需要观察的对象和当对象改变时运行的回调函数。第一步先创建一个函数:
const onChange = (objToWatch, onChangeFunction) => {}
首先梳理下目前需要做的:希望每当 objToWatch 对象改变时都将执行 onChangeFunction。也就是说当一个属性被访问或修改,或新添加一个属性,或删除一个属性的情况。
在 onChange 函数中返回一个空 handler 的 proxy 对象。因为没有定义 traps 所以任何操作都是传值到目标对象。
const onChange = (objToWatch, onChangeFunction) => { const handler = {}; return new Proxy(objToWatch, handler); };
因为理解了 get trap 所以首先将注意力放在 当一个属性被访问或修改 这个条件上。当返回属性值时将执行 onChangeFunction,于是写出下面的代码:
const onChange = (objToWatch, onChangeFunction) => { const handler = { get(target, property, receiver) { onChangeFunction(); return target[property]; } }; return new Proxy(objToWatch, handler); };
现在将注意力放在 添加或删除一个属性 上。当设置一个属性值或者修改值时触发的是 set trap:
const onChange = (objToWatch, onChangeFunction) => { const handler = { get(target, property, receiver) { onChangeFunction(); return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { onChangeFunction(); return Reflect.set(target, property, value); } }; return new Proxy(objToWatch, handler); };
注意这里改写并使用 Reflect API(Proxy 里的 trap 也推荐使用 Reflect)。Reflect.get()
是 get trap 对应的反射方法,同时也是 set 操作的默认行为。Reflect.set()
同理。
Reflect 对象所代表的是反射接口,是给底层操作提供默认行为的方法的集合,这些操作是能够被代理重写的。每个代理 trap 都有一个对应的反射方法,每个方法都与对应的 trap 函数同名,并且接收的参数也与之一致。下表总结了这些行为:
代理 trap | 被重写的行为 | 默认行为 |
---|---|---|
get | 读取一个属性的值 | Reflect.get() |
set | 写入一个属性 | Reflect.set() |
has | in 运算符 | Reflect.has() |
deleteProperty | delete 运算符 | Reflect.deleteProperty() |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() | Reflect.defineProperty |
ownKeys | Object.keys 、Object.getOwnPropertyNames() 与 Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
construct | 使用 new 调用一个函数 | Reflect.construct() |
可以点击这里阅读了解为什么最好使用 Reflect API。通过查阅表了解到 deleteProperty trap 则是使用 Reflect.deleteProperty()
,既可添加 deleteProperty 完成最后一个触发条件 删除一个属性:
const onChange = (objToWatch, onChangeFunction) => { const handler = { get(target, property, receiver) { onChangeFunction(); return Reflect.get(target, property, receiver); }, set(target, property, value) { onChangeFunction(); return Reflect.set(target, property, value); }, deleteProperty(target, property) { onChangeFunction(); return Reflect.deleteProperty(target, property); } }; return new Proxy(objToWatch, handler); };
到此 on-change 的改写就完成的差不多了,可以通过 console 测试:
const logger = () => console.log('我被执行'); const obj = { a: 'a' }; const proxy = onChange(obj, logger); console.log(proxy.a); // 我被执行 in get trap proxy.b = 'b'; // 我被执行 in set trap delete proxy.a; // 我被执行 in deleteProperty trap
可看到控制台将输出三次的 "我被执行"。但是如果 obj 为数组且数组中包含一个对象的话,那么修改这个数组里的对象将不会触发 logger 函数。举个例子 [1, 2, {a: false}]
当设置 array[2].a = true
将不会触发 logger。
可以通过简单的方式来处理这个 bug,通过判断 Reflect.get() 的返回值,如果值为 object 则返回一个新的 proxy:
get(target, property, receiver) { onChangeFunction(); const value = Reflect.get(target, property, receiver); if (typeof value === 'object') { return new Proxy(value, handler); } return value; }
这样 onChange 便可观察 array 与 object 的变化。是不是对 proxy 认知更清晰了呢?这篇文章涉及的只是 proxy 的基础知识,更深入扩展建议阅读 understanding ecmascript 6 第十二章 proxy 篇。
原文:https://codeburst.io/understanding-javascript-proxies-by-examining-on-change-library-f252eddf76c2
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论