兔兔的奶爸

ES6中的Metaprogramming: Symbols 为什么令人惊叹

兔兔的奶爸 · 2016-12-10翻译 · 1300阅读 原文链接 十年踪迹审校

你听说过es6对吧?它是 JavaScript 新版本,拥有很多令人惊叹的新特性。我常常兴高采烈地谈论我在 ES6 中发现的令人难以置信的新功能,这使很多同事感到懊恼 (似乎并不是每个人都喜欢别人占用自己的午餐休息时间来谈论 ES6 模块)。

ES6 提供的一组很不错的新功能,它带来了一系列新的元编程工具,提供了一组底层 hooks 代码的方法。并没有多少人写过关于这组新功能的文章,因此我想我可以写一篇比较细致的文章,分为 3 部分文章来讨论它们(顺便说一下:因为我懒癌晚期,这篇文章完成 90%之后,在我的草稿箱里躺了三个月,到今天发表出来的时候,网上已经有更多介绍这方面的文章):

第一部分: Symbols (这个帖子) (第二部分: Reflect | 第三部分: Proxies

元编程( Metaprogramming)

首先, 让我们快速浏览和发现的元编程的奇妙世界。笼统地说,元编程关心的是语言的内在机制,而不是“高级”数据建模或业务逻辑。如果编程可以被形容为为“编写程序”, 元编程就应该被描述为“编写‘能编写程序的程序’”。你也许每天都在使用元编程,只是没有注意到它。

元编程有一些“分支”,其一是代码生成 (Code Generation), 又名 eval , 它是我们的老朋友了,自从 JavaScript 诞生以来就存在 (JS 在ES1就有eval, 甚至在try/catchswitch 出现之前)。 几乎所有你今天可以使用的其他语言都有 Code Generation 功能。

元编程的另一分支是 反射 (Reflection) :找出并调整应用程序的语义和结构。JavaScript 有相当多与反射相关的方法。如Functions 中的 Function#nameFunction#length, 以及 Function#bindFunction#callFunction#apply. Object上的所有可用方法都是反射, 例如: Object.getOwnProperties (补充一点,不修改代码,但是收集有关代码信息的反射工具通常被称为 Introspection)。JavaScript 也有 Reflection/Introspection 操作符, 比如 typeofinstanceofdelete

反射是元编程中很酷的一部分,因为它允许你改变应用程序内部的工作原理。以 Ruby 举个例子,在 Ruby 中,你可以指定运算符作为方法,这一点允许你在使用特定类时重写这些运算符(有时称为“运算符重载”)。

class BoringClass
end
class CoolClass
  def ==(other_object)
   other_object.is_a? CoolClass
  end
end
BoringClass.new == BoringClass.new #=> false
CoolClass.new == CoolClass.new #=> true!

与其他语言(如Ruby或Python)相比,Javascript的元编程特性是不够高级的,尤其是在运算符重载等方面,但 ES6 开始在这方面有所改进。

ES6 中的元编程

ES6 新增了三种 API:SymbolReflectProxy。 第一眼看可能有点迷惑,三个相互独立的 API 都用于元编程的么?但是当你分开看每一个的时候,确实会让你有这种感觉。

  • Symbols 用于实现的反射(Reflection within implementation) -- 把它们用在你现有的类和对象上来改变行为。

  • Reflect 用于通过 introspection 来实现反射(Reflection through introspection) -- 用于发现你代码中非常底层的信息。

  • Proxy 用于通过代理来实现反射(Reflection through intercession) -- 包装对象和通过“拦截器”来获取他们的行为。

所以它们是怎样工作的呢?它们怎么有用?这篇文章将讲述Symbols,而其余的两篇文章将分别讲述ReflectProxy

Symbols - 实现的反射

Symbols 是 JavaScript 新的基本类型. 就像 Number, String, 和 Boolean 类型, Symbols 用 Symbol方法来创建。不同于其他基本类型,Symbols 没有字面量语法(例如 String 有 '...'、Array 有 [...]、Object 有 {...})。创建 Symbols 唯一的使用方法是使用 Symbol 这个不是构造函数的构造函数。

Symbol(); // symbol
console.log(Symbol()); // prints "Symbol()" to the console
assert(typeof Symbol() === 'symbol')
new Symbol(); // TypeError: Symbol is not a constructor

Symbols 具有内置的可调试信息

Symbols 可以给出一个描述,这实际上只是用于调试,这样打印日志到控制台会使开发更容易。

console.log(Symbol('foo')); // prints "Symbol(foo)" to the console.
assert(Symbol('foo').toString() === 'Symbol(foo)');

Symbols 可以作为 Object 的 keys 使用

这是 Symbols 真正有趣的地方。Symbols与对象密切相关。 他们可以作为对象的 key(类似于字符串可以作为对象的 key),这意味着你可以分配任意多个唯一的 Symbols 到一个对象,并且这些 Symbols 不会与字符串键或其他唯一符号冲突。

var myObj = {};
var fooSym = Symbol('foo');
var otherSym = Symbol('bar');
myObj['foo'] = 'bar';
myObj[fooSym] = 'baz';
myObj[otherSym] = 'bing';
assert(myObj.foo === 'bar');
assert(myObj[fooSym] === 'baz');
assert(myObj[otherSym] === 'bing');

此外, Symbols 不会在使用 for infor of 或者 Object.getOwnPropertyNames时被展示出来,在对象中获取 Symbols 的唯一方法是Object.getOwnPropertySymbols

var fooSym = Symbol('foo');
var myObj = {};
myObj['foo'] = 'bar';
myObj[fooSym] = 'baz';
Object.keys(myObj); // -> [ 'foo' ]
Object.getOwnPropertyNames(myObj); // -> [ 'foo' ]
Object.getOwnPropertySymbols(myObj); // -> [ Symbol(foo) ]
assert(Object.getOwnPropertySymbols(myObj)[0] === fooSym);

这意味着 Symbol 给对象带来一个全新用法,它们为对象提供了一种隐藏属性,不会被迭代出来,不能使用已有的反射工具获取,也不与对象中的其他属性冲突!

Symbols 是完全唯一的

默认情况下,每个新的 Symbol 具有完全唯一的值。如果你创建了一个 Symbol(var mysym = Symbol()),它会在 JavaScript引擎创建一个全新的值。如果你没有 Symbol 的引用,你就不能使用它。这也意味着两个symbols 将永远不会等于相同的值,即使他们有相同的描述。

assert.notEqual(Symbol(), Symbol());
assert.notEqual(Symbol('foo'), Symbol('foo'));
assert.notEqual(Symbol('foo'), Symbol('bar'));

var foo1 = Symbol('foo');
var foo2 = Symbol('foo');
var object = {
    [foo1]: 1,
    [foo2]: 2,
};
assert(object[foo1] === 1);
assert(object[foo2] === 2);

除非他们不是

嗯,这有一个小坑需要注意,因为还有另一种方法,可以很容易地获取和复用 Symbols,即:Symbol.for()。这个方法在全局符号注册表(global Symbol registry)下创 Symbol。顺便说一下:这个注册表是跨域的,这意味着来自 iframe 或者 service worker 的 Symbol 将与从当前 frame 生成的 Symbol 相同。

assert.notEqual(Symbol('foo'), Symbol('foo'));
assert.equal(Symbol.for('foo'), Symbol.for('foo'));

// Not unique:
var myObj = {};
var fooSym = Symbol.for('foo');
var otherSym = Symbol.for('foo');
myObj[fooSym] = 'baz';
myObj[otherSym] = 'bing';
assert(fooSym === otherSym);
assert(myObj[fooSym] === 'bing');
assert(myObj[otherSym] === 'bing');

// Cross-Realm
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
assert.notEqual(iframe.contentWindow.Symbol, Symbol);
assert(iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')); // true!

拥有全局 Symbols 会使事情更复杂,但好的方面是,我们会(容易)获取它。现在你们有些人可能会说“哎呀!?我怎么知道哪个 Symbols 是唯一的Symbols,哪个不是?对于这个问题,我觉得“没关系,不会有什么坏事情发生,因为我们有Symbol.keyFor()

var localFooSymbol = Symbol('foo');
var globalFooSymbol = Symbol.for('foo');

assert(Symbol.keyFor(localFooSymbol) === undefined);
assert(Symbol.keyFor(globalFooSymbol) === 'foo');
assert(Symbol.for(Symbol.keyFor(globalFooSymbol)) === Symbol.for('foo'));

Symbols 是什么,Symbols 不是什么

所以我们对于什么是 Symbols 有了一定的概念,并且了解了它是如何工作的。但同样重要的是知道对于怎样使用 Symbols 是好的,怎样使用是不好的(因为如果不深入了解的话,用错一样东西很容易):

  • Symbols永远不会与对象字符串属性名冲突。这让他们非常适合扩展你已经给出的对象(例如,作为函数的参数)而不会以明显的方式影响对象。

  • Symbols 不能使用已有的反射工具读取。你需要新的Object.getOwnPopertySymbols() 来存取一个对象的Symbols,这让Symbols 更好的存储一块你不想让别人通过正常操作得到的信息。在某些特殊情况下,使用Object.getOwnPropertySymbols()来获取 Symbols。

  • Symbols 不是私有的 。换句话说,所有的对象中的Symbols,都可以使用 Object.getOwnSymbols()获取到。所以别在对象中用 symbol 存储你想要真正私有的信息,因为这些信息还是可以被获取的。

  • 能使用像Object.assign这样的新方法把可枚举的 Symbols复制给其他对象 。如果你尝试调用 Object.assign(newObject, objectWithSymbols) ,第二个参数(objectWithSymbols)中所有的(可枚举)的Symbols 将被复制到第一个参数 (newObject)。如果你不想这种情况发生,用Object.defineProperty使它们不可枚举,

  • Symbols 不会被强制转换为原始值。如果你尝试转换一个Symbol变成原始值(+Symbol(), ''+Symbol(), Symbol() + 'foo'),将会抛出一个异常,这可以防止在将其设置为属性名称时意外对其进行字符串化。

  • Symbols 并不总是唯一。如前所述,Symbol.for() 返回一个非唯一的Symbol。不要总是假设你拥有的符号是独一无二的,除非是你自己创建的(确保不是通过 Symbol.for 创建的)。

  • Symbols 不同于 Ruby 的 Symbols。他们有一些相似之处,比如有一个统一的符号注册表,但仅此而已。别把他们当做 Ruby 的 Symbols 一样使用。

好吧,怎么使用 Symbols 才真正好呢

实际上,Symbols 只是稍微不同的方式来将属性添加到对象上。你可以轻松地使用内建的 Symbols 作为标准方法,就像Object.prototype.hasOwnProperty,它显示出从 Object 继承来的所有东西(基本上是一切)。事实上,其他语言,比如在 Python中:Symbol.iterator 等价于 __iter__Symbol.hasInstance 等价于 __instancecheck__,我猜Symbol.toPrimitive 大概与 __cmp__类似。Python 的方式可以说是一个比较糟糕的方法,而 JavaScript 因为有了 Symbols,不需要任何奇怪的语法,用户不用担心莫名其妙地与这些特殊方法冲突。

Symbols,在我看来,可以在以下两种情况使用:

1.作为一个唯一的值用在你原先可能得使用字符串或整数表示的地方:

假设你有一个日志类库,它包含许多种日志级别,比如logger.levels.DEBUGlogger.levels.INFOlogger.levels.WARN 等等。在 ES5的代码中,你需要使这些作为字符串(所以logger.levels.DEBUG ==='debug')或数字(logger.levels.DEBUG === 10)。这两个都不理想,因为这些值不是唯一的值,但Symbols 是!所以logger.levels 很容易改成这样:

log.levels = {
    DEBUG: Symbol('debug'),
    INFO: Symbol('info'),
    WARN: Symbol('warn'),
};
log(log.levels.DEBUG, 'debug message');
log(log.levels.INFO, 'info message');

2. 用在一个对象中需要存放元数据的地方

你也可以用它们来存储一些对实际对象来说不那么重要的自定义元数据属性。这可以被认为是一个额外的不可枚举的层(毕竟,不可枚举的键仍然出现在Object.getOwnProperty中)。下面让我们新建一个可信赖的Collection类,并且添加一个隐藏在Symbol语句中的 size 引用(只要记住 Symbols 不是私有的,你可以,而且应该,只在你不介意被其它应用改变的地方使用。)

var size = Symbol('size');
class Collection {
    constructor() {
        this[size] = 0;
    }

    add(item) {
        this[this[size]] = item;
        this[size]++;
    }

    static sizeOf(instance) {
        return instance[size];
    }

}

var x = new Collection();
assert(Collection.sizeOf(x) === 0);
x.add('foo');
assert(Collection.sizeOf(x) === 1);
assert.deepEqual(Object.keys(x), ['0']);
assert.deepEqual(Object.getOwnPropertyNames(x), ['0']);
assert.deepEqual(Object.getOwnPropertySymbols(x), [size]);
3. 赋予开发者通过你的 API 为他们的对象添加 hooks 的能力

好吧,这听起来可能有点怪异,但是听我说。让我们假设我们有一个console.log 样式的功能函数 ,这个函数可以接受任何对象,并将其记录到控制台。 对于如何在控制台中显示给定的对象,这个函数有自己的规则。 但作为一名使用这个API的开发人员,可以通过使用 inspect Symbol(检查常量) 在 hook 下提供一个方法来覆盖原有的规则:

// 从API的 Symbol 常量中获取一个 魔法  inspect Symbols
var inspect = console.Symbols.INSPECT;

var myVeryOwnObject = {};
console.log(myVeryOwnObject); // logs out {}

myVeryOwnObject[inspect] = function () { return 'DUUUDE'; };
console.log(myVeryOwnObject); // logs out DUUUDE

按照这个想法实现的inspect hook看起来可能像这样:

console.log = function (…items) {
    var output = '';
    for(const item of items) {
        if (typeof item[console.Symbols.INSPECT] === 'function') {
            output += item[console.Symbols.INSPECT](item);
        } else {
            output += console.inspect[typeof item](item);
        }
        output += '  ';
    }
    process.stdout.write(output + '\n');
}

澄清一下,这并是说你应该修改代码给对象提供什么扩展。这样肯定是不对的,为此,请查看WeakMaps,它可以提供辅助对象,以便您在对象上收集您自己的元数据。 Node.js 的 已经为console.log提供了类似的实现 。它使用了 String ('inspect') 而不是一个 Symbol,这意味着你可以设置 x.inspect = function(){},但这可能与你的类本身的方法发生冲突,并发生意外。因而使用 Symbols 是实现这种行为非常有效的一种方式。 这种使用 Symbols方 式的意义是深远的,以至于随着我们 Symbols 越来越了解,使之成为 javascript 语言的一部分。

内建的Symbols

让 Symbols 有用的关键是 JavaScript 提供了一组 Symbols 常量,称为“内建的 Symbols”。这些实际上是在 Symbol 类上的一组静态属性,它们在其他本地对象(如Arrays,Strings)和 JavaScript 引擎内部实现。这是真正的“实现的反射”部分,因为这些内建的 Symbols 改变了JavaScript 内部的行为。 下面我详细介绍他们都是干嘛的 ?为什么他们让人如此惊叹!

Symbol.hasInstance: instanceof

Symbol.hasInstance是一个驱动instanceof行为的 Symbol。当一个符合ES6的引擎在表达式中看到instanceof运算符时,它调用Symbol.hasInstance。例如:lho instanceof rho 将调用 rho[Symbol.hasInstance](lho)(这里的 rho 是right hand operand(右边的操作),而 lho 是 the left hand operand(左边的操作))。然后根据方法来确定它是否继承自该特定实例,你的实现可能像这样:

class MyClass {
    static [Symbol.hasInstance](lho) {
        return Array.isArray(lho);
    }
}
assert([] instanceof MyClass);

(上面代码中,MyClass是一个类,new MyClass()会返回一个实例。该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。)

Symbol.iterator

如果你听说过关于 Symbols 的事情,你可能听说过Symbol.iterator,在 ES6 带来了新的语法,for of 循环,它调用了右边操作的Symbol.iterator以获取值进行迭代,换句话说使用iteratorfor of 是等价的。

var myArray = [1,2,3];

//有 for of
for(var value of myArray) {
    console.log(value);
}

// 没有 for of
var _myArray = myArray[Symbol.iterator]();
while(var _iteration = _myArray.next()) {
    if (_iteration.done) {
        break;
    }
    var value = _iteration.value;
    console.log(value);
}

Symbol.iterator允许你重写of操作,这意味着你让一个 library 使用这个方法,开发者会很爱你:

class Collection {
  *[Symbol.iterator]() {
    var i = 0;
    while(this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }

}
var myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(var value of myCollection) {
    console.log(value); // 1, then 2
}
Symbol. isConcatSpreadable

Symbol.isConcatSpreadable是一个非常特殊的 Symbol ,它驱动 Array#concat 的行为。你可以看到,Array#concat能接受多个参数,如果是数组, 则本身将作为concat操作的一部分被展开(或扩展)。(数组的默认行为是可以展开)。参考下面的代码:

x = [1, 2].concat([3, 4], [5, 6], 7, 8);
assert.deepEqual(x, [1, 2, 3, 4, 5, 6, 7, 8]);  //true

ES6 将使用Symbol.isConcatSpreadable判定是否每个参数都是可以被展开的。这更常用于说,你继承于 Array 的类, 对 Array#concat 不会友好(不会被Array#concat展开),而不是被展开:

class ArrayIsh extends Array {
    get [Symbol.isConcatSpreadable]() {
        return true;
    }
}
class Collection extends Array {
    get [Symbol.isConcatSpreadable]() {
        return false;
    }
}
arrayIshInstance = new ArrayIsh();
arrayIshInstance[0] = 3;
arrayIshInstance[1] = 4;
collectionInstance = new Collection();
collectionInstance[0] = 5;
collectionInstance[1] = 6;
spreadableTest = [1,2].concat(arrayInstance).concat(collectionInstance);
assert.deepEqual(spreadableTest, [1, 2, 3, 4, <Collection>]);

Symbol.unscopables

这个 Symbol 有一点有趣的历史。大体上,在开发ES6的时候,TC(译者注:tc39?不太确定是不是这个)在流行的JS库中发现了一些旧代码,这些库执行了这样的事情:

var keys = [];
with(Array.prototype) {
    keys.push('foo');
}

这段代码在ES5及之前的老代码中工作的很好,但是ES6 现在有 Array#keys,这意味着当你执行 with(Array.prototype)keys现在是Array#keys方法,不是你设置的变量。所以这有三个解决方案:

  1. 尝试获取所有网站中使用这段代码的库,并改变/更新他们的代码。(这不可能)。

  2. 移除Array#keys 方法,然后祈祷删除以后不会出现其他的bug(这不是真正的解决了问题)

  3. 写一个hack包住这些代码,防止一定范围内的with语句。

TC 选择了第三个方案,所以Symbol.unscopables就诞生了,它定义了一个对象中不可见的值,当在with语句中使用时不应该设置。你可能永远都不需要使用它,也不会再日常 Javascript 中遇到它,但它示范了一部分 Symbols 的强大功能,下面是完整的代码:

Object.keys(Array.prototype[Symbol.unscopables]); // -> ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']

// Without unscopables:
class MyClass {
    foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
    foo(); // 1!!
}

// Using unscopables:
class MyClass {
    foo() { return 1; }
    get [Symbol.unscopables]() {
        return { foo: true };
    }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
    foo(); // 2!!
}

Symbol.match

这是另一个针对函数的 Symbol。String#match函数现在用它来判定给定的对象用什么值来做匹配。 所以,你可以提供自己的匹配实现来使用,而不是使用正则表达式:

class MyMatcher {
    constructor(value) {
        this.value = value;
    }
    [Symbol.match](string) {
        var index = string.indexOf(this.value);
        if (index === -1) {
            return null;
        }
        return [this.value];
    }
}
var fooMatcher = 'foobar'.match(new MyMatcher('foo'));
var barMatcher = 'foobar'.match(new MyMatcher('bar'));
assert.deepEqual(fooMatcher, ['foo']);
assert.deepEqual(barMatcher, ['bar']);

Symbol.replace

Symbol.match一样,Symbol.replace 也被允许添加自定义类到String#replace中你通常使用正则表达式的地方:

class MyReplacer {
    constructor(value) {
        this.value = value;
    }
    [Symbol.replace](string,) {
        var index = string.indexOf(this.value);
        if (index === -1) {
            return string;
        }
        if (typeof replacer === 'function') {
            replacer = replacer.call(undefined, this.value, string);
        }
        return ${string.slice(0, index)}${replacer}${string.slice(index + this.value.length)};
    }
}
var fooReplaced = 'foobar'.replace(new MyReplacer('foo'), 'baz');
var barMatcher = 'foobar'.replace(new MyReplacer('bar'), function () { return 'baz' });
assert.equal(fooReplaced, 'bazbar');
assert.equal(barReplaced, 'foobaz');

是的,就像Symbol.matchSymbol.replaceSymbol.search的存在是为了支持String#search,允许自定义类替代正则表达式:

class MySearch {
    constructor(value) {
        this.value = value;
    }
    [Symbol.search](string) {
        return string.indexOf(this.value);
    }
}
var fooSearch = 'foobar'.search(new MySearch('foo'));
var barSearch = 'foobar'.search(new MySearch('bar'));
var bazSearch = 'foobar'.search(new MySearch('baz'));
assert.equal(fooSearch, 0);
assert.equal(barSearch, 3);
assert.equal(bazSearch, -1);

Symbol.split

好的,最后一个 String symbols,Symbol.split 是为了支持String#split,像这样使用:

class MySplitter {
    constructor(value) {
        this.value = value;
    }
    [Symbol.split](string) {
        var index = string.indexOf(this.value);
        if (index === -1) {
            return string;
        }
        return [string.substr(0, index), string.substr(index + this.value.length)];
    }
}
var fooSplitter = 'foobar'.split(new MySplitter('foo'));
var barSplitter = 'foobar'.split(new MySplitter('bar'));
assert.deepEqual(fooSplitter, ['', 'bar']);
assert.deepEqual(barSplitter, ['foo', '']);

Symbol.species

Symbol.species是一个非常巧妙的 Symbol,它作用于一个类的 constructor 属性,允许类用这个方法创建一个自身的新实例。以Array#map为例,它用每一次 callback 的返回值创建了一个新的数组,在 ES5 中Array#map的代码可能看起来像这样:

Array.prototype.map = function (callback) {
    var returnValue = new Array(this.length);
    this.forEach(function (item, index, array) {
        returnValue[index] = callback(item, index, array);
    });
    return returnValue;
}

在ES6 中Array#map与所有不改变数组本身的数组方法一样,升级为使用Symbol.species属性创建对象,因此ES6 中Array#map 的代码看起来更类似于这样:

Array.prototype.map = function (callback) {
    var Species = this.constructor[Symbol.species];
    var returnValue = new Species(this.length);
    this.forEach(function (item, index, array) {
        returnValue[index] = callback(item, index, array);
    });
    return returnValue;
}

注意如果你要一个类 Foo 继承 Array(class Foo extends Array ), 当你每当调用 Foo#map时,他将返回一个新的数组(无聊),你必须编写自己的Map实现Foo's而不是Array's,现在Foo#map返回一个Foo,多亏了Symbol.species

class Foo extends Array {
    static get [Symbol.species]() {
        return this;
    }
}

class Bar extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}

assert(new Foo().map(function(){}) instanceof Foo);
assert(new Bar().map(function(){}) instanceof Bar);
assert(new Bar().map(function(){}) instanceof Array);

你可能会问“为什么不使用 this.constructor 替代this.constructor[Symbol.species]呢?” 好吧,Symbol.species提供了一个可定制的入口点来创建类型 , 你可能不总是想要子类,并且有方法创建你的子类 - 比如下面的例子:

class TimeoutPromise extends Promise {
    static get [Symbol.species]() {
        return Promise;
    }
}

可以创建此 TimeoutPromise 以执行超时的操作,但是你当然不想一个Promise的超时影响到一整条 Promise 链,所以Symbol.species 可以用来告诉TimeoutPromise 从他的原型方法返回Promise。相当方便。

Symbol.toPrimitive

这个 Symbol 是最接近于能让我们重载抽象相等运算符的 Symbol(简写==)。基本上,Symbol.toPrimitive被用在当 Javascript 引擎需要转换你的对象为原始值的时候。比如

  • 如果你使用 +object ,那么js将调用object[Symbol.toPrimitive]('number');

  • 如果你使用 ''+object',那么js将调用object[Symbol.toPrimitive]('string')

  • 如果你执行if(object)这样的东西之后会调用object[Symbol.toPrimitive]('default') 在这之前,我们有valueOftoString 来改变他们,但都这都是愚蠢的,你应该永远不会得到他们想要的行为。Symbol.toPrimitive可以像这样实现:

class AnswerToLifeAndUniverseAndEverything {
    [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
            return 'Like, 42, man';
        } else if (hint === 'number') {
            return 42;
        } else {
            // when pushed, most classes (except Date)
            // default to returning a number primitive
            return 42;
        }
    }
}

var answer = new AnswerToLifeAndUniverseAndEverything();
+answer === 42;
Number(answer) === 42;
''+answer === 'Like, 42, man';
String(answer) === 'Like, 42, man';

Symbol.toStringTag

好的,这是内建的 Symbol 值的最后一个。继续吧,你都看了这么多了,你可以的!Symbol.toStringTag其实是非常酷的一个内建的 Symbol 值。如果你曾经试图为typeof运算符实现自己的替换,你可能会碰到Object#toString(),并且如何返回这个古怪的'[object Object]''[object Array]' 字符串。在ES6之前,这种行为是在规范的缝隙中定义的,但是今天,在“富饶”的ES6大陆上我们有了专门为此而生的 Symbol!任何对象通过 Object#toString() 将被检查它们是否有[Symbol.toStringTag],它应该返回一个字符串,如果它存在,则会被用于生成字符串,例如:

class Collection {

  get [Symbol.toStringTag]() {
    return 'Collection';
  }

}
var x = new Collection();
Object.prototype.toString.call(x) === '[object Collection]'

再补充一点,如果你使用Chai来做测试,现在它底层支持使用符号做类型检测,所以你现在写 expect(x).to.be.a('Collection') 可以通过测试(如上面的代码那样实现 x,对了你需要运行在支持 Symbol.toStringTag 的浏览器上运行上面的代码)。

缺失的内置 Symbol:Symbol.isAbstractEqual

你可能现在已经在想:我真的喜欢Symbols 中有关反射的想法。对于我来说,它还是缺失了一个让我很兴奋的Symbol,Symbol.isAbstractEqual。有了Symbol.isAbstractEqual这个内建 Symbol 可以使抽象等式运算符(==)用主流的形式使用。这个内置 Symbol 可以让你的类以自己的方式使用==,就像在Ruby、Python等语言中一样。允许类重写==意味着当你看到类似于lho == rho这样的代码,可以转化成rho[Symbol.isAbstractEqual](lho)。可以采用向后兼容的方式来做,也就是为所有现有基本数据类型的原型(如Number.prototype)定义默认值,同时可以让很多规范更清晰,从而让开发者有理由在实际开发中使用==

结论

你对Symbols有什么看法?仍然困惑?还是说想骂谁?我的Twitter@keithamu,感觉不错就和我联系,也许有一天我可能会占用你的整个午饭时间告诉你很多关于ES6中很我喜欢的新功能。

现在你已经通读了所有关于Symbols的说明,你应该看第二部分 - Reflect了。

相关文章