代理和反射是ES6新出的特性,主要为开发者提供了拦截并向基本操作嵌入额外行为的能力。即可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。
一、代理(Proxy)
proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
代理通过 Proxy 构造函数创建,该构造函数接收两个参数:目标对象(target)和处理程序对象(handler)。缺少任何一个参数都会抛出TypeError。具体语法如下:
const p = new Proxy(target, handler)
target:要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组、函数、甚至另外一个代理)。handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了执行各种操作时代理p的行为。
1.1 代理简介
(1)定义捕获器
代理的主要功能在于可以定义捕获器(trap)。捕获器就是处理程序对象中定义的“基本操作的拦截器”。
每个处理程序对象可以包括0个或多个捕获器,每个捕获器对应一种基本操作。不同的捕获器有不同的参数。
每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。
如下代码中,触发get操作时,代理对象会触发get()捕获器。
注意:只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
const target = {
name: 'haha'
};
const handler = {
get(){
return 'handler';
}
}
const proxy = new Proxy(target, handler);
console.log(target.name); // haha
console.log(proxy.name); // handler
此外,目标对象和代理对象访问的属性是同一个值,如下代码:
const target = {
name: 'haha'
};
const handler = {
get(){
return 'handler';
}
}
const proxy = new Proxy(target, handler);
proxy.name = 'ab';
console.log(target.name); // ab
console.log(proxy.name); // handler
捕获处理程序也必须遵循 “捕获器不变式”,例如:目标对象上的某个属性不可配置,捕获器返回一个与该属性不同的值时就会抛出 TypeError。
const target = {};
Object.defineProperty(target, 'name', {
value: 12,
configurable: false,
writable: false
})
const handler = {
get(){
return 'handler';
}
}
const proxy = new Proxy(target, handler);
console.log(proxy.name);
// TypeError: 'get' on proxy: property 'name' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value
(2)可撤销代理
在某些场景下,可能需要终端代理对象与目标对象之间的联系。而使用 new Proxy 创建的普通代理对象,这种联系在代理对象的生命周期内会一直存在。
对此,Proxy 提供了 revocable() 方法,支持撤销代理对象与目标对象之间的关联。
具体使用如下:
const target = {
name: 'haha'
};
const handler = {
get(){
return 'handler';
}
}
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.name);
// 撤销代理
revoke();
console.log(proxy.name); // TypeError: Cannot perform 'get' on a proxy that has been revoked
该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke},其中:
proxy:表示新生成的代理对象本身,和用一般方式new Proxy(target, handler)创建的代理对象没什么不同,只是它可以被撤销掉。revoke:撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。
(3)代理另一个代理
代理也可以去代理另外一个代理对象,这样可以在目标对象之上构建多层拦截。
const target = {
name: 'haha'
};
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy');
return Reflect.get(...arguments)
}
})
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy');
return Reflect.get(...arguments)
}
})
console.log(secondProxy.name);
// second proxy
// first proxy
// haha
1.2 处理函数(handler)
代理可以捕获13种不同的基本操作,下面将一一介绍。
(1)get()
handler.get()方法用于拦截对象的读取属性操作。
var p = new Proxy(target, {
get: function(target, property, receiver) {
}
});
- 参数:
target:目标对象property:被获取的属性名receiver:Proxy 或者 继承 Proxy 的对象
- 返回值:可以返回任何值
- 拦截的操作:
- 访问属性:
proxy.property或proxy[property] - 访问原型链上的属性:
Object.create(proxy)[property](create函数用于创建一个新对象,且以当前对象为原型) Reflect.get(proxy, property, receiver)
- 访问属性:
- 捕获不变式:
- 如果
target.property不可写且不可配置(即:writable 或 configurable 为false),则处理程序返回的值必须与target.property匹配 - 如果
target.property不可配置且[[get]]特性为undefined,处理程序的返回值必须是undefined
- 如果
参数如下所示:
const target = {
name: 'haha'
};
const proxy = new Proxy(target, {
get(target, property, receiver) {
console.log(target); // { name: 'haha' }
console.log(property); // name
console.log(receiver); // { name: 'haha' }
return 'proxy';
}
})
拦截的操作如下所示:
const target = {
name: 'haha'
};
const proxy = new Proxy(target, {
get(target, property, receiver) {
return 'proxy';
}
})
console.log(proxy.name); // proxy
console.log(proxy['name']); // proxy
console.log(Object.create(proxy)['name']); // proxy
(2)set()
handler.set()方法是设置属性值操作的捕获器。
const p = new Proxy(target, {
set: function(target, property, value, receiver) {
}
});
- 参数:
target:目标对象property:将被设置的属性名 或 Symbolvalue:要赋给属性的值receiver:接收最初赋值的对象(通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)
- 拦截的操作:
- 指定属性值:
proxy[property] = value或proxy.property = value - 指定继承者的属性值:
Object.create(proxy)[property] = value Reflext.set()
- 指定属性值:
- 返回值:
- true:表示属性设置成功
- false:表示属性设置失败。如果是在严格模式下,返回false会抛出一个 TypeError 异常
- 捕获不变式:
- 如果
target.property不可写且不可配置(即:writable 或 configurable 为false),则不能修改目标属性的值 - 如果
target.property不可配置且[[set]]特性为undefined,则不能修改目标属性的值 - 在严格模式下,处理程序中返回false会抛出TypeError
- 如果
const target = {
name: 'haha'
};
const proxy = new Proxy(target, {
set(target, property, value, receiver) {
target[property] = value;
console.log(target, property, value); // { name: 'lala' } name lala
return true;
}
})
proxy.name = 'lala';
对于 receiver 参数:假设有一段代码执行
obj.name = "jen",obj不是一个 proxy,且自身不含name属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的set()处理器会被调用,而此时,obj会作为 receiver 参数传进来。
(3)has()
handler.has() 方法是针对 in 操作符的代理方法
in:判断指定的属性是否在指定的对象或其原型链上
var p = new Proxy(target, {
has: function(target, prop) {
}
});
- 参数:
target:目标对象property:需要检查是否存在的属性
- 返回值:
- Boolean值,表示是否存在
- 拦截的操作:
- 属性查询:
property in proxy - 继承属性查询:
property in Objext.create(proxy) - with检查:
with(proxy) {{property};} Reflect.has()
- 属性查询:
- 捕获器不变式:
- 如果 target.property 存在且不可配置,则处理程序必须返回 true
- 如果 target.property 存在且目标对象不可扩展,则处理程序必须返回 true
const target = {name: 'haha'}
const proxy = new Proxy(target, {
has(target, property) {
console.log('has');
return property in target;
}
})
'name' in proxy; // has
(4)defineProperty()
handler.defineProperty()用于拦截对对象的 Object.defineProperty() 操作。
var p = new Proxy(target, {
defineProperty: function(target, property, descriptor) {
}
});
- 参数:
target:目标对象property:待检索其描述的属性名descriptor:待定义或修改的属性的描述符。即:enumerable、configurable、writable、value、get 和 set
- 返回值:该方法必须返回一个布尔值,表示定义该属性的操作成功与否。
- 拦截的操作:
Object.defineProperty(proxy, property, descriptor)Reflect.defineProperty(proxy, property, descriptor)
- 捕获不变式:
- 如果目标对象不可扩展,则无法定义属性
- 如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性
- 如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性
- 在严格模式下,
false作为handler.defineProperty方法的返回值的话将会抛出TypeError异常。
const target = {name: 'haha'}
const proxy = new Proxy(target, {
defineProperty(target, property, descriptor) {
console.log('defineProperty');
return true;
}
})
Object.defineProperty(proxy, 'age', {value: 1})
(5)getOwnPropertyDescriptor()
handler.getOwnPropertyDescriptor()用于拦截对对象的 Object.getOwnPropertyDescriptor() 操作。
var p = new Proxy(target, {
getOwnPropertyDescriptor: function(target, prop) {
}
});
- 参数:
target:目标对象property:待获取其描述的属性名
- 返回值:该方法必须返回一个对象,或者该属性不存在的时候返回
undefined。 - 拦截的操作:
Object.getOwnPropertyDescriptor(proxy, property)Reflect.getOwnPropertyDescriptor(proxy, property)
- 捕获不变式:
- 如果自有的 target.property 存在且不可配置,则必须返回一个表示该属性存在的对象
- 如果自有的 target.property 存在且可配置,则必须返回一个表示该属性可配置的对象
- 如果自有的 target.property 存在且 target 不可扩展,则必须返回一个表示该属性存在的对象
- 如果自有的 target.property 不存在且 target 不可扩展,则必须返回
undefined表示该属性不存在 - 如果自有的 target.property 不存在,则不能返回表示该属性可配置的对象
(6)deleteProperty()
handler.deleteProperty方法用于拦截对对象属性 delete 操作。
var p = new Proxy(target, {
deleteProperty: function(target, property) {
}
});
- 参数:
target:目标对象property:待删除的属性名
- 返回值:该方法必须返回一个Boolean类型的值,表示该属性是否被成功删除。
- 拦截的操作:
- 删除属性:
delete proxy.property或delete proxy[property] Reflect.deleteProperty(proxy, property)
- 删除属性:
- 捕获不变式:
- 如果自有的 target.property 存在且不可配置,则不能删除这个属性
(7)ownKeys()
handler.ownKeys()方法用于拦截获取键值的一些操作。
var p = new Proxy(target, {
ownKeys: function(target) {
}
});
- 参数:
target:目标对象
- 返回值:该方法必须返回一个包含字符串或Symbol的可枚举对象。
- 拦截的操作:
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()Reflect.ownKeys()
- 捕获不变式:
- 返回的可枚举对象中必须包含 target 所有 不可配置、自有的属性
- 如果目标对象不可扩展,那么结果列表必须包含目标对象的所有自有(own)属性的 key,不能有其它值。
(8)getPrototypeOf()
handler.getPrototypeOf()方法在读取代理对象的原型时,该方法会被调用。
const p = new Proxy(obj, {
getPrototypeOf(target) {
...
}
});
- 参数:
target:目标对象
- 返回值:该方法必须返回一个对象 或者
null - 拦截的操作:
Object.getPrototypeOf(proxy)proxy.__proto__Object.prototype.isPrototypeOf(proxy)proxy instanceof ObjectReflect.getPrototypeOf(proxy)
- 捕获不变式:
- 如果 target 不可扩展,则
Object.getPrototypeOf(proxy)唯一有效的返回值就是 Object。
- 如果 target 不可扩展,则
(9)setPrototypeOf()
handler.setPrototypeOf()方法主要用来拦截 Object.setPrototypeOf()。
var p = new Proxy(target, {
setPrototypeOf: function(target, prototype) {
}
});
- 参数:
target:目标对象prototype:对象新原型 或 null
- 返回值:该方法必须返回布尔值,表示原型修改是否成功。返回非布尔值会被转型为布尔值。
- 拦截的操作:
Object.setPrototypeOf(proxy)Reflect.setPrototypeOf(proxy)
- 捕获不变式:
- 如果 target 不可扩展,则唯一有效的参数是
Object.getPrototypeOf(target)的返回值。
- 如果 target 不可扩展,则唯一有效的参数是
(10)isExtensible()
handler.isExtensible()方法主要用来拦截 Object.isExtensible()。
var p = new Proxy(target, {
isExtensible: function(target) {
}
});
- 参数:
target:目标对象
- 返回值:该方法必须返回布尔值,表示 target 是否可以扩展。返回非布尔值会被转型为布尔值。
- 拦截的操作:
Object.isExtensible(proxy)Reflect.isExtensible(proxy)
- 捕获不变式:
- 如果 target 可扩展,则处理程序必须返回 true。
- 如果 target 不可扩展,则处理程序必须返回 false
Object.isExtensible(proxy)必须同Object.isExtensible(target)返回相同值。
(11)preventExtensions()
handler.preventExtensions()方法主要用来拦截 Object.preventExtensions()。
var p = new Proxy(target, {
preventExtensions: function(target) {
}
});
- 参数:
target:目标对象
- 返回值:该方法必须返回布尔值,表示 target 是否已经不可扩展。返回非布尔值会被转型为布尔值。
- 拦截的操作:
Object.preventExtensions(proxy)Reflect.preventExtensions(proxy)
- 捕获不变式:
- 如果
Object.isExtensible(proxy)是false,则处理程序必须返回true。
- 如果
(12)apply()
handler.apply()方法主要用来拦截函数的调用
var p = new Proxy(target, {
apply: function(target, thisArg, argumentsList) {
}
});
- 参数:
target:目标对象thisArg:被调用时的上下文对象argumentList:被调用时的参数数组
- 返回值:任何值
- 拦截的操作:
proxy(...args)Function.prototype.apply()和Function.prototype.call()Reflect.apply()
- 捕获不变式:
- target 必须是一个函数对象
(13)construct()
handler.construct()方法用于拦截 new 操作符。为了使 new 操作符在生成的 Proxy 对象上生效,用于初始化代理的目标对象自身必须具有 [[Construct]] 内部方法
var p = new Proxy(target, {
construct: function(target, argumentsList, newTarget) {
}
});
- 参数:
target:目标对象argumentList:constructor 的参数列表newTarget:最初被调用的构造函数
- 返回值:任何值
- 拦截的操作:
new proxy(...args)Reflect.construct()
- 捕获不变式:
- target 必须可以用作构造函数
1.3 代理模式
使用代理可以在代码中实现一些有用的编程模式。
(1)跟踪属性访问
通过拦截 get、set、has 等操作,可以跟踪对象属性什么时候被访问、被查询。
如下代码,我们可以通过代理去追踪,那些属性被访问了或者被修改了。
const person = {
name: 'haha',
age: 12
}
const proxyPerson = new Proxy(person, {
get(target, prop) {
console.log(`get ${prop} ${target[prop]}`);
return Reflect.get(...arguments);
},
set(target, prop, value) {
console.log(`set ${prop} ${value}`);
return Reflect.set(...arguments);
}
})
proxyPerson.name; // get name haha
proxyPerson.age = 15; // set age 15
(2)隐藏属性
通过代理可以隐藏目标对象上的某些属性。
如下代码中,我们不想用户的密码被访问到,我们可以通过代理去隐藏。
const user = {
name: 'haha',
password: 123,
}
const proxyUser = new Proxy(user, {
get(target, prop) {
if (prop === 'password') {
return undefined;
}
return Reflect.get(...arguments);
},
})
console.log(proxyUser.password); // undefined
(3)属性验证
因为所有的赋值操作都会触发 set() 捕获器,所以可以根据所赋的值决定是允许赋值还是拒绝赋值。
如下代码,当某个属性只能设置为数值类型时,我们可以通过代理来限制。
const target = {
onlyNumber: 2022
}
const proxy = new Proxy(target, {
set(target, prop) {
if (typeof target[prop] === 'number') {
return false;
}
return Reflect.set(...arguments);
},
})
proxy.onlyNumber = 'asd';
console.log(proxy) // { onlyNumber: 2022 }
(4)函数与构造函数参数验证
这个与(3)的功能类似,只不过是在handler.apply() 和 handler.construct 中限制函数和构造函数的参数。
(5)数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起,这样就可以实现各种模式,从而让不同的代码互操作。
如下代码,就可以通过代理,使得每创建一个新对象,都会被记录到 userList 中
const userList = [];
class User {
constructor(name) {
this.name = name;
}
}
const proxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments);
userList.push(newUser);
return newUser;
}
})
new proxy('haha');
new proxy('lala');
console.log(userList); // [ User { name: 'haha' }, User { name: 'lala' } ]
二、反射(Reflect)
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy.handlers的方法相同。
Reflect 不是一个函数对象,是不可构造的。
Reflect 对象提供了以下静态方法(与 proxy.handler 方法命名相同):
// 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。
Reflect.apply(target, thisArg, args)
// 对构造函数进行 new 操作,相当于执行 new target(...args)。
Reflect.construct(target, args)
// 获取对象身上某个属性的值,类似于 target[name]。如果没有该属性,则返回undefined。
Reflect.get(target, name, receiver)
// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.set(target, name, value, receiver)
// Reflect.defineProperty方法基本等同于Object.defineProperty,直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,不同的是,Object.defineProperty返回此对象。而Reflect.defineProperty会返回布尔值.
Reflect.defineProperty(target, name, desc)
// 作为函数的delete操作符,相当于执行 delete target[name]。
Reflect.deleteProperty(target, name)
// 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
Reflect.has(target, name)
// 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响, Object.keys返回所有可枚举属性的字符串数组).
Reflect.ownKeys(target)
// 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性),类似于 Object.isExtensible()。返回表示给定对象是否可扩展的一个Boolean 。(Object.seal 或 Object.freeze 方法都可以标记一个对象为不可扩展。)
Reflect.isExtensible(target)
// 让一个对象变的不可扩展,也就是永远不能再添加新的属性。
Reflect.preventExtensions(target)
// 如果对象中存在该属性,如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。类似于 Object.getOwnPropertyDescriptor()。
Reflect.getOwnPropertyDescriptor(target, name)
// 返回指定对象的原型.类似于 Object.getOwnPropertyDescriptor()。
Reflect.getPrototypeOf(target)
// 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。如果 target 不是 Object ,或 prototype 既不是对象也不是 null,抛出一个 TypeError 异常。
Reflect.setPrototypeOf(target, prototype)
个人认为 Reflect 其实就是为 Proxy 的处理程序提供了一种便捷的调用原本功能的方法。例如:当我拦截 get() 操作时,我只是想跟踪哪些属性被访问,获取的值不变。如果不用Reflect的话,那就是:
get(target, prop) {
...
return target[prop]
}
使用 Reflect 的话,直接:
get() {
...
return Reflect.get(...arguments);
}
因为 Proxy 的 handler 每个方法在 Reflect 中都有对应的静态方法,所以个人认为 Reflect 就是提供了一种便捷的方式。
本文到这里就结束了,自己对代理和反射也有一些了解啦,特此记录,也希望能够分享给有需要的初学者们。文中有错误的地方还请大佬们多多指正。
[1] 《JavaScript 高级程序语言设计(第四版)》
[2] Proxy —— MDN