前面几篇博客已经写过了有关 Symbols 和 Reflect 相关的知识,首先来重复看一下:
元编程是什么:元编程(笼统地说)是所有关于一门语言的底层机制,而不是数据建模或者业务逻辑那些高级抽象。如果程序可以被描述为 “制作程序”,元编程就能被描述为 “让程序来制作程序”。例如,反射就是元编程中非常酷的一部分,因为它允许你改变应用程序的内部工作机制。
Symbols 是实现了的反射(Reflection within implementation)—— 你将 Symbols 应用到你已有的类和对象上去改变它们的行为。
Reflect 是通过自省(introspection)实现反射(Reflection through introspection) —— 通常用来探索非常底层的代码信息。
Proxy 是通过调解(intercession)实现反射(Reflection through intercession) —— 包裹对象并通过自陷(trap)来拦截对象行为。
Proxy 是 ES6 中一个新的 API,是一个全局构造函数,你可以传递给它一个对象,以及一些钩子,它将会为你返回一个新的对象:这个对象将会魔法般的被给出的那些钩子包裹。对象因此拥有了代理。
创建代理
Proxy 构造函数接受两个参数,其一是你想要代理的初始对象,其二是一系列处理钩子(handler hooks)。
需要注意,代理维持了一个你创建对象的引用——如果你有了一个原始对象的引用,任何你和原始对象的交互,都会影响到代理,类似地,任何你对代理做的改变,反过来也都会影响到原始对象。换句话说,Proxy 返回了一个包裹了传入对象的新对象,但是任何你对二者的操作,都会影响到它们彼此:
// 代理和原对象的相互影响
var myObject = {};
var proxiedMyObject = new Proxy(myObject, {/*以及一系列处理钩子*/});
assert(myObject !== proxiedMyObject);
myObject.foo = true;
assert(proxiedMyObject.foo === true);
proxiedMyObject.bar = true;
assert(myObject.bar === true);
代理的钩子
钩子本质上就是一系列函数。这些函数控制了你如何和代理对象的交互,因此他们也顺带控制了你如何和原对象的交互。处理钩子都是一些 JS 的内置方法。这些内置方法就是 Reflect 方法的 API。
代理 Proxy 和反射 Reflect,他们两个可以说是一一对应的。每一个代理钩子都对应到一个反射方法,反之亦然,每一个反射方法都有一个代理钩子。完整的反射方法及对应的代理处理钩子如下:
通过对这些方法的重写,我们可以改写外界与对象交互的方式。换句话说,代理对象就像是原对象的代理人,对象如何对外界作出反应,必须通过代理人规定的方法。
几个栗子
构建一个可无限链接(chainable)的 API
function urlBuilder(domain) {
var parts = [];
var proxy = new Proxy(function () {
var returnValue = domain + '/' + parts.join('/');
parts = [];
return returnValue;
}, {
has: function () {
return true;
},
get: function (object, prop) {
parts.push(prop);
return proxy;
},
});
return proxy;
}
var google = urlBuilder('http://google.com');
assert(google.search.products.bacon.and.eggs() === 'http://google.com/search/products/bacon/and/eggs')
这个函数的功能通过最后一句话就可以大致看出:将对方法的访问依次转变为网络地址。
在代理中,重写了方法 has 保证所有属性的访问都是有的。对 get 方法的重写则保证了获取属性值的时候依旧返回一个 proxy 完成链式调用,并记录下访问的属性名。
实现一个 “方法缺失” 钩子
许多其他的编程语言都允许你使用一个内置的反射方法去重写一个类的行为,例如,在 PHP 中有 call,在 Ruby 中有 method_missing,在 Python 中则有 getattr。JavaScript 缺乏这个机制,但现在我们有了代理去实现它。
这个机制的作用是,对于任何存在方法,该方法能够按预期被执行。对于不存在方法,在调用时会被 method_missing 替代。另外,method-missing 接受方法名作为第一个参数,这对于判断用户意图非常有用。
通过实现一个 get 方法的代理,可以完成这个功能:
function Foo() {
return new Proxy(this, {
get: function (object, property) {
if (Reflect.has(object, property)) {
return Reflect.get(object, property);
} else {
return function methodMissing() {
console.log('you called ' + property + ' but it doesn\'t exist!');
}
}
}
});
}
Foo.prototype.bar = function () {
console.log('you called bar. Good job!');
}
foo = new Foo();
foo.bar();
// you called bar. Good job!
foo.this_method_does_not_exist();
// you called this_method_does_not_exist but it doesn't exist!
隐藏属性,不被 getOwnPropertyNames、Object.keys、in 等迭代方法找到
var example = new Proxy({ foo: 1, bar: 2 }, {
has: function () { return false; },
ownKeys: function () { return []; },
getOwnPropertyDescriptor: function () { return false; },
});
assert(example.foo === 1);
assert(example.bar === 2);
assert('foo' in example === false);
assert('bar' in example === false);
assert(example.hasOwnProperty('foo') === false);
assert(example.hasOwnProperty('bar') === false);
assert.deepEqual(Object.keys(example), [ ]);
assert.deepEqual(Object.getOwnPropertyNames(example), [ ]);
完美隐藏!
可撤销代理
代理还有最后一个大招:一些代理可以被撤销。为了创建一个可撤销的代理,你需要使用 Proxy.revocable(target, handler) (而不是 new Proxy(target, handler)),并且,最终返回一个结构为 {proxy, revoke()} 的对象来替代直接返回一个代理对象。
function youOnlyGetOneSafetyNet(object) {
var revocable = Proxy.revocable(object, {
get(target, property) {
if (Reflect.has(target, property)) {
return Reflect.get(target, property);
} else {
// 如果属性不存在,代理就会被撤销
revocable.revoke();
return 'You only get one safety net';
}
}
});
return revocable.proxy;
}
var myObject = youOnlyGetOneSafetyNet({ foo: true });
assert(myObject.foo === true);
assert(myObject.foo === true);
assert(myObject.foo === true);
assert(myObject.bar === 'You only get one safety net'); // 代理被撤销
myObject.bar // TypeError
myObject.bar // TypeError
Reflect.has(myObject, 'bar') // TypeError
你可以看到例子中最后一行的右侧,如果代理已经被撤销,任何在代理对象上的操作都会抛出 TypeError —— 即便这些操作句柄还没有被代理。