- 原文地址:Metaprogramming in ES6: Part 3 - Proxies
- 原文做者:Keith Cirkel
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:yoyoyohamapi
- 校对者:caoyi0905 PCAaron
这是个人 ES6 元编程系列的第三部分,也是最后一部分,还记得这个系列的文章我一年以前就开始动笔了,而且承诺不会花一年才写完,但现实就是我还真花费了如此多的时间去完成。在最后这篇文章中,咱们要看看多是 ES6 中最酷的反射特性:代理(Proxy)。因为反射和本文的部份内容有关,若是你还没读过上一篇讲述 ES6 Reflect API 的文章,以及更早的、讲述 ES6 Symbols 的文章,先倒回去阅读一下,这样才能更好地理解本文。和其余部分同样,我先引用一下在第一部分提到过的观点:前端
所以,Proxy
是一个全新的全局构造函数(相似 Date
或者 Number
),你能够传递给其一个对象,以及一些钩子(hook),它能为你返回一个 新的 对象,新的对象使用这些充满魔力的钩子包裹了老对象。如今,你拥有了代理,但愿你喜欢它,我也高兴你回到这个系列中来。node
关于代理,有不少须要阐述的。但对新手来讲,先让咱们看看怎么建立一个代理。react
Proxy 构造函数接受两个参数,其一是你想要代理的初始对象,其二是一系列处理钩子(handler hooks)。咱们先忽略第二个钩子参数,看看怎么为现有对象建立代理。线索即在代理这个名字中:它们维持了一个你建立对象的引用,可是若是你有了一个原始对象的引用,任何你和原始对象的交互,都会影响到代理,相似地,任何你对代理作的改变,反过来也都会影响到原始对象。换句话说,Proxy 返回了一个包裹了传入对象的新对象,可是任何你对两者的操做,都会影响到它们彼此。为了证明这一点,请看代码:android
var myObject = {};
var proxiedMyObject = new Proxy(myObject, {/*以及一系列处理钩子*/});
assert(myObject !== proxiedMyObject);
myObject.foo = true;
assert(proxiedMyObject.foo === true);
proxiedMyObject.bar = true;
assert(myObject.bar === true);复制代码
目前为止,咱们什么目的也没达到,相较于直接使用被代理对象,代理并不能提供任何额外收益。只有用上了处理钩子,咱们才能在代理上作一些有趣的事儿。ios
处理钩子是一系列的函数,每个钩子都有一个具体名字以供代理识别,每个钩子也控制了你如何和代理交互(所以,也控制了你和被包裹对象的交互)。处理钩子勾住了 JavaScript 的 “内置方法”,若是你对此感受熟悉,是由于咱们在 上一篇介绍 Reflect API 的文章 中提到了内置方法。git
是时候铺开来讲代理了。我把代理放到系列的最后一部分的重要缘由是:因为代理和反射就像一对苦命鸳鸯交织在一块儿,所以咱们须要先知道反射是如何工做的。如你所见,每个代理钩子都对应到一个反射方法,反之亦然,每个反射方法都有一个代理钩子。完整的反射方法及对应的代理处理钩子以下:es6
apply
(以一个 this
参数和一系列 arguments
(参数序列)调用函数)construct
(以一系列 arguments
及一个可选的、指明了原型的构造函数调用一个类函数或者构造函数)defineProperty
(在对象上定义一个属性,并声明该属性中诸如对象可迭代性这样的元信息)getOwnPropertyDescriptor
(得到一个属性的 “属性描述子”:描述子包含了诸如对象可迭代性这样的元信息)deleteProperty
(从对象上删除某个属性)getPrototypeOf
(得到某实例的原型)setPrototypeOf
(设置某实例的原型)isExtensible
(判断一个对象是不是 “可扩展的”,亦即判断是否能够为其添加属性)preventExtensions
(阻止对象被扩展)get
(获得对象的某个属性)set
(设置对象的某个属性)has
(在不断言(assert)属性值的状况下,判断对象是否含有某个属性)ownKeys
(得到某个对象自身全部的 key,排除掉其原型上的 key)在反射那一部分中(再啰嗦一遍,若是你没看过,赶快去看),咱们已经浏览过上述全部方法了(并附带有例子)。代理用相同的参数集实现了每个方法。实际上, 代理的默认行为实际上已经实现了对每一个处理程序钩子的反射调用(其内部机制对于不一样的 JavaScript 引擎可能会有所区别,但对于没有说明的钩子,咱们只须要认为它和对应的反射方法行为一致便可)。这也意味着,任何你没有指定的钩子,都具备和默认情况一致的行为,就像它从未被代理过同样:github
// 咱们新建立了代理,并定义了与默认建立时同样的行为
proxy = new Proxy({}, {
apply: Reflect.apply,
construct: Reflect.construct,
defineProperty: Reflect.defineProperty,
getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor,
deleteProperty: Reflect.deleteProperty,
getPrototypeOf: Reflect.getPrototypeOf,
setPrototypeOf: Reflect.setPrototypeOf,
isExtensible: Reflect.isExtensible,
preventExtensions: Reflect.preventExtensions,
get: Reflect.get,
set: Reflect.set,
has: Reflect.has,
ownKeys: Reflect.ownKeys,
});复制代码
如今,我能够深刻到每一个代理钩子的工做细节中去了,可是基本上都是复制粘贴反射中的例子(只须要修改不多的部分)。若是只是介绍每一个钩子的功能,对代理来讲就不太公平,由于代理是去实现一些炫酷用例的。因此,本文剩余内容都将为你展现经过代理完成的炫酷的东西,甚至是一些你没了代理就没法完成的事。web
同时,为了让内容更具交互性,我为每一个例子都建立一个小的库来展现对应的功能。我会给出每一个例子对应的代码仓库连接。npm
之前面的例子为基础 —— 咱们仍使用 [[Get]]
自陷:只须要再施加一点魔法,咱们就能构建一个拥有无数方法的 API,当你最终调用其中某个方法时,将返回全部你被你连接的值。fluent API(流畅 API) 为 web 请求构建了各个 URL,Chai 这类的测试框架将各个英文单词连接组成高可读的测试断言,经过这些,咱们知道可无限连接的 API 是多么有用。
为了实现这个 API,咱们就须要钩子勾住 [[Get]]
,将取到的属性保存到数组中。代理 ( Proxy ) 将包装一个函数,返回全部检索到的支持的Array,并清空数组,以即可以重用它。咱们也会勾住 [[HasProperty]]
,由于咱们想告诉 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')复制代码
你也能够用相同的模式实现一个树遍历的 fluent API,这相似于你在 jQuery 或者 React 中看到的选择器:
function treeTraverser(tree) {
var parts = [];
var proxy = new Proxy(function (parts) {
let node = tree; // 从树的根节点开始
for (part of parts) {
if (!node.props || !node.props.children || node.props.children.length === 0) {
throw new Error(`Node ${node.tagName} has no more children`);
}
// 若是该部分是一个子节点,就深刻到该子节点进行下一次遍历
let index = node.props.children.findIndex((child) => child.tagName == part);
if(index === -1) {
throw new Error(`Cannot find child: ${part} in ${node.tagName}`);
}
node = node.props.children[index];
}
return node.props;
}, {
has: function () {
return true;
},
get: function () {
parts.push(prop);
return proxy;
}
});
return proxy;
}
var myDomIsh = treeTraverserExample({
tagName: 'body',
props: {
children: [
{
tagName: 'div',
props: {
className: 'main',
children: [
{
tagName: 'span',
props: {
className: 'extra',
children: [
{ tagName: 'i', props: { textContent: 'Hello' } },
{ tagName: 'b', props: { textContent: 'World' } },
]
}
}
]
}
}
]
}
});
assert(myDomIsh.div.span.i().textContent === 'Hello');
assert(myDomIsh.div.span.b().textContent === 'World');复制代码
我已经发布了一个更加可复用的版本到 github.com/keithamus/p… 上,npm 上也有其同名的包。
许多其余的编程语言都容许你使用一个内置的反射方法去重写一个类的行为,例如,在 PHP 中有 __call
,在 Ruby 中有 method_missing
,在 Python 中则有 __getattr__
。JavaScript 缺少这个机制,但如今咱们有了代理去实现它。
在开始介绍代理的实现以前,咱们先看下 Ruby 是怎么作的,来从中得到一些灵感:
class Foo
def bar()
print "you called bar. Good job!"
end
def method_missing(method)
print "you called `#{method}` but it doesn't exist!"
end
end
foo = Foo.new
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!复制代码
对于任何存在方法,在此例中是 bar
,该方法可以按预期被执行。对于不存在方法,好比 foo
或者 this_method_does_not_exist
,在调用时会被 method_missing
替代。另外,method_missing
接受方法名做为第一个参数,这对于判断用户意图很是有用。
咱们能够经过混入 ES6 Symbol 实现相似的功能:使用一个函数包裹类,该函数将返回使用了 get
([[Get]]
)自陷的代理,或者说是拦截了 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!复制代码
当你有若干方法功能很是相似,而且能够从函数名推测功能间的差别性,上面的作法就很是有用。将函数的功能区分从参数转移到函数名,将带来更好的可读性。做为此的一个例子 —— 你能够快速轻易地建立一个单位转换 API,如货币或者是进制的转化:
const baseConvertor = new Proxy({}, {
get: function baseConvert(object, methodName) {
var methodParts = methodName.match(/base(\d+)toBase(\d+)/);
var fromBase = methodParts && methodParts[1];
var toBase = methodParts && methodParts[2];
if (!methodParts || fromBase > 36 || toBase > 36 || fromBase < 2 || toBase < 2) {
throw new Error('TypeError: baseConvertor' + methodName + ' is not a function');
}
return function (fromString) {
return parseInt(fromString, fromBase).toString(toBase);
}
}
});
baseConvertor.base16toBase2('deadbeef') === '11011110101011011011111011101111';
baseConvertor.base2toBase16('11011110101011011011111011101111') === 'deadbeef';复制代码
固然,你也能够手动建立总计 1296 组合状况的方法,或者单独经过一个循环来建立这些方法,可是这二者都须要用更多的代码来完成。
一个更加具体的例子是 Ruby on Rails 中的 ActiveRecord,其源于 “动态查找(dynamic finders)”。ActiveRecord 基本上实现了 “method_missing” 来容许你根据列查询一个表。使用函数名做为查询关键字,避免了使用传递一个复杂对象来建立查询语句:
Users.find_by_first_name('Keith'); # [ Keith Cirkel, Keith Urban, Keith David ]
Users.find_by_first_name_and_last_name('Keith', 'David'); # [ Keith David ]复制代码
在 JavaScript 中,咱们也能实现相似功能:
function RecordFinder(options) {
this.attributes = options.attributes;
this.table = options.table;
return new Proxy({}, function findProxy(methodName) {
var match = methodName.match(new RegExp('findBy((?:And)' + this.attributes.join('|') + ')'));
if (!match){
throw new Error('TypeError: ' + methodName + ' is not a function');
}
});
});复制代码
和其余例子同样,我已经写了一个关于此的库放到了 github.com/keithamus/p…,npm 上也能够到同名的包。
getOwnPropertyNames
、Object.keys
、in
等全部迭代方法中隐藏全部的属性咱们可使用代理让一个对象的全部的属性都隐藏起来,除非是要得到属性的值。下面罗列了全部 JavaScript 中你能够判断某属性是否存在于一个对象的方法:
Reflect.has
、Object.hasOwnProperty
、Object.prototype.hasOwnProperty
以及 in
运算符所有使用了 [[HasProperty]]
。代理能够经过 has
拦截它。Object.keys
/Object.getOwnPropertyNames
都使用了 [[OwnPropertyKeys]]
。代理能够经过 ownKeys
进行拦截。Object.entries
(一个即将到来的 ES2017 特性),也使用了 [[OwnPropertyKeys]]
,代理仍然能够经过 ownKeys
进行拦截。Object.getOwnPropertyDescriptor
使用了 [[GetOwnProperty]]
。特别特别让人兴奋的是,代理能够经过 getOwnPropertyDescriptor
进行拦截。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), [ ]);复制代码
老实说,我也没有发现这个模式有特别大的用处。可是,我仍是建立了一个关于此的一个库,并放在了github.com/keithamus/p…,它能让你单独地设置某个属性不可见了,而不是一锅端地让全部属性不可见。
对新规范所添加的内容一直敏锐追踪的人们可能已经注意到了, Object.observe
开始被考虑归入 ES2016 了。Object.observe
的拥护者已经开始计划 起草包含有有 Object.observe 的提案,他们对此有一个很是好的理由:草案初衷就是要帮助框架做者解决数据绑定(Data Binding)的问题。如今,随着 React 和 Polymer 1.0 的发布,数据绑定框架有所降温,不可变数据(immutable data)开始变得流行。
庆幸的是,代理让诸如 Object.observe 这样的规范变得多余,如今咱们能够经过代理实现一个更加底层的 Object.observe。为了更加接近 Object.observe 所具备的特性,咱们须要钩住 [[Set]]
、[[PreventExtensions]]
、[[Delete]]
以及 [[DefineOwnProperty]]
这些内置方法 —— 代理分别可使用 set
、preventExtensions
、deleteProperty
及 defineProperty
进行拦截:
function observe(object, observerCallback) {
var observing = true;
const proxyObject = new Proxy(object, {
set: function (object, property, value) {
var hadProperty = Reflect.has(object, property);
var oldValue = hadProperty && Reflect.get(object, property);
var returnValue = Reflect.set(object, property, value);
if (observing && hadProperty) {
observerCallback({ object: proxyObject, type: 'update', name: property, oldValue: oldValue });
} else if(observing) {
observerCallback({ object: proxyObject, type: 'add', name: property });
}
return returnValue;
},
deleteProperty: function (object, property) {
var hadProperty = Reflect.has(object, property);
var oldValue = hadProperty && Reflect.get(object, property);
var returnValue = Reflect.deleteProperty(object, property);
if (observing && hadProperty) {
observerCallback({ object: proxyObject, type: 'delete', name: property, oldValue: oldValue });
}
return returnValue;
},
defineProperty: function (object, property, descriptor) {
var hadProperty = Reflect.has(object, property);
var oldValue = hadProperty && Reflect.getOwnPropertyDescriptor(object, property);
var returnValue = Reflect.defineProperty(object, property, descriptor);
if (observing && hadProperty) {
observerCallback({ object: proxyObject, type: 'reconfigure', name: property, oldValue: oldValue });
} else if(observing) {
observerCallback({ object: proxyObject, type: 'add', name: property });
}
return returnValue;
},
preventExtensions: function (object) {
var returnValue = Reflect.preventExtensions(object);
if (observing) {
observerCallback({ object: proxyObject, type: 'preventExtensions' })
}
return returnValue;
},
});
return { object: proxyObject, unobserve: function () { observing = false } };
}
var changes = [];
var observer = observe({ id: 1 }, (change) => changes.push(change));
var object = observer.object;
var unobserve = observer.unobserve;
object.a = 'b';
object.id++;
Object.defineProperty(object, 'a', { enumerable: false });
delete object.a;
Object.preventExtensions(object);
unobserve();
object.id++;
assert.equal(changes.length, 5);
assert.equal(changes[0].object, object);
assert.equal(changes[0].type, 'add');
assert.equal(changes[0].name, 'a');
assert.equal(changes[1].object, object);
assert.equal(changes[1].type, 'update');
assert.equal(changes[1].name, 'id');
assert.equal(changes[1].oldValue, 1);
assert.equal(changes[2].object, object);
assert.equal(changes[2].type, 'reconfigure');
assert.equal(changes[2].oldValue.enumerable, true);
assert.equal(changes[3].object, object);
assert.equal(changes[3].type, 'delete');
assert.equal(changes[3].name, 'a');
assert.equal(changes[4].object, object);
assert.equal(changes[4].type, 'preventExtensions');复制代码
正如你所看到的,咱们用一小段代码实现了一个相对完整的 Object.observe。该实现和规范之间的差别在于,Object.observe 是可以改变对象的,而代理则返回了一个新对象,而且 unobserver 函数也不是全局的。
和其余例子同样,我也写了关于此的一个库并放在了 github.com/keithamus/p… 以及 npm 上。
代理还有最后一个大招:一些代理能够被撤销。为了建立一个可撤销的代理,你须要使用 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 —— 即使这些操做句柄尚未被代理。我以为这多是可撤销代理的一种能力。若是全部的操做都能与对应的 Reflect 返回一致(这会使得代理冗余,并让对象表现得好像从未设置过代理同样),将使该特性更加有用。这个特性被放在了本文压轴部分,也是由于我也不真正肯定可撤回代理的具体用例。
我但愿这篇文章让你认识到代理是一个强大到难以想象的工具,它弥补了 JavaScript 内部曾经的缺失。在方方面面,Symbol、Reflect、以及代理都为 JavaScript 开启了新的篇章 —— 就如同 const 和 let,类和箭头函数那样。const 和 let 再也不让代码显得混乱肮脏,类和箭头函数让代码更简洁,Symbol、Reflect、和 Proxy 则开始给予开发者在 JavaScript 中进行底层的元编程。
这些新的元编程工具不会在短期内放慢发展的速度:EcamScript 的新版本正逐渐完善,并添加了更多有趣的行为,例如 Reflect.isCallable
和 Reflect.isConstructor
的提案,亦或 stage 0 关于 Reflect.type
的提案,亦或 function.sent
这个元属性的提案
,亦或这个包含了更多函数元属性的提案。这些新的 API 也激发了一些关于新特性的有趣讨论,例如 这个关于添加 Reflect.parse
的提案,就引发了关于建立一个 AST(Abstract Syntax Tree:抽象语法树)标准的讨论。
你是怎么看待新的 Proxy API 的?已经计划用在你的项目里面了?能够在 Twitter 上给我留言让我知道你的想法,我是 @keithamus。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。