前言
首先在展开代理(Proxy)和反射(Reflect)两个 API 之前,先引入一个概念 ———— 元编程(Metaprogramming)。
元编程是一种编程技术,其中计算机程序能够将其他程序视为其数据。这意味着一个程序可以被设计为读取、生成、分析或转换其他程序,甚至在运行时修改自身。 ———— 【维基百科 Metaprogramming】
简而概之,就是说对于编程的再编程。一个机器 A(即程序)生产产品,一个机器 B(即程序)能够生产或者优化机器 A,那么这个机器 B 就是元编程。
元编程分类
- 对程序进行结构访问(只读权限),如 Object.*等 API(Object.keys)访问
- 对程序进行结构更改,如通过属性访问后,使用赋值运算符或 delete API 等修改
- 重新定义默认的语言行为,如本文要展开的 Proxy API
因此,Proxy 就是为开发者提供了一种程序拦截并拓展额外能力的操作,即修改了原先的语义。
而 Reflect 则是为开发者在处理程序时提供更好的原始行为调用方法,无需开发者手动重建。
对象的访问和控制
先来看看 Proxy 还未面世之前,JS 都有哪些对象控制方式
getter & setter
这两个方法是用于对象属性的访问和控制
- 通过对象字面量定义
const person = {
name: 'dilomen',
get getName() {
console.log('getter');
return this.name;
},
set setName(value) {
console.log('setter');
this.name = value;
},
};
- 通过 ES6 的 class 定义
class Person {
constructor() {
this.name = 'dilomen';
}
get getName() {
console.log('getter');
return this.name;
}
set setName(value) {
console.log('setter');
this.name = value;
}
}
const person = new Person();
person.name = 'Alice';
console.log(person.getName);
- 通过 Object.defineProperty 定义
通过 Object.defineProperty 创建的 get/set 方法,和当前函数的私有变量出于同一个作用域,所以 get/set 方法分别创建了含有私有变量的闭包,因此可以访问,但是也要注意内存泄露问题。
function Person() {
let _name = 'dilomen';
Object.defineProperty(this, 'name', {
get() {
console.log(' defineProperty getter');
return _name;
},
set(value) {
console.log('defineProperty setter');
_name = value;
},
});
}
const person = new Person();
person.name = 'Alice';
console.log(person);
Proxy
主角登场~
如果说 getter/setter 是对单个属性进行控制,那么 Proxy 就是对整个对象的所有交互的控制!包括已有的属性,和未来新增的属性等等。
/**
* target 要控制的对象
* handler 捕获器对象
*/
const proxy = new Proxy(target, handler);
handler 具体的对象属性如上,具体使用可以查阅 MDN,这边就不在展开了
可撤销代理
当我们希望在处理某个逻辑后,需要对对象的代理,那么就必须使用以下方式构建 proxy
const person = {
name: 'dilomen',
};
const handler = {
get() {
return 'Alice';
},
};
// 使用Proxy.revocable可以构建一个可撤销代理
const { proxy, revoke } = Proxy.revocable(person, handler);
// 通过暴露出的revoke方法可以取消掉代理,并且该操作不可逆,即取消后,代理就永远不存在了
revoke();
console.log(proxy.name); // TypeError
原型链
原型链是 JS 实现继承的一种方式,这边主要是了解这个知识就可 ———— 当访问一个对象的自身属性不存在时,那么就会一层层往原型链上寻找
- 那么如果 proxy 代理是原型链中的一个节点会如何呢?
let target = {};
let thing = Object.create(
new Proxy(target, {
get(trapTarget, key, receiver) {
throw new Error('属性不存在');
},
})
);
thing.name = 'thing';
console.log(thing.name); // "thing"
thing.data; // Error: 属性不存在
当代理被作为原型时,只有当默认操作执行到原型上时才会调用代理捕获器。可以从上面的代码看出,当获取 name 属性时,是 thing 自有属性,所以没有走到原型上,而 data 则原型链查找触发到了代理。
当代理被作为原型时,trapTarget 是原型对象,receiver 是实例对象。按照上面的代码,trapTarget 就是 target,receiver 就是 thing,所以可以访问代理的原始目标和要操作的对象。
- 类(函数)继承原型代理,其原型不是一个代理
function NoSuchProperty() {}
NoSuchProperty.prototype = new Proxy(
{},
{
get(trapTarget, key, receiver) {
throw new Error('属性不存在');
},
}
);
class Person extends NoSuchProperty {
constructor(name) {
super();
this.name = name;
}
}
let person = new Person('dilomen');
const personProto = Object.getPrototypeOf(person);
console.log(personProto === NoSuchProperty.prototype); // Person原型不是一个代理,但是继承了NoSuchProperty的代理
console.log(Object.getPrototypeOf(personProto) === NoSuchProperty.prototype);
Reflect
Reflect 反射 API 是一个提供了各种拦截 JS 操作静态方法的内置对象
-
一些和对象 API 对应的方法
- Object API 更多的是通用程序的应用,而 Reflect API 更多用于细粒度的对象控制和操作
-
状态标记:返回结果为操作是否执行成功。主要是以下方法:
-
Reflect.defineProperty()
-
Reflect.preventExtensions()
-
Reflect.setPrototypeOf()
-
Reflect.set()
-
Reflect.deleteProperty()
const o = {}; try { Object.defineProperty(o, 'foo', 'bar'); console.log('success'); } catch(e) { console.log('failure'); } // 返回的是布尔值 if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) { console.log('success'); } else { console.log('failure'); }
-
-
替换操作符
方法 操作符 Reflect.get() 对象属性访问操作符 Reflect.set() =赋值操作符 Reflect.has() in 操作符或 with() Reflect.deleteProperty() delete 操作符 Reflect.construct() new 操作符
Proxy + Reflect
Reflect 的静态对象和 handler 是完全对应的,所以可以直接使用以下方式,等于创建了一个所有捕获器都转发到反射 API 的空代理
new Proxy(target, Reflect);
也可以对单个或几个捕获器进行定义
new Proxy(target, {
get(target, propertyKey) {
report(propertyKey + '属性被调用了');
return Reflect.get(target, propertyKey);
},
});
使用场景
- 不确定是哪个或哪些属性
- 涉及的属性比较多
- 存在新增当前不存在的属性
添加日志,跟踪属性
new Proxy(target, {
get: (target, propertyKey) {
report(propertyKey + '属性被访问了');
return Reflect.get(target, propertyKey);
},
});
多个属性针对性处理
const properties = ['xxx', 'yyy', 'zzz'];
new Proxy(target, {
set: (target, propertyKey, value) {
if (properties.includes(propertyKey)) {
// do somethings
}
return Reflect.set(target, propertyKey, value);
},
});
拓展语言能力
如让 js 支持负数组索引
new Proxy(target, {
get: (target, index) {
return target[index < 0 ? target.length + index : index]
},
set: (target, index, value) {
// 同
}
})
数据绑定&可观察对象
参考 Vue3 的实现
性能
虽然 Proxy 的功能很强大,但是性能也是一个不容忽视的问题
从下面的程序可看出,当对 proxy 有大量控制操作时,将会出现严重的性能问题
const obj = {
num: 1,
};
function execute(obj) {
for (let i = 0; i < 1000000; i++) {
obj.num = i;
}
}
const proxy = new Proxy(obj, {});
console.time('execute');
execute(obj); // execute: 2.2158203125 ms
console.timeEnd('execute');
console.time('proxy execute');
execute(proxy); // proxy execute: 323.283935546875 ms
console.timeEnd('proxy execute');
参考资料
- Metaprogramming with proxies
- javascript.info
- 《JavaScript 高级程序设计》(第 4 版)
- 《JavaScript 忍者秘籍》(第 2 版)
- 《深入理解 ES6》