你不知道的JavaScript(下)
第7章 第4节 代理
7.4 代理
代理(Proxy
)是ES6新增的元编程特性;
- 代理是一种 有你创建的特殊的对象,它“封装”另一个普通对象,或者说挡在这个普通对象前面;
- 可以在代理对象上注册特殊的处理函数(即trap),代理上执行的各种操作都会调用它;
- 通过这些处理函数,除了把操作转发给原始目标/被封装对象之外,还有机会执行额外的逻辑;
注:元编程是指操作目标是程序本身的行为特性的编程;元编程的目标是利用语言自身的内省能力使代码的其余部分更具描述性、表达性和灵活性;
trap处理函数之get
:
- 当试图访问对象属性时,会被
[[Get]]
拦截;
var obj = {a:1};
var handlers = {
get(target, key, context){
// target就是Proxy包裹的对象,这里就是obj
// key是要访问的对象属性名
// context是当前操作的上下文对象(self/接收者/代理),这里就是指Proxy对象,即pobj
console.log('get 拦截');
// 这里通过 APIReflect 的 Reflect.get(...)方法完成 obj操作的转发
return Reflect.get(target,key,context);
}
};
var pobj = new Proxy(obj, handlers);
obj.a;//get,无log
pobj.a;//get, log“get 拦截”
关于Reflect函数:
- 这里使用了
APIReflect
的Reflect.get(...)
,需要注意的是,每个可用的代理trap
都有一个对应的同名Reflect
函数; - 这种映射的对称是有意的:
- 每个代理处理函数 在对称的元编程任务执行的时候进行拦截;
- 每个Reflect工具 在一个对象上执行相应的元编程任务;
- 几乎可以确定二者是同时工作的;
所有能够定义的trap处理函数:
函数 | 释义 | 访问方式 |
---|---|---|
get(…) | 通过[[Get]] ,在代理上访问一个属性 | Reflect.get(..) .属性运算符 [..]属性运算符 |
set(…) | 通过[[Set]] ,在代理上设置一个属性值 | Reflect.set(..) 赋值运算符= 对象属性的结构赋值 |
deleteProperty(…) | 通过[[Delete]] ,从代理对象上删除一个属性 | Reflect.deleteProperty(..) delete |
apply(…) 如果目标为函数 | 通过[[Call]] ,将代理作为普通函数/方法调用 | Reflect.apply(..) call(..) apply(..) (..)调用运算符 |
construct(…) 如果目标为构造函数 | 通过[[Construct]] ,将代理作为构造函数调用 | Reflect.construct(..) new |
getOwnPropertyDescriptor(…) | 通过[[GetOwnProperty]] ,从代理中提取一个属性描述符 | Object.getOwnPropertyDescriptor(..) Reflect.getOwnPropertyDescriptor(..) |
defineProperty(…) | 通过[[DefineOwnProperty]] ,在代理上设置一个属性描述符 | Object.defineProperty(..) Reflect.defineProperty(..) |
getPrototypeOf(…) | 通过[[GetPrototypeOf]] ,得到代理的[[Prototype]] | Object.getPrototypeOf(..) Reflect.getPrototypeOf(..) __proto__ Object#isPrototypeOf(..) instanceOf |
setPrototypeOf(…) | 通过[[SetPrototypeOf]] ,设置代理的[[Prototype]] | Object.setPrototypeOf(..) Relect.setPrototypeOf(..) __proto__ ` |
preventExtensions(…) | 通过[[PreventExtensions]] ,使得代理变成不可扩展的 | Object.preventExtensions(..) Reflect.preventExtensions(..) |
isExtensible(…) | 通过[[IsExtensible]] ,检测代理是否可扩展 | Object.isExtensible(..) Reflect.isExtensible(..) |
ownKeys(…) | 通过[[OwnPropertyKeys]] ,提取代理自己的属性和/ 或符号属性 | Object.keys(..) Object.getOwnPropertyNames(..) Object.getOwnSymbolPropertise(..) Reflect.ownkeys(..) JSON.stringify(..) |
enumerate(…) | 通过[[Enumerate]] ,取得代理拥有的和“继承来的”可枚举属性的迭代器 | Reflect.enumerate(..) for..in |
has(…) | 通过[[HasProperty]] ,检查代理是否拥有或者“继承了”某个属性 | Reflect.has(..) object#hasOwnProperty(..) 'prop' in obj |
上面罗列的各种trap动作,其中某些trap是由其他trap间接触发的:
- 由set,设定属性值(新增or修改),会触发getOwnPropertyDescriptor(…)和defineProperty(…);
- 自定义set函数时,在其context上可以手动调用(也可以不)它们;
var handlers = {
getOwnPropertyDescriptor(target, prop){
console.log("getOwnPropertyDescriptor");
return Object.getOwnPropertyDescriptor(target, prop);
},
defineProperty(target, prop, desc){
console.log("defineProperty");
return Object.defineProperty(target, prop, desc);
},
// set(target,key,val,context) {..}
}
// ...
proxy = new Proxy({}, handlers);
proxy.a = 2;
// getOwnPropertyDescriptor
// defineProperty
代理局限性
可以在对象上执行的很广泛的一组基本操作,都可以通过这些元编程处理函数trap;但有一些例外,如一些运算符操作对象;
可取消代理
正常的代理机制在创建后不能修改,只有代理对象存在,相应的机制也就一直存在;如果想创建一个在你想要停止它作为代理时便可以被停用的代理,就可以使用可取消代理;
可取消代理:
- 创建:
Proxy.revocable(..)
创建,这只是一个普通函数,接收两个参数:target
和handlers
; - 返回值:与
new Proxy(..)
不同,Proxy.revocable(..)
的返回值不是代理本身,而是一个对象,它有两个属性:proxy
:代理对象revode
:取消代理的函数调用
- 取消之后,任何对代理的访问(触发trap的)都会抛出TypeError;
- 使用场景:代理对象创建之后提供给其他三方使用,如果该代理被替换了,就可以通过取消操作,三方报错后,重新拉取新的代理对象;
var obj = {a:1}
var handlers = {
get(target,key,context){
// target === obj
// context === pobj
console.log("accessing",key);
return target[key];
}
}
// 解构赋值
var { proxy:pobj, revoke:prevoke } = Proxy.revocable(obj, handlers);
pobj.a;// 正常触发
prevoke();
pobj.a;// TypeError
使用代理
使用代理的优点:拦截毒系查能几乎所有行为;这意味着可以扩展对象特性;
我们看几个例子:这些例子都值得仔细品味下,事实上运用好这些技巧,可以为编程带来很多益处;
- 代理在前
// 对数组进行代理包裹:实现取值和赋值的扩展处理
// 直接与messages_proxy交流,控制对messages的访问
var messages = [];
var handlers = {
get(target,key){
if(typeof target[key] == "string"){
// 注意理解这里的正则:中括号分组中的^符号表示取反的意思,即把字符串中匹配到的 所有不是 字母数字下划线汉字的字符替换为空串
return target[key].replace(/[^\w]/g,"");
}
return target[key];
},
set(target, key, val){
// 这个if判断决定 数组只能添加 不重复、小写的字符串
if(typeof val == "string"){
val = val.toLowerCase();
if(target.indexOf(val) == -1){
target.push(val);
}
}
return true;
}
}
var messages_proxy = new Proxy(messages, handlers);
// ...
messages_proxy.push("Wello...", 42, "World!!","WORLD!!");
messages_proxy.forEach(function (item){
console.log(item);
});
// hello world
messages.forEach(function (item){
console.log(item);
});
// hello... world!!
- 代理在后
// 上边是代理与目标交流,代理控制对目标的访问;
// 接下来反转上面的模式,让目标与代理交流,目标控制对代理的访问(最简单的方式是把proxy对象放到主对象的[[Prototype]]链中)
// 直接与greeter交流,控制对catchall的访问
var handlers = {
get(target, key, context){
return function(){
context.speak(key + "!");
}
}
}
var catchall = new Proxy({},handlers);
var greeter = {
speak(who="someone"){
console.log("hello ",who);
}
}
// 设定greeter回退到catchall
Object.setPrototypeOf(greeter, catchall);
greeter.speak();// hello someone
greeter.speak("world");// hello world
// 默认的对象属性行为会检查[[Prototype]]链,catchall查看everyone属性,代理的get函数会返回一个用访问属性名(everyone)调用speak的函数
greeter.everyone();// hello everyone!
- No Such Property/Method
- 有时候希望访问或设置一个不存在的属性时,不想普通js对象那么不具有防御性;
- 我们希望预定义好一个对象的所有属性和方法,访问不存在的属性名的时候能够抛出一个错误;
// 代理在前的方案
var obj = {
a:1,
foo(){
console.log("a:",this.a);
}
}
var handlers = {
get(target, key, context){
if(Reflect.has(target, key )){
return Reflect.get(target,key,context);
}else{
throw "No Such property/method!";
}
},
set(target, key, val, context){
if(Reflect.has(target, key)){
return Reflect.set(target, key, val, context);
}else{
throw "No Such property/method!";
}
}
}
var pobj = new Proxy(obj, handlers);
pobj.a = 3;
pobj.foo();// a:3
pobj.b = 4;// Error: No Such property/method!
pobj.bar();// Error: No Such property/method!
// 代理在后的方案
var handlers = {
get(){
throw "No Such property/method!";
},
set(){
throw "No Such property/method!";
}
}
var pobj = new Proxy({},handlers);
var obj = {
a:1,
foo(){
console.log("a:",this.a);
}
}
// 设定obj回退到pobj
Object.setPrototypeOf(obj, pobj);
obj.a = 3;
obj.foo();// a:3
obj.b = 4;// Error: No Such property/method!
obj.bar();// Error: No Such property/method!
- 代理hack
[[Prototype]]
链[[Prototype]]
机制主要通道是[[Get]]
运算;当一个属性没找到时,[[Get]]
会自动把这个运算转给[[Prototype]]
对象处理;- 使用代理的 get(…) trap 可以来模拟或扩展
[[Prototype]]
机制; - 比如创建一个环状
[[Prototype]]
,或是多个[[Prototype]]
(也就是多继承);
注:
- Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
const returnedTarget = Object.assign(target, source);
- Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
const me = Object.create(person);
- Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null。
Object.setPrototypeOf(obj, prototype)
;- for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
- for…in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性。
- JSON.stringify() 方法将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。
// hack 环状[[Prototype]]
// 创建两个对象,通过[[Prototype]]连成环状(至少看起来是这样,实际上并不是一个真正的还,因为引擎会报错)
// 注:这个例子中没有用代理/转发[[Set]],所以比较简单,要完整模拟[[Prototype]],需要实现一个set(..)处理函数来搜索[[Prototype]]链寻找匹配的属性,并遵守其描述符特性;
var handlers = {
get(target, key, context){
if(Reflect.has(target, key)){
return Reflect.get(target, key, context);
}else{
// 伪环状[[Prototype]]
return Reflect.get(target[Symbol.for("[[Prototype]]")], key, context);
}
}
}
var obj1 = new Proxy({
name:"obj-1",
foo(){
console.log("foo:", this.name);
}
},handlers);
var obj2 = Object.assign(Object.create(obj1),{
name:"obj-2",
bar(){
console.log("bar:", this.name);
this.foo();
}
})
// 伪环状[[Prototype]]链接
// Symbol.for("[[Prototype]]")只是提供了一个方便的与我们正在执行的任务关联的命名钩子
obj1[Symbol.for("[[Prototype]]")] = obj2;
obj1.bar();
// bar: obj-1 <-- 通过代理伪装[[Prototype]]
// foo: obj-1 <-- this上下文依然保留着
obj2.foo();
// foo: obj-2 <-- 通过[[Prototype]]
// hack 多个[[Prototype]](也就是多继承)
var obj1 = {
name:"obj-1",
foo(){
console.log("obj1.foo:", this.name);
}
}
var obj2 = {
name:"obj-2",
foo(){
console.log("obj2.foo:", this.name);
},
bar(){
console.log("obj2.bar:", this.name);
}
}
var handlers = {
get(target, key, context){
if(Reflect.has(target, key)){
return Reflect.get(target, key, context);
}else{
// 伪装多个[[Prototype]]
for(var P of target[Symbol.for("[[Prototype]]")]){
if(Reflect.has(P, key)){
return Reflect.get(P, key, context);
}
}
}
}
}
obj3 = new Proxy({
name:"obj-3",
baz(){
this.foo();
this.bar();
}
},handlers);
// 伪装多个[[Prototype]]链接
obj3[Symbol.for("[[Prototype]]")] = [obj1, obj2];
obj3.baz();
// obj1.foo: obj-3
// obj2.bar: obj-3
代理使得很多其他威力强大的元编程任务称为可能;
7.5 Reflect API
Reflect对象是一个平凡对象(就行Math),不像其他内置原生值一样是函数/构造器;
- 持有对应于各种可控的元编程任务的静态函数;
- 这些函数一一对应着代理可以定义的处理函数方法(trap);
part1
其中一些函数看起来和Object上的同名函数类似:
Reflect.getOwnPropertyDescriptor(..)
Reflect.defineProperty(..)
Reflect.getPrototypeOf(..)
Reflect.setPrototypeOf(..)
Reflect.preventExtensions(..)
Reflect.isExtensible(..)
一般的,这些工具和Object.*
的对应工具行为方式类似;区别在于:
- 如果第一个参数不是对象,
Object.*
会视图把它转换为一个对象; - 而
Reflect.*
会抛出一个错误;
part2
还有一些工具可以访问/查看一个对象的键:
工具API | 释义 |
---|---|
Reflect.ownKeys(..) | 返回所有“拥有”的(不是“继承”的)键的列表,就像 Object.getOwnPropertyNames (..) 和Object.getOwnPropertySymbols(..) 返回的一样 |
Reflect.enumerate(..) | 返回一个产生所有(拥有的和“继承的”)可枚举的(enumerable)非符号键集合的迭 代器;本质上说,这个键的 集合和 foo…in 循环处理的那个键的集合是一样的; |
Reflect.has(..) | 实 质 上 和 in 运 算 符 一 样, 用 于 检 查 某 个 属 性 是 否 在 某 个 对 象 上 或 者 在 它 的 [[Prototype]] 链上。比如,Reflect.has(o, “foo”) 实质上就是执行 “foo” in o。 |
part3
函数调用和构造器调用可以通过使用下面这些工具手动执行,与普通的语法(比如,(…) 和 new)分开:
工具API | 释义 |
---|---|
Reflect.apply(..) | 举例来说,Reflect.apply(foo,thisObj,[42,“bar”]) 以 thisObj 作为 this 调用 foo(…) 函数,传入参数 42 和 “bar”。 |
Reflect.construct(..) | 举例来说,Reflect.construct(foo,[42,“bar”]) 实质上就是调用 new foo(42,“bar”)。 |
part4
可以使用下面这些工具来手动执行对象属性访问、设置和删除:
工具API | 释义 |
---|---|
Reflect.get(..) | 举例来说,Reflect.get(o,“foo”) 提取 o.foo。 |
Reflect.set(..) | 举例来说,Reflect.set(o,“foo”,42) 实质上就是执行 o.foo = 42。 |
Reflect.deleteProperty(..) | 举例来说,Reflect.deleteProperty(o,“foo”) 实质上就是执行 delete o.foo。 |
注:Reflect 的元编程能力提供了模拟各种语法特性的编程等价物,把之前隐藏的抽象操作暴 露出来。比如,你可以利用这些能力扩展功能和 API,以实现领域特定语言(DSL)。
属性排序(了解)
- ES6之前,对象属性的列出顺序依赖于具体实现;
- ES6,拥有属性列出顺序是由
[[OwnPropertyKeys]]
算法定义的;- 这个算法产生所有拥有的属性(字符串或符号),不管是否可枚举;
- 这个顺 序 只 对
Reflect.ownKeys(..)
( 以 及 扩 展 的Object.getOwnPropertyNames(..)
和Object. getOwnPropertySymbols(..))
有保证。
其顺序为:
- (1) 首先,按照数字上升排序,枚举所有整数索引拥有的属性;
- (2) 然后,按照创建顺序枚举其余的拥有的字符串属性名;
- (3) 最后,按照创建顺序枚举拥有的符号属性。
注:
[[Enumerate]]
算法(ES6 规范,9.1.11 节)只从目标对象和它的[[Prototype]]
链产生可枚举属性。它用于Reflect.enumerate(..)
和for..in
。可以观察到的顺序和具体 的实现相关,不由规范控制;
注:
Object.keys(..)
调用[[OwnPropertyKeys]]
算法取得拥有的所有键的列表。但 是,它会过滤掉不可枚举属性,然后把这个列表重新排序来遵循遗留的与实现相关的行 为特性,特别是JSON.stringify(..)
和for..in
。因此通过扩展,这个顺序也和Reflect. enumerate(..)
顺序相匹配。
注:换句话说,所有这 4 种机制
(Reflect.enumerate(..)
、Object.keys(..)
、for..in
和JSON. stringify(..))
都会匹配同样的与具体实现相关的排序,尽管严格上说是通过不同的路径。
小结:
- 对 于 ES6 来 说,
Reflect.ownKeys(..)
、Object.getOwnPropertyNames(..)
和Object.getOwnPropertySymbols(..)
的顺序都是可预测且可靠的,这由规范保证。所以依赖于这个顺序的代码是安全的。 Reflect.enumerate(..)
、Object.keys(..)
和for..in
(以及扩展的JSON.stringification(..)
) 还像过去一样,可观察的顺序是相同的。但是这个顺序不再必须与Reflect.ownKeys(..)
相 同。在使用它们依赖于具体实现的顺序时仍然要小心。
7.6 特性测试(了解)
由你运行的用来判断一个特性是否可用的测试;测试程序的运行环境,然后确定程序行为方式,这是一种元编程技术。