lishengzxc / bblog

My Blog
https://github.com/lishengzxc/bblog/issues
178 stars 8 forks source link

【翻译】You Don't Know JS: Meta Programming #4

Open lishengzxc opened 8 years ago

lishengzxc commented 8 years ago

Javascript 元编程

原文链接: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() { }
};

在上面的代码段中,想想看这么几个问题?

除此之外,还有那些作为回调函数传递的函数呢?

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.targetnew变成了一个虚拟的上下文,因此那个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

在第二章的"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.toStringTagSymbol.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参数,表示当前运算的模式,一共有三种模式。

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也就变得无意义了。

Proxies

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.fooproxy['foo']Reflect.get(..),返回类型不限。最后一个参数receiver可选,当target对象设置了propKey属性的get函数时,receiver对象会绑定get函数的this对象。

set(target, propKey, value, receiver)

拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = vReflect.set(..),返回一个布尔值。

has(target, propKey)

拦截propKey in proxyReflect.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()后,不触发了。)

Proxy 的局限

有些操作是不属于可拦截的。比如,对象的运算操作是不能被拦截的:

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]]")的值了。

这种模式的一个重要的优点是,obj1obj2的定义基本是不通过它们之间的这种循环关系侵入。虽然上面的代码为了说明白道理而有些琐碎,但如果你仔细观察,代理的处理程序逻辑是通用的(并不需要明确的obj1obj2)。所以这些逻辑完全可以抽象出来,写成一个可以叫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去查找了。

obj1obj2代表了obj3的两个平行的原型链。obj1obj2本身可能还有自己正常的原型,也可以像obj3那样是模拟的原型,可多委托。

正如前面的循环原型链一样,多重继承一样可以抽象出一个可以叫setPrototypesOf(..)的函数(注意这里有个“S”),这个任务同样留个读者吧!

使用 Reflect

Reflect对象和是一个普通的对象(像Math),而不是一个内置函数或者构造器。

它拥有对应的可以控制各种元编程任务的静态方法。这些功能和Proxy一一对应。

下面的这些名称你可能看起来很眼熟(因为他们也是Object上的方法):

这些方法和在Object上的同名方法一样。然后,一个区别在于,Object上这么方法的第一个参数是一个对象,Reflect遇到这种情况会扔出一个错误。

补充: Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

一个对象的键们可以这样被访问:

函数的调用和构造调用可以手动通过下面 API 执行:

-Reflect.apply(..):举个例子,Reflect.apply(foo,thisObj,[42,"bar"])调用了foo()函数,上下文是thisObj,参数为42bar
-Reflect.construct(..):举个例子,Reflect.construct(foo,[42,"bar"])等于new foo(42,"bar")

对象属性访问,设置,删除可以手动通过下面 API 执行:

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..inJSON.stringify(..)继续共享一个可观察的排序,但是排序不一定和Reflect.ownKeys(..),所以我们仍然需要小心。

END