特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS
元编程是针对程序本身的行为进行操作的编程。换句话说,它是为你程序的编程而进行的编程。是的,很拗口,对吧?
例如,如果你为了调查对象a
和另一个对象b
之间的关系 —— 它们是被[[Prototype]]
链接的吗? —— 而使用a.isPrototypeOf(b)
,这通常称为自省,就是一种形式的元编程。宏(JS中还没有) —— 代码在编译时修改自己 —— 是元编程的另一个明显的例子。使用for..in
循环枚举一个对象的键,或者检查一个对象是否是一个“类构造器”的 实例,是另一些常见的元编程任务。
元编程关注以下的一点或几点:代码检视自己,代码修改自己,或者代码修改默认的语言行为而使其他代码受影响。
元编程的目标是利用语言自身的内在能力使你其他部分的代码更具描述性,表现力,和/或灵活性。由于元编程的 元 的性质,要给它一个更精确的定义有些困难。理解元编程的最佳方法是通过代码来观察它。
ES6在JS已经拥有的东西上,增加了几种新的元编程形式/特性。
函数名
有一些情况,你的代码想要检视自己并询问某个函数的名称是什么。如果你询问一个函数的名称,答案会有些令人诧异地模糊。考虑如下代码:
function daz() {
// ..
}
var obj = {
foo: function() {
// ..
},
bar: function baz() {
// ..
},
bam: daz,
zim() {
// ..
}
};
在这前一个代码段中,“obj.foo()
的名字是什么?”有些微妙。是"foo"
,""
,还是undefined
?那么obj.bar()
呢 —— 是"bar"
还是"baz"
?obj.bam()
称为"bam"
还是"daz"
?obj.zim()
呢?
另外,作为回调被传递的函数呢?就像:
function foo(cb) {
// 这里的 `cb()` 的名字是什么?
}
foo( function(){
// 我是匿名的!
} );
在程序中函数可以被好几种方法所表达,而函数的“名字”应当是什么并不总是那么清晰和明确。
更重要的是,我们需要区别函数的“名字”是指它的name
属性 —— 是的,函数有一个叫做name
的属性 —— 还是指它词法绑定的名称,比如在function bar() { .. }
中的bar
。
词法绑定名称是你将在递归之类的东西中所使用的:
function foo(i) {
if (i < 10) return foo( i * 2 );
return i;
}
name
属性是你为了元编程而使用的,所以它才是我们在这里的讨论中所关注的。
产生这种用困惑是因为,在默认情况下一个函数的词法名称(如果有的话)也会被设置为它的name
属性。实际上,ES5(和以前的)语言规范中并没有官方要求这种行为。name
属性的设置是一种非标准,但依然相当可靠的行为。在ES6中,它已经被标准化。
提示: 如果一个函数的name
被赋值,它通常是在开发者工具的栈轨迹中使用的名称。
推断
但如果函数没有词法名称,name
属性会怎么样呢?
现在在ES6中,有一个推断规则可以判定一个合理的name
属性值来赋予一个函数,即使它没有词法名称可用。
考虑如下代码:
var abc = function() {
// ..
};
abc.name; // "abc"
如果我们给了这个函数一个词法名称,比如abc = function def() { .. }
,那么name
属性将理所当然地是"def"
。但是由于缺少词法名称,直观上名称"abc"
看起来很合适。
这里是在ES6中将会(或不会)进行名称推断的其他形式:
(function(){ .. }); // name:
(function*(){ .. }); // name:
window.foo = function(){ .. }; // name:
class Awesome {
constructor() { .. } // name: Awesome
funny() { .. } // name: funny
}
var c = class Awesome { .. }; // name: Awesome
var o = {
foo() { .. }, // name: foo
*bar() { .. }, // name: bar
baz: () => { .. }, // name: baz
bam: function(){ .. }, // name: bam
get qux() { .. }, // name: get qux
set fuz() { .. }, // name: set fuz
["b" + "iz"]:
function(){ .. }, // name: biz
[Symbol( "buz" )]:
function(){ .. } // name: [buz]
};
var x = o.foo.bind( o ); // name: bound foo
(function(){ .. }).bind( o ); // name: bound
export default function() { .. } // name: default
var y = new Function(); // name: anonymous
var GeneratorFunction =
function*(){}.__proto__.constructor;
var z = new GeneratorFunction(); // name: anonymous
name
属性默认是不可写的,但它是可配置的,这意味着如果有需要,你可以使用Object.defineProperty(..)
来手动改变它。
元属性
在第三章的“new.target
”一节中,我们引入了一个ES6的新概念:元属性。正如这个名称所暗示的,元属性意在以一种属性访问的形式提供特殊的元信息,而这在以前是不可能的。
在new.target
的情况下,关键字new
作为一个属性访问的上下文环境。显然new
本身不是一个对象,这使得这种能力很特殊。然而,当new.target
被用于一个构造器调用(一个使用new
调用的函数/方法)内部时,new
变成了一个虚拟上下文环境,如此new.target
就可以指代这个new
调用的目标构造器。
这是一个元编程操作的典型例子,因为它的意图是从一个构造器调用内部判定原来的new
的目标是什么,这一般是为了自省(检查类型/结构)或者静态属性访问。
举例来说,你可能想根据一个构造器是被直接调用,还是通过一个子类进行调用,来使它有不同的行为:
class Parent {
constructor() {
if (new.target === Parent) {
console.log( "Parent instantiated" );
}
else {
console.log( "A child instantiated" );
}
}
}
class Child extends Parent {}
var a = new Parent();
// Parent instantiated
var b = new Child();
// A child instantiated
这里有一个微妙的地方,在Parent
类定义内部的constructor()
实际上被给予了这个类的词法名称(Parent
),即便语法暗示着这个类是一个与构造器分离的不同实体。
警告: 与所有的元编程技术一样,要小心不要创建太过聪明的代码,而使未来的你或其他维护你代码的人很难理解。小心使用这些技巧。
通用 Symbol
在第二章中的“Symbol”一节中,我们讲解了新的ES6基本类型symbol
。除了你可以在你自己的程序中定义的symbol以外,JS预定义了几种内建symbol,被称为 通用(Well Known) Symbols(WKS)。
定义这些symbol值主要是为了向你的JS程序暴露特殊的元属性来给你更多JS行为的控制权。
我们将简要介绍每一个symbol并讨论它们的目的。
Symbol.iterator
在第二和第三章中,我们介绍并使用了@@iterator
symbol,它被自动地用于...
扩散和for..of
循环。我们还在第五章中看到了在新的ES6集合中定义的@@iterator
。
Symbol.iterator
表示在任意一个对象上的特殊位置(属性),语言机制自动地在这里寻找一个方法,这个方法将构建一个用于消费对象值的迭代器对象。许多对象都带有一个默认的Symbol.iterator
。
然而,我们可以通过设置Symbol.iterator
属性来为任意对象定义我们自己的迭代器逻辑,即便它是覆盖默认迭代器的。这里的元编程观点是,我们在定义JS的其他部分(明确地说,是操作符和循环结构)在处理我们所定义的对象值时所使用的行为。
考虑如下代码:
var arr = [4,5,6,7,8,9];
for (var v of arr) {
console.log( v );
}
// 4 5 6 7 8 9
// 定义一个仅在奇数索引处产生值的迭代器
arr[Symbol.iterator] = function*() {
var idx = 1;
do {
yield this[idx];
} while ((idx += 2) < this.length);
};
for (var v of arr) {
console.log( v );
}
// 5 7 9
Symbol.toStringTag
和 Symbol.hasInstance
最常见的元编程任务之一,就是在一个值上进行自省来找出它是什么 种类 的,者经常用来决定它们上面适于实施什么操作。对于对象,最常见的两个自省技术是toString()
和instanceof
。
考虑如下代码:
function Foo() {}
var a = new Foo();
a.toString(); // [object Object]
a instanceof Foo; // true
在ES6中,你可以控制这些操作的行为:
function Foo(greeting) {
this.greeting = greeting;
}
Foo.prototype[Symbol.toStringTag] = "Foo";
Object.defineProperty( Foo, Symbol.hasInstance, {
value: function(inst) {
return inst.greeting == "hello";
}
} );
var a = new Foo( "hello" ),
b = new Foo( "world" );
b[Symbol.toStringTag] = "cool";
a.toString(); // [object Foo]
String( b ); // [object cool]
a instanceof Foo; // true
b instanceof Foo; // false
在原型(或实例本身)上的@@toStringTag
symbol指定一个用于[object ___]
字符串化的字符串值。
@@hasInstance
symbol是一个在构造器函数上的方法,它接收一个实例对象值并让你通过放回true
或false
来决定这个值是否应当被认为是一个实例。
注意: 要在一个函数上设置@@hasInstance
,你必须使用Object.defineProperty(..)
,因为在Function.prototype
上默认的那一个是writable: false
。更多信息参见本系列的 this与对象原型。
Symbol.species
在第三章的“类”中,我们介绍了@@species
symbol,它控制一个类内建的生成新实例的方法使用哪一个构造器。
最常见的例子是,在子类化Array
并且想要定义slice(..)
之类被继承的方法应当使用哪一个构造器时。默认地,在一个Array
的子类实例上调用的slice(..)
将产生这个子类的实例,坦白地说这正是你经常希望的。
但是,你可以通过覆盖一个类的默认@@species
定义来进行元编程:
class Cool {
// 将 `@@species` 倒推至被衍生的构造器
static get [Symbol.species]() { return this; }
again() {
return new this.constructor[Symbol.species]();
}
}
class Fun extends Cool {}
class Awesome extends Cool {
// 将 `@@species` 强制为父类构造器
static get [Symbol.species]() { return Cool; }
}
var a = new Fun(),
b = new Awesome(),
c = a.again(),
d = b.again();
c instanceof Fun; // true
d instanceof Awesome; // false
d instanceof Cool; // true
就像在前面的代码段中的Cool
的定义展示的那样,在内建的原生构造器上的Symbol.species
设定默认为return this
。它在用户自己的类上没有默认值,但也像展示的那样,这种行为很容易模拟。
如果你需要定义生成新实例的方法,使用new this.constructor[Symbol.species](..)
的元编程模式,而不要用手写的new this.constructor(..)
或者new XYZ(..)
。如此衍生的类就能够自定义Symbol.species
来控制哪一个构造器来制造这些实例。
Symbol.toPrimitive
在本系列的 类型与文法 一书中,我们讨论了ToPrimitive
抽象强制转换操作,它在对象为了某些操作(例如==
比较或者+
加法)而必须被强制转换为一个基本类型值时被使用。在ES6以前,没有办法控制这个行为。
在ES6中,在任意对象值上作为属性的@@toPrimitive
symbol都可以通过指定一个方法来自定义这个ToPrimitive
强制转换。
考虑如下代码:
var arr = [1,2,3,4,5];
arr + 10; // 1,2,3,4,510
arr[Symbol.toPrimitive] = function(hint) {
if (hint == "default" || hint == "number") {
// 所有数字的和
return this.reduce( function(acc,curr){
return acc + curr;
}, 0 );
}
};
arr + 10; // 25
Symbol.toPrimitive
方法将根据调用ToPrimitive
的操作期望何种类型,而被提供一个值为"string"
,"number"
,或"default"
(这应当被解释为"number"
)的 提示(hint)。在前一个代码段中,+
加法操作没有提示("default"
将被传递)。一个*
乘法操作将提示"number"
,而一个String(arr)
将提示"string"
。
警告: ==
操作符将在一个对象上不使用任何提来示调用ToPrimitive
操作 —— 如果存在@@toPrimitive
方法的话,将使用"default"
被调用 —— 如果另一个被比较的值不是一个对象。但是,如果两个被比较的值都是对象,==
的行为与===
是完全相同的,也就是引用本身将被直接比较。这种情况下,@@toPrimitive
根本不会被调用。关于强制转换和抽象操作的更多信息,参见本系列的 类型与文法。
正则表达式 Symbols
对于正则表达式对象,有四种通用 symbols 可以被覆盖,它们控制着这些正则表达式在四个相应的同名String.prototype
函数中如何被使用:
-
@@match
:一个正则表达式的Symbol.match
值是使用被给定的正则表达式来匹配一个字符串值的全部或部分的方法。如果你为String.prototype.match(..)
传递一个正则表达式做范例匹配,它就会被使用。匹配的默认算法写在ES6语言规范的第21.2.5.6部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@match)。你可以覆盖这个默认算法并提供额外的正则表达式特性,比如后顾断言。
Symbol.match
还被用于isRegExp
抽象操作(参见第六章的“字符串检测函数”中的注意部分)来判定一个对象是否意在被用作正则表达式。为了使一个这样的对象不被看作是正则表达式,可以将Symbol.match
的值设置为false
(或falsy的东西)强制这个检查失败。 -
@@replace
:一个正则表达式的Symbol.replace
值是被String.prototype.replace(..)
使用的方法,来替换一个字符串里面出现的一个或所有字符序列,这些字符序列匹配给出的正则表达式范例。替换的默认算法写在ES6语言规范的第21.2.5.8部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@replace)。
一个覆盖默认算法的很酷的用法是提供额外的
replacer
可选参数值,比如通过用连续的替换值消费可迭代对象来支持"abaca".replace(/a/g,[1,2,3])
产生"1b2c3"
。 -
@@search
:一个正则表达式的Symbol.search
值是被String.prototype.search(..)
使用的方法,来在一个字符串中检索一个匹配给定正则表达式的子字符串。检索的默认算法写在ES6语言规范的第21.2.5.9部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@search)。
-
@@split
:一个正则表达式的Symbol.split
值是被String.prototype.split(..)
使用的方法,来将一个字符串在分隔符匹配给定正则表达式的位置分割为子字符串。分割的默认算法写在ES6语言规范的第21.2.5.11部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@split)。
覆盖内建的正则表达式算法不是为心脏脆弱的人准备的!JS带有高度优化的正则表达式引擎,所以你自己的用户代码将很可能慢得多。这种类型的元编程很精巧和强大,但是应当仅用于确实必要或有好处的情况下。
Symbol.isConcatSpreadable
@@isConcatSpreadable
symbol可以作为一个布尔属性(Symbol.isConcatSpreadable
)在任意对象上(比如一个数组或其他的可迭代对象)定义,来指示当它被传递给一个数组concat(..)
时是否应当被 扩散。
考虑如下代码:
var a = [1,2,3],
b = [4,5,6];
b[Symbol.isConcatSpreadable] = false;
[].concat( a, b ); // [1,2,3,[4,5,6]]
Symbol.unscopables
@@unscopables
symbol可以作为一个对象属性(Symbol.unscopables
)在任意对象上定义,来指示在一个with
语句中哪一个属性可以和不可以作为此法变量被暴露。
考虑如下代码:
var o = { a:1, b:2, c:3 },
a = 10, b = 20, c = 30;
o[Symbol.unscopables] = {
a: false,
b: true,
c: false
};
with (o) {
console.log( a, b, c ); // 1 20 3
}
一个在@@unscopables
对象中的true
指示这个属性应当是 非作用域(unscopable) 的,因此会从此法作用域变量中被过滤掉。false
意味着它可以被包含在此法作用域变量中。
警告: with
语句在strict
模式下是完全禁用的,而且因此应当被认为是在语言中被废弃的。不要使用它。更多信息参见本系列的 作用域与闭包。因为应当避免with
,所以这个@@unscopables
symbol也是无意义的。
代理
在ES6中被加入的最明显的元编程特性之一就是proxy
特性。
一个代理是一种由你创建的特殊的对象,它“包”着另一个普通的对象 —— 或者说挡在这个普通对象的前面。你可以在代理对象上注册特殊的处理器(也叫 机关(traps)),当对这个代理实施各种操作时被调用。这些处理器除了将操作 传送 到原本的目标/被包装的对象上之外,还有机会运行额外的逻辑。
一个这样的 机关 处理器的例子是,你可以在一个代理上定义一个拦截[[Get]]
操作的get
—— 它在当你试图访问一个对象上的属性时运行。考虑如下代码:
var obj = { a: 1 },
handlers = {
get(target,key,context) {
// 注意:target === obj,
// context === pobj
console.log( "accessing: ", key );
return Reflect.get(
target, key, context
);
}
},
pobj = new Proxy( obj, handlers );
obj.a;
// 1
pobj.a;
// accessing: a
// 1
我们将一个get(..)
处理器作为 处理器 对象的命名方法声明(Proxy(..)
的第二个参数值),它接收一个指向 目标 对象的引用(obj
),属性的 键 名称("a"
),和self
/接受者/代理本身(pobj
)。
在追踪语句console.log(..)
之后,我们通过Reflect.get(..)
将操作“转送”到obj
。我们将在下一节详细讲解Reflect
API,但要注意的是每个可用的代理机关都有一个相应的同名Reflect
函数。
这些映射是故意对称的。每个代理处理器在各自的元编程任务实施时进行拦截,而每个Reflect
工具将各自的元编程任务在一个对象上实施。每个代理处理器都有一个自动调用相应Reflect
工具的默认定义。几乎可以肯定你将总是一前一后地使用Proxy
和Reflect
。
这里的列表是你可以在一个代理上为一个 目标 对象/函数定义的处理器,以及它们如何/何时被触发:
-
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(..)
,Reflect.setPrototypeOf(..)
,或__proto__
) -
preventExtensions(..)
:通过[[PreventExtensions]]
使代理成为不可扩展的(Object.preventExtensions(..)
或Reflect.preventExtensions(..)
) -
isExtensible(..)
:通过[[IsExtensible]]
,检测代理的可扩展性(Object.isExtensible(..)
或Reflect.isExtensible(..)
) -
ownKeys(..)
:通过[[OwnPropertyKeys]]
,取得一组代理的直属属性和/或直属symbol属性(Object.keys(..)
,Object.getOwnPropertyNames(..)
,Object.getOwnSymbolProperties(..)
,Reflect.ownKeys(..)
,或JSON.stringify(..)
) -
enumerate(..)
:通过[[Enumerate]]
,为代理的可枚举直属属性及“继承”属性请求一个迭代器(Reflect.enumerate(..)
或for..in
) -
has(..)
:通过[[HasProperty]]
,检测代理是否拥有一个直属属性或“继承”属性(Reflect.has(..)
,Object#hasOwnProperty(..)
,或"prop" in obj
)
提示: 关于每个这些元编程任务的更多信息,参见本章稍后的“Reflect
API”一节。
关于将会触发各种机关的动作,除了在前面列表中记载的以外,一些机关还会由另一个机关的默认动作间接地触发。举例来说:
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
);
}
},
proxy = new Proxy( {}, handlers );
proxy.a = 2;
// getOwnPropertyDescriptor
// defineProperty
在设置一个属性值时(不管是新添加还是更新),getOwnPropertyDescriptor(..)
和defineProperty(..)
处理器被默认的set(..)
处理器触发。如果你还定义了你自己的set(..)
处理器,你或许对context
(不是target
!)进行了将会触发这些代理机关的相应调用。
代理的限制
这些元编程处理器拦截了你可以对一个对象进行的范围很广泛的一组基础操作。但是,有一些操作不能(至少是还不能)被用于拦截。
例如,从pobj
代理到obj
目标,这些操作全都没有被拦截和转送:
var obj = { a:1, b:2 },
handlers = { .. },
pobj = new Proxy( obj, handlers );
typeof obj;
String( obj );
obj + "";
obj == pobj;
obj === pobj
也许在未来,更多这些语言中的底层基础操作都将是可拦截的,那将给我们更多力量来从JavaScript自身扩展它。
警告: 对于代理处理器的使用来说存在某些 不变量 —— 它们的行为不能被覆盖。例如,isExtensible(..)
处理器的结果总是被强制转换为一个boolean
。这些不变量限制了一些你可以使用代理来自定义行为的能力,但是它们这样做只是为了防止你创建奇怪和不寻常(或不合逻辑)的行为。这些不变量的条件十分复杂,所以我们就不再这里全面阐述了,但是这篇博文(http://www.2ality.com/2014/12/es6-proxies.html#invariants)很好地讲解了它们。
可撤销的代理
一个一般的代理总是包装着目标对象,而且在创建之后就不能修改了 —— 只要保持着一个指向这个代理的引用,代理的机制就将维持下去。但是,可能会有一些情况你想要创建一个这样的代理:在你想要停止它作为代理时可以被停用。解决方案就是创建一个 可撤销代理:
var obj = { a: 1 },
handlers = {
get(target,key,context) {
// 注意:target === obj,
// context === pobj
console.log( "accessing: ", key );
return target[key];
}
},
{ proxy: pobj, revoke: prevoke } =
Proxy.revocable( obj, handlers );
pobj.a;
// accessing: a
// 1
// 稍后:
prevoke();
pobj.a;
// TypeError
一个可撤销代理是由Proxy.revocable(..)
创建的,它是一个普通的函数,不是一个像Proxy(..)
那样的构造器。此外,它接收同样的两个参数值:目标 和 处理器。
与new Proxy(..)
不同的是,Proxy.revocable(..)
的返回值不是代理本身。取而代之的是,它返回一个带有 proxy 和 revoke 两个属性的对象 —— 我们使用了对象解构(参见第二章的“解构”)来将这些属性分别赋值给变量pobj
和prevoke
。
一旦可撤销代理被撤销,任何访问它的企图(触发它的任何机关)都将抛出TypeError
。
一个使用可撤销代理的例子可能是,将一个代理交给另一个存在于你应用中、并管理你模型中的数据的团体,而不是给它们一个指向正式模型对象本身的引用。如果你的模型对象改变了或者被替换掉了,你希望废除这个你交出去的代理,以便于其他的团体能够(通过错误!)知道要请求一个更新过的模型引用。
使用代理
这些代理处理器带来的元编程的好处应当是显而易见的。我们可以全面地拦截(而因此覆盖)对象的行为,这意味着我们可以用一些非常强大的方式将对象行为扩展至JS核心之外。我们将看几个模式的例子来探索这些可能性。
代理前置,代理后置
正如我们早先提到过的,你通常将一个代理考虑为一个目标对象的“包装”。在这种意义上,代理就变成了代码接口所针对的主要对象,而实际的目标对象则保持被隐藏/被保护的状态。
你可能这么做是因为你希望将对象传递到某个你不能完全“信任”的地方去,如此你需要在它的访问权上强制实施一些特殊的规则,而不是传递这个对象本身。
考虑如下代码:
var messages = [],
handlers = {
get(target,key) {
// 是字符串值吗?
if (typeof target[key] == "string") {
// 过滤掉标点符号
return target[key]
.replace( /[^\w]/g, "" );
}
// 让其余的东西通过
return target[key];
},
set(target,key,val) {
// 仅设置唯一的小写字符串
if (typeof val == "string") {
val = val.toLowerCase();
if (target.indexOf( val ) == -1) {
target.push(val);
}
}
return true;
}
},
messages_proxy =
new Proxy( messages, handlers );
// 在别处:
messages_proxy.push(
"heLLo...", 42, "wOrlD!!", "WoRld!!"
);
messages_proxy.forEach( function(val){
console.log(val);
} );
// hello world
messages.forEach( function(val){
console.log(val);
} );
// hello... world!!
我称此为 代理前置 设计,因为我们首先(主要、完全地)与代理进行互动。
我们在与messages_proxy
的互动上强制实施了一些特殊规则,这些规则不会强制实施在messages
本身上。我们仅在值是一个不重复的字符串时才将它添加为元素;我们还将这个值变为小写。当从messages_proxy
取得值时,我们过滤掉字符串中所有的标点符号。
另一种方式是,我们可以完全反转这个模式,让目标与代理交互而不是让代理与目标交互。这样,代码其实只与主对象交互。达成这种后备方案的最简单的方法是,让代理对象存在于主对象的[[Prototype]]
链中。
考虑如下代码:
var handlers = {
get(target,key,context) {
return function() {
context.speak(key + "!");
};
}
},
catchall = new Proxy( {}, handlers ),
greeter = {
speak(who = "someone") {
console.log( "hello", who );
}
};
// 让 `catchall` 成为 `greeter` 的后备方法
Object.setPrototypeOf( greeter, catchall );
greeter.speak(); // hello someone
greeter.speak( "world" ); // hello world
greeter.everyone(); // hello everyone!
我们直接与greeter
而非catchall
进行交互。当我们调用speak(..)
时,它在greeter
上被找到并直接使用。但当我们试图访问everyone()
这样的方法时,这个函数并不存在于greeter
。
默认的对象属性行为是向上检查[[Prototype]]
链(参见本系列的 this与对象原型),所以catchall
被询问有没有一个everyone
属性。然后代理的get()
处理器被调用并返回一个函数,这个函数使用被访问的属性名("everyone"
)调用speak(..)
。
我称这种模式为 代理后置,因为代理仅被用作最后一道防线。
"No Such Property/Method"
一个关于JS的常见的抱怨是,在你试着访问或设置一个对象上还不存在的属性时,默认情况下对象不是非常具有防御性。你可能希望为一个对象预定义所有这些属性/方法,而且在后续使用不存在的属性名时抛出一个错误。
我们可以使用一个代理来达成这种想法,既可以使用 代理前置 也可以 代理后置 设计。我们将两者都考虑一下。
var obj = {
a: 1,
foo() {
console.log( "a:", this.a );
}
},
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!";
}
}
},
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!
对于get(..)
和set(..)
两者,我们仅在目标对象的属性已经存在时才转送操作;否则抛出错误。代理对象应当是进行交互的主对象,因为它拦截这些操作来提供保护。
现在,让我们考虑一下反过来的 代理后置 设计:
var handlers = {
get() {
throw "No such property/method!";
},
set() {
throw "No such property/method!";
}
},
pobj = new Proxy( {}, handlers ),
obj = {
a: 1,
foo() {
console.log( "a:", this.a );
}
};
// 让 `pobj` 称为 `obj` 的后备
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!
在处理器如何定义的角度上,这里的 代理后置 设计相当简单。与拦截[[Get]]
和[[Set]]
操作并仅在目标属性存在时转送它们不同,我们依赖于这样一个事实:不管[[Get]]
还是[[Set]]
到达了我们的pobj
后备对象,这个动作已经遍历了整个[[Prototype]]
链并且没有找到匹配的属性。在这时我们可以自由地、无条件地抛出错误。很酷,对吧?