原文链接: https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20&%20beyond/ch7.md
参考链接: http://es6.ruanyifeng.com/#docs/symbol
元编程是一种操作程序本身的编程方式。换句话来说,就是编写操作代码的代码,听起来是不是很酷?
举个例子,如果你想去探测一个对象 a
和另一个对象 b
他们的是不是在一条原型链上?可以用 a.isPrototypeOf(b)
,这通常为认为是一种元编程的方式—自省。宏(Javascript 还没有宏)是另外一种在那些编译型语言中的元编程的方式。用 for..in
来迭代遍历一个对象的 key
,或者检查一个对象是不是一个 class constructor
的实例,这些都是另外一些元编程需要做的事情。
元编程关注以下这些规定:
元编程的目标是利用语言自己本身的能力更好的让你的代码变得更加的可描述,有表现力和灵活。正是因为元编程的元性质,所以找一个更精确的定义给它是一件比较难的事情。理解元编程的最好的方法,我觉得就是通过一个个例子。
ES6
为 Javascript 在原有的基础之上增加了一些新的特性来更好的支持元编程。
在有的时候,你的代码也许想要检查自己并想知道一些函数的名字是什么。如果你询问一个函数的名字是什么,答案会是出奇的模糊的。我们细想一下:
function daz() {} var obj = { foo: function() { }, bar: function baz() { }, bam: daz, zim() { } };
在上面的代码段中,想想看这么几个问题?
obj.foo()
的名字是什么?是 "foo"
, ""
, underfined
? obj.bar()
的名字是什么?是 "bar"
或者 "baz"
? obj.bam()
的名字是什么?是 "bam"
或者 "daz"
? obj.zim()
的名字是什么? 除此之外,还有那些作为回调函数传递的函数呢?
function foo(cb) { // 这里的`cb()`的名字是什么? } foo( function(){ // 我是匿名的! } );
在程序中,我们有相当多的方法让那些函数名模糊的函数变得具有可描述性。
还有更重要的是,我们需要区分函数的名字( "name" of a function
)是不是来自于函数自己的 "name"
属性,或者说它来自于它的词法约束名,比如 "bar"
和 function 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"
如果我们强制给定词法绑定名的话,像这样
var abc = function def() { // .. } abc.name; // "def"
那么 name
属性的值将会理所当然的是 "def"
。在没有缺少词法绑定名的情况下,直观的 "abc"
看起来才是适当的 name
的值。
下面是在 ES6
中的其他形式的声明的函数推断出来 name
属性的值的结果:
(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
看起来像一个对象了)。不过,在函数被构造调用(一个函数或者方法被 new
调用)的时候的内部用了 new.target
, 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
这里有一些细小的差别,在父类中的构造函数是被实际赋予了词法绑定名的。
需要注意的是:如同所有的元编程技巧,要小心创建你未来的自己或他人维护你的代码,让其是能够被理解的,谨慎使用这些编程方法。
在第二章的 "Symbols"
部分中,我们了解了 ES6
中新的原始类型 symbol
,除此之外,你可以在自己的程序中定义 symbols
,Javascript 预定义了些内置的 symbols
。
这些内置的 symbols
主要是为了暴露一些特殊的元属性来让你对 Javascript 的行为有更多的控制权。
我们来简单介绍和讨论下它们的用处。
Symbol.iterator
在第二章和第三种中,我们已经介绍并使用过了 @@iterator
,它会在展开运算符 ...
和 for..of
循环中被自动使用。同时我们也看到了 @@iterator
也被 ES6 作为一个新的部分所添加。
Symbol.iterator
指向该对象的默认遍历器方法,即该对象进行 for..of
循环时,会调用这个方法。
然而,我们还可以通过 Symbol.iterator
为任何对象定义自己想要的的迭代逻辑,甚至去重写默认的迭代器。元编程就是我们去定义 Javascript 的那些操作运算循环结构的行为。
看下这个例子:
var arr = [4,5,6,7,8,9]; for (var v of arr) { console.log( v ); } // 4 5 6 7 8 9 // define iterator that only produces values // from odd indexes 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()
和 instance of
比如:
function Foo() {} var a = new Foo(); a.toString(); // [object Object] a instanceof Foo; // true
从 ES6 开始,你能够控制 toString()
和 instance of
的行为:
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
对象的 Symbol.toStringTag
属性,指向一个方法。在该对象上面调用 Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在 toString
方法返回的字符串之中,表示对象的类型。
对象的 Symbol.hasInstance
属性,指向一个内部方法。当其他对象使用 instance of
运算符,判断是否为该对象的实例时,会调用这个方法。
Symbol.species
在第三章中,我们介绍过了 @@species
,它指向一个方法,该对象作为构造函数创造实例时,会调用这个方法。
我们可以通过 Symbol.species
来改写构造函数默认行为:
class Cool { // Symbol.species属性默认的读取器 static get [Symbol.species]() { return this; } again() { return new this.constructor[Symbol.species](); } } class Fun extends Cool {} class Awesome extends Cool { // 强制将子类的构造行为变成父类的 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
如果 this.constructor[Symbol.species]
存在,就会使用这个属性作为构造函数,来创造新的实例对象。
如果你想定义一个方法生成一个新的实例,你应该使用 new this.constructor[Symbol.species](..)
,而不是 new this.constructor(..)
或者 new XYZ(..)
,这样子类是能通过 Symbol.species
来控制构造函数能生产你想要你的实例。
Symbol.toPrimitive
在类型和语法系列中,我们讨论过一些类型的显式隐式转换,在 ES6 之前,我们是没有办法去控制变量类型转换的行为的。
从 ES6 开始,对象的 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") { // sum all numbers return this.reduce( function(acc,curr){ return acc + curr; }, 0 ); } }; arr + 10; // 25 更改后的效果
Symbol.toPrimitive
被调用时,会接受一个字符串 hint
参数,表示当前运算的模式,一共有三种模式。
Number
:该场合需要转成数值 String
:该场合需要转成字符串 Default
:该场合可以转成数值,也可以转成字符串 let obj = { [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return 123; case 'string': return 'str'; case 'default': return 'default'; default: throw new Error(); } } }; 2 * obj // 246 3 + obj // '3default' obj === 'default' // true String(obj) // 'str'
上面的例子想告诉我们, +
运算符是 default
模式, *
运算符是 数值模式
, String(..)
是字符串模式。
注意: ==
运算符是 default
模式, ===
运算符不会命中任何模式。
Regular Expression Symbols
下面是4个关于正则表达式对象的内置 Symbols
,它们能够控制字符串原型上的4个同名方法:
对象的 Symbol.match
属性,指向一个函数。当执行 str.match(myObject)
时,如果该属性存在,会调用它,返回该方法的返回值。
String.prototype.match(regexp) // 等同于 regexp[Symbol.match](this) class MyMatcher { [Symbol.match](string) { return 'hello world'.indexOf(string); } } 'e'.match(new MyMatcher()) // 1
对象的 Symbol.replace
属性,指向一个方法,当该对象被 String.prototype.replace
方法调用时,会返回该方法的返回值。
String.prototype.replace(searchValue, replaceValue) // 等同于 searchValue[Symbol.replace](this, replaceValue)
对象的 Symbol.search
属性,指向一个方法,当该对象被 String.prototype.search
方法调用时,会返回该方法的返回值。
String.prototype.search(regexp) // 等同于 regexp[Symbol.search](this) class MySearch { constructor(value) { this.value = value; } [Symbol.search](string) { return string.indexOf(this.value); } } 'foobar'.search(new MySearch('foo')) // 0
对象的 Symbol.split
属性,指向一个方法,当该对象被 String.prototype.split
方法调用时,会返回该方法的返回值。
String.prototype.split(separator, limit) // 等同于 separator[Symbol.split](this, limit)
注意:覆盖内置的正则表达式算法,你需要十分的小心,因为 Javascript 自己的正则表达式引擎已经是高度优化过了,如果你使用自己的代码,可能会慢很多。元编程虽然强大,但是你需要在真的有必要的情况和油漆的情况下使用。
Symbol.isConcatSpreadable
对象的 Symbol.isConcatSpreadable
属性等于一个布尔值,表示该对象使用 Array.prototype.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
对象的 Symbol.unscopables
属性,指向一个对象。该对象指定了使用 with
关键字时,哪些属性会被 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 }
true
意味着对象上的这个属性会被 with
排除,因此从词法返回变量中过滤掉,由 with
环境外的值代替。 false
则相反。
注意:在 Javascript 的 strict
模式下, with
是被禁用的。因此我们考虑不要用它。在作用域和闭包章节,我们知道了,应该避免去使用 with
,所以 Symbol.unscopables
也就变得无意义了。
在 ES6
中一个最明显的为元编程提供的特性就是 Proxy
了。
Proxy
是为其他的一些普通的对象最一层拦截,你能够其上注册一些特殊的回调,外界对该对象的访问,都会通过这层拦截,并触发回调。因此提供了一种机制,可以对外界的访问进行过滤和改写,为目标对象添加自己额外的逻辑。
下面这个例子,就是在对象的一个属性的 get
进行拦截:
var obj = { a: 1 }; var handlers = { get(target,key,context) { // note: 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
我们声明 handlers
,并给它定义了一个 get
方法(其实 handlers
就是 Proxy()
的第二个参数),它接受到目标对象 obj
,关键属性 a
和代理对象 pobj
。
通过 Reflect.get()
,我们不仅执行了 console.log()
,还进入了 obj
中,对属性 a
就行了访问。我们将在下介绍 Reflect
,但是请注意,每一个可用的代理都会有一个对应的同名的反射函数。
这些同名的设计的映射是故意而为之的,执行一个相应的元编程的时候,代理对其进行拦截,同时反射在每个对象上执行元编程任务。每个代理都有其默认的定义,对应的反射会被自动调用。在绝大多数情况下,你是会同时用到代理和反射的。
下面是 Proxy
支持的拦截操作一览。
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
get(target, propKey, receiver)
拦截对象属性的读取,比如 proxy.foo
和 proxy['foo']
、 Reflect.get(..)
,返回类型不限。最后一个参数 receiver
可选,当 target
对象设置了 propKey
属性的 get
函数时, receiver
对象会绑定 get
函数的 this
对象。
set(target, propKey, value, receiver)
拦截对象属性的设置,比如 proxy.foo = v
或 proxy['foo'] = v
、 Reflect.set(..)
,返回一个布尔值。
has(target, propKey)
拦截 propKey in proxy
、 Reflect.has(..)
的操作,返回一个布尔值。
deleteProperty(target, propKey)
拦截 delete proxy[propKey]
、 Reflect.deleteProperty(..)
的操作,返回一个布尔值。
enumerate(target)
拦截 for (var x in proxy)
、 Reflect.enumerate(..)
,返回一个遍历器。
ownKeys(target)
拦 Object.getOwnPropertyNames(proxy)
、 Object.getOwnPropertySymbols(proxy)
、 Object.keys(proxy)
、 Reflect.get()
、 Reflect.ownKeys(..)
,返回一个数组。该方法返回对象所有自身的属性,而 Object.keys()
仅返回对象可遍历的属性。
getOwnPropertyDescriptor(target, propKey)
拦截 Object.getOwnPropertyDescriptor(proxy, propKey)
、 Reflect.getOwnPropertyDescriptor(..)
,返回属性的描述对象。
defineProperty(target, propKey, propDesc)
拦截 Object.defineProperty(proxy, propKey, propDesc
)、 Object.defineProperties(proxy, propDescs)
、 Reflect.defineProperty(..)
,返回一个布尔值。
preventExtensions(target)
拦截 Object.preventExtensions(proxy)
、 Reflect.preventExtensions(..)
,返回一个布尔值。
getPrototypeOf(target)
拦 Object.getPrototypeOf(proxy)
、 Reflect.preventExtensions(..)
、 __proto__
、 Object.isPrototypeOf()
、 instanceof
,返回一个对象。
isExtensible(target)
拦截 Object.isExtensible(proxy)
、 Reflect.isExtensible(..)
,返回一个布尔值。
setPrototypeOf(target, proto)
拦截 Object.setPrototypeOf(proxy, proto)
、 Reflect. setPrototypeOf(..)
,返回一个布尔值。
apply(target, object, args)
拦截 Proxy
实例作为函数调用的操作,比如 proxy(...args)
、 proxy.call(object, ...args)
、 proxy.apply(...)
、 Reflect.apply(..)
。
construct(target, args, proxy)
拦截Proxy实例作为构造函数调用的操作,比如 new proxy(...args)
、 Reflect.construct(..)
。
注意:需要更多以上的相关信息,需要你看接下来的 Reflect
章节。
除了以上列表给出的触发动作以外,还有些拦截可能是通过其他其他动作间接触发。比如:
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()
的拦截,你可能会也可能不会使用上下文中的变量,这将触发这些代理(自己定义 set()
后,不触发了。)
有些操作是不属于可拦截的。比如,对象的运算操作是不能被拦截的:
var obj = { a:1, b:2 }, handlers = { .. }, pobj = new Proxy( obj, handlers ); typeof obj; String( obj ); obj + ""; obj == pobj; obj === pobj
也许在未来,Javascript 能给我们这样的权利。
Proxy
这些代理处理程序的元编程的好处应该是显而易见的。几乎完全拦截我们可以(和覆盖)对象的行为,这意味着我们可以在一些非常强大的方式超出核心 Javascript 对象行为。我们来看看几个例子模式探索的可能性。
正如我们前面所提到的,通常认为代理就是“包装”的目标对象。在这个意义上,代理成为主对象的代码的接口,为实际的目标对象保持隐藏/保护。
有时参数接受变量,不一定是值得信赖,所以你需要强制执行一些特殊的规则。比如:
var messages = [], handlers = { get(target,key) { // string value? if (typeof target[key] == "string") { // filter out punctuation return target[key] .replace( /[^/w]/g, "" ); } // pass everything else through return target[key]; }, set(target,key,val) { // only set unique strings, lowercased if (typeof val == "string") { val = val.toLowerCase(); if (target.indexOf( val ) == -1) { target.push(val); } } return true; } }, messages_proxy = new Proxy( messages, handlers ); // elsewhere: 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
,具体逻辑请阅读上面例子的注释。
或者,我们可以完全反转这个模式。当目标对象影响代理而不是代理影响目标对象的时候,代码真的只与主对象互相影响了。做到这一点最简单的方案是要在主要对象的 [[Prototype]]
链中代理对象。
比如:
var handlers = { get(target,key,context) { return function() { context.speak(key + "!"); }; } }, catchall = new Proxy( {}, handlers ), greeter = { speak(who = "someone") { console.log( "hello", who ); } }; // setup `greeter` to fall back to `catchall` Object.setPrototypeOf( greeter, catchall ); greeter.speak(); // hello someone greeter.speak( "world" ); // hello world greeter.everyone(); // hello everyone!
我们直接和 greeter
而不是和 catchall
交互。当我们调用 speak()
,它是在 greeter
上发现被直接调用的。但是,当我们试图去调用像 everyone()
的方法的时候,改方法其实是不存在的。
默认的对象属性会去查找 [[Prototype]]
链,所以 catchall
会去参看对象上的每一个属性。代理的 get()
会去调用 speak()
,所以才有 hello everyone!
这一结果。
我称这种设计为 代理后置 ,这是使用代理的最后手段。
关于 Javascipt 常见的抱怨是:设置不存在的属性的时候是没有报错的。你可能希望预定义所有的对象属性和方法,并且如果有一个不存在的属性名随后使用抛出一个错误。
我们可以通过使用 Proxy
来实现,代理的前置后置都可以,比如:
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()
的代理,我们可以在真正的操作之前去判断对象的属性是否存在,如果是不存在的属性,就抛出错误。代理对象 pobj
是主要的与代码交互的对象,它为真是的对象提供了保护。
现在,我们看下代理后置:
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 ); } }; // setup `obj` to fall back to `pobj` 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()
去模拟或者拓展原型机制的概念。
我们第一个要改变的事情是去考虑创建两个通过原型循环链接的对象(或者,至少它们看起来是这样的。)你不能真正创造一个真正的双向循环的原型链,因为 Javascript 的引擎会抛出错误。但是 Proxy
可以伪造!
var handlers = { get(target,key,context) { if (Reflect.has( target, key )) { return Reflect.get( target, key, context ); } // fake circular `[[Prototype]]` else { return Reflect.get( target[ Symbol.for( "[[Prototype]]" ) ], key, context ); } } }, obj1 = new Proxy( { name: "obj-1", foo() { console.log( "foo:", this.name ); } }, handlers ), obj2 = Object.assign( Object.create( obj1 ), { name: "obj-2", bar() { console.log( "bar:", this.name ); this.foo(); } } ); // fake circular `[[Prototype]]` link obj1[ Symbol.for( "[[Prototype]]" ) ] = obj2; obj1.bar(); // bar: obj-1 <-- through proxy faking [[Prototype]] // foo: obj-1 <-- `this` context still preserved obj2.foo(); // foo: obj-2 <-- through [[Prototype]]
注意:在这个例子中,我们不需要使用 set()
代理,我们想让例子变得简单。如果为了完全模仿原型链机制,你可能还要去实现一个 set()
,搜索原型链相匹配的属性并遵守对象属性的描述法行为。
在上面的代码片段中, obj2
是通过以 obj1
为原型创建的。但要创建反向的联动的,我们就需要在 obj1
的属性上创建一个 Symbol.for("[[Prototype]]")
,这个 symbol
的设置看似十分的黑魔法,但事实并非如此,它只是让程序能方便的和要执行的任务所关联。
然后,代理的 get()
首先去查找,先判断目标对象上有没有该 key
,如果没有,就手动切换到存储在 Symbol.for("[[Prototype]]")
,接下来的任务就交给 Symbol.for("[[Prototype]]")
的值了。
这种模式的一个重要的优点是, obj1
和 obj2
的定义基本是不通过它们之间的这种循环关系侵入。虽然上面的代码为了说明白道理而有些琐碎,但如果你仔细观察,代理的处理程序逻辑是通用的(并不需要明确的 obj1
和 obj2
)。所以这些逻辑完全可以抽象出来,写成一个可以叫 setCircularPrototypeOf(..)
的函数,我们把这个函数的完成读者。
现在,我们已经看到了如何通过代理 get()
去模拟原型链,让我们去想的更加远一点,那一个对象有多个原型(又名“多重继承”)?实现起来相当简单:
var obj1 = { name: "obj-1", foo() { console.log( "obj1.foo:", this.name ); }, }, obj2 = { name: "obj-2", foo() { console.log( "obj2.foo:", this.name ); }, bar() { console.log( "obj2.bar:", this.name ); } }, handlers = { get(target,key,context) { if (Reflect.has( target, key )) { return Reflect.get( target, key, context ); } // fake multiple `[[Prototype]]` else { for (var P of target[ Symbol.for( "[[Prototype]]" ) ]) { if (Reflect.has( P, key )) { return Reflect.get( P, key, context ); } } } } }, obj3 = new Proxy( { name: "obj-3", baz() { this.foo(); this.bar(); } }, handlers ); // fake multiple `[[Prototype]]` links obj3[ Symbol.for( "[[Prototype]]" ) ] = [ obj1, obj2 ]; obj3.baz(); // obj1.foo: obj-3 // obj2.bar: obj-3
注意:和前面在实现循环原型链中提到的一样,我们没有去实现 set()
相关的处理,模拟 [[Set]]
的动作会很复杂。
obj3
被设置看似是2个原型。在 obj3.baz()
中,会去先去请求 obj1.foo()
(因为先到先得,我们 obj3[ Symbol.for("[[Prototype]]" )]
数组中的第一个值是 obj1
)。如果我们改变顺序,让 obj2
排在第一个,可能结果就不一样了。
但是,如果在 obj1
中,没有找到 bar()
,它就会去 obj2
去查找了。
obj1
和 obj2
代表了 obj3
的两个平行的原型链。 obj1
和 obj2
本身可能还有自己正常的原型,也可以像 obj3
那样是模拟的原型,可多委托。
正如前面的循环原型链一样,多重继承一样可以抽象出一个可以叫 setPrototypesOf(..)
的函数(注意这里有个“S”),这个任务同样留个读者吧!
Reflect
Reflect
对象和是一个普通的对象(像 Math
),而不是一个内置函数或者构造器。
它拥有对应的可以控制各种元编程任务的静态方法。这些功能和 Proxy
一一对应。
下面的这些名称你可能看起来很眼熟(因为他们也是 Object
上的方法):
Reflect.getOwnPropertyDescriptor(..)
Reflect.defineProperty(..)
Reflect.getPrototypeOf(..)
Reflect.setPrototypeOf(..)
Reflect.preventExtensions(..)
Reflect.isExtensible(..)
这些方法和在 Object
上的同名方法一样。然后,一个区别在于, Object
上这么方法的第一个参数是一个对象, Reflect
遇到这种情况会扔出一个错误。
补充:
Reflect
对象与 Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。 Reflect
对象的设计目的有这样几个。
Object
对象的一些明显属于语言内部的方法(比如 Object.defineProperty
),放到 Reflect
对象上。现阶段,某些方法同时在 Object
和 Reflect
对象上部署,未来的新方法将只部署在 Reflect
对象上。 Object
方法的返回结果,让其变得更合理。比如, Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc)
则会返回 false
。 Object
操作都变成函数行为。某些 Object
操作是命令式,比如 name in obj
和 delete obj[name]
,而 Reflect.has(obj, name)
和 Reflect.deleteProperty(obj, name)
让它们变成了函数行为。 Reflect
对象的方法与 Proxy
对象的方法一一对应,只要是 Proxy
对象的方法,就能在 Reflect
对象上找到对应的方法。这就让 Proxy
对象可以方便地调用对应的 Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy
怎么修改默认行为,你总可以在 Reflect
上获取默认行为。 一个对象的键们可以这样被访问:
Reflect.ownKeys(..)
:返回对象自己的键(不是通过“继承”的),同样也返回 Object.getOwnPropertyNames()
和 Object.getOwnPropertySymbols()
Reflect.enumerate(..)
:返回对象上可枚举的属性(包括“继承”过来的)。 Reflect.has(..)
:和 in
操作符差不多。 函数的调用和构造调用可以手动通过下面 API 执行:
- Reflect.apply(..)
:举个例子, Reflect.apply(foo,thisObj,[42,"bar"])
调用了 foo()
函数,上下文是 thisObj
,参数为 42
和 bar
。
- Reflect.construct(..)
:举个例子, Reflect.construct(foo,[42,"bar"])
等于 new foo(42,"bar")
。
对象属性访问,设置,删除可以手动通过下面 API 执行:
Reflect.get()
: Reflect.get(o,"foo")
等于 o.foo
。 Reflect.set()
: Reflect.set(o,"foo",42)
等于 o.foo = 42
。 Reflect.deleteProperty()
: Reflect.deleteProperty(o,"foo")
等于 delete o.foo
。 Reflect
的元编程能力可以让你等效的模拟以前隐藏的各种语法特性。这样你就可以使用这些功能为特定语言(DSL)拓展新功能和 API。
在 ES6 之前,规范里面没有定义如何列出顺序的列出对象和列出对象的性能,一般来说,是大多数 Javascript 的引擎来创建对象属性的属性的,因此软件开发者们强烈建议不用根据依赖它排的顺序。
到了 ES6,对象的可枚举的键的顺序的算法被规定。
这个规则是:
1. 首先,如果索引是整数,升序。
2. 其次,根据来根据索引 String
创建排序。
3. 最后,根据 Symbol
创建顺序。
例子:
var o = {}; o[Symbol("c")] = "yay"; o[2] = true; o[1] = true; o.b = "awesome"; o.a = "cool"; Reflect.ownKeys( o ); // [1,2,"b","a",Symbol(c)] Object.getOwnPropertyNames( o ); // [1,2,"b","a"] Object.getOwnPropertySymbols( o ); // [Symbol(c)]
如果还有原型上的键值呢?
var o = { a: 1, b: 2 }; var p = Object.create( o ); p.c = 3; p.d = 4; for (var prop of Reflect.enumerate( p )) { console.log( prop ); } // c d a b for (var prop in p) { console.log( prop ); } // c d a b JSON.stringify( p ); // {"c":3,"d":4} Object.keys( p ); // ["c","d"]
在 ES6 中, Reflect.ownKeys(..)
, Object.getOwnPropertyNames(..)
, Object.getOwnPropertySymbols(..)
的顺序是可靠可预测的,因此它是安全的。
Reflect.enumerate(..)
, Object.keys(..)
, for..in
和 JSON.stringify(..)
继续共享一个可观察的排序,但是排序不一定和 Reflect.ownKeys(..)
,所以我们任然需要小心。