说道Proxy就会想到各种编程中需要使用的代理服务器,在ES6新增这样一个新对象:
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
出现了setting count,getting count,因为拦截了对象的属性访问方法。
如何实现Proxy
通过虚拟化-ES6 则为 JavaScript 中最基本的概念“对象(object) ”引入了虚拟化支持。
什么是对象
目前的定义:对象是属性的集合
所有的对象都具有一些基本的功能:
- 对象都有属性。你可以 get、 set 或删除它们或做更多操作
- 对象都有原型。 这也是 JS 中继承特性的实现方式。
- 有一些对象是可以被调用的函数或构造函数。
ES6标准中定义了14种方法:双方括号[[ ]]代表内部方法
- obj.[[Get]](key, receiver) —— 获取属性值。
当 JS 代码执行以下方法时被调用: obj.prop 或 obj[key]。
obj 是当前被搜索的对象, receiver 是我们首先开始搜索这个属性的对象。有时我们必须要搜索几个对象, obj 可能是一个在 receiver 原型链上的对象。 - obj.[[Set]](key, value, receiver) —— 为对象的属性赋值。
当 JS 代码执行以下方法时被调用: obj.prop = value 或 obj[key] = value。
执行类似 obj.prop += 2 这样的赋值语句时,首先调用[[Get]]方法,然后调用[[Set]]方法。对于++和--操作符来说亦是如此。 - obj.[HasProperty] —— 检测对象中是否存在某属性。
当 JS 代码执行以下方法时被调用: key in obj。 - obj.[Enumerate] ——列举对象的可枚举属性。
当 JS 代码执行以下方法时被调用: for (key in obj) …这个内部方法会返回一个可迭代对象, for-in 循环可通过这个方法得到对象属性的名称。 - obj.[GetPrototypeOf] —— 返回对象的原型。
当 JS 代码执行以下方法时被调用: obj.[__proto__]或Object.getPrototypeOf(obj)。 - functionObj.[[Call]](thisValue, arguments) —— 调用一个函数。
当 JS 代码执行以下方法时被调用: functionObj()或 x.method()。可选的。不是每一个对象都是函数。 - constructorObj.[[Construct]](arguments, newTarget) —— 调用一个构造函数。
当 JS 代码执行以下方法时被调用:举个例子, new Date(2890, 6, 2)。可选的。不是每一个对象都是构造函数。
参数 newTarget 在子类中起一定作用。
代理Proxy
ES6规范定义了一个全新的全局构造函数:Proxy它可以接受两个参数:目标对象(target) 与句柄对象(handler)。
var target = {}, handler = {};
var proxy = new Proxy(target, handler);
先讨论代理和目标对象之间的关系,再研究句柄对象的功用
将代理的所有内部方法转发至目标,例如调用proxy.[[Enumerate]]()就返回target.[[Enumerate]]();执行一条触发proxy.[[Set]]()方法的语句:proxy.color = "pink";
创建的代理会尽可能与目标的行为一致,当然也不完全相同 proxy!==target.
如果代理的目标是一个 DOM 元素,相应的代理就不是,此时类似 document.body.appendChild(proxy)的操作会触发类型错误(TypeError)。
代理句柄
句柄对象的方法可以覆写任意代理的内部方法。例如可以定义一个handler.set()方法拦截所有给对象赋值的行为:
var target = {};
var handler = {
set: function (target, key, value, receiver) {
throw new Error("请不要为这个对象设置属性。 ");
}
};
var proxy = new Proxy(target,handler);
MDN上关于Proxy的示例代码可以参考示例代码学习
示例一:“对象自动填充”
实现一个Tree()函数:
function Tree() {
return new Proxy({}, handler);
}
var handler = {
get: function (target, key, receiver) {
if (!(key in target)) {
target[key] = Tree(); // 自动创建一个子树
}
return Reflect.get(target, key, receiver);
}
};
在ES6中定义了一个新的反射reflect对象,只执行委托给目标的默认行为,reflect.get()
示例二:只读视图
我们需要实现一个函数, readOnlyView(object),它可以接受任何对象作为参数,并返回一个与此对象行为一致的代理,该代理不可被变更, 就像这样:
> var newMath = readOnlyView(Math);
> newMath.min(54, 40);
40
> newMath.max = Math.min;
Error: can't modify read-only view
> delete newMath.sin;
Error: can't modify read-only view
为了实现上述功能,需要进行干预,第一步是拦截可能修改目标对象的五种内部方法:
function NOPE() {
throw new Error("can't modify read-only view");
}
var handler = {
// 覆写所有五种可变方法。
set: NOPE,
defineProperty: NOPE,
deleteProperty: NOPE,
preventExtensions: NOPE,
setPrototypeOf: NOPE
};
function readOnlyView(target) {
return new Proxy(target, handler);
}
借助只读视图阻止了赋值、属性定义等过程。
上述方如果使用[[Get]]的一些方法任然可能返回可变对象。所以即使一些对象x是只读视图,但是x.prop仍然是可变的,BUG
解决方法如下:
function NOPE() {
throw new Error("can't modify read-only view");
}
var handler = {
// 覆写所有五种可变方法。
set: NOPE,
// 在只读视图中包裹其它结果。
get: function (target, key, receiver) {
// 从执行默认行为开始。
var result = Reflect.get(target, key, receiver);
// 确保返回一个不可变对象!
if (Object(result) === result) {
// result 是一个对象。
return readOnlyView(result);
}
// result 是一个原始原始类型,所以已经具备不可变的性质。
return result;
},
defineProperty: NOPE,
deleteProperty: NOPE,
preventExtensions: NOPE,
setPrototypeOf: NOPE
};
function readOnlyView(target) {
return new Proxy(target, handler);
}
同样,getPrototypeOf 和 getOwnPropertyDescriptor 这两个方法也需要进行同样的处理,创建代理非常简单,但是创建一个具有直观行为的代理相当困难
代理的优缺点
- 代理可以帮助你观察或记录对象访问,当调试代码时助你一臂之力,测试框架用代理来创建模拟对象(mock object)。
- 代理可以帮助你强化普通对象的能力,例如:惰性属性填充。
- 与 WeakMap 深度结合。在 WeakMap 中创建代理时的缓存内存,无论传递多少次对象给 readOnlyView,只创建一个代理
- 代理可解除。 ES6 规范中还定义了另外一个函数: Proxy.revocable(target,handler)。这个函数可以像 new Proxy(target, handler)一样创建代理,但是创建好的代理后续可被解除。(Proxy.revocable 方法返回一个对象,该对象有一个.proxy 属
性和一个.revoke 方法。),代理解除后停止运行并抛出所有内部的方法。 - 对象不变性。在某些情况下, ES6 需要代理的句柄方法来报告与目标对象状态一致结果,以此来保证所有对象甚至是代理的不变性。举个例子,除非目标不可扩展(inextensible),否则代理不能被声明为不可扩展的
对象到底什么?
对象是什么?可能现在最贴切的答案需要用 12 个内部方法进行定义:对象是在 JS程序中拥有[[Get]]、 [[Set]]等操作的实体。
本节参考《ES6-In-Depth》