ES6中的Metaprogramming: Symbols 为什么令人惊叹
<p>你听说过es6对吧?他是在很多方面令人惊叹的 javascript 新版本。我常常兴高采烈地谈论我在 ES6 中发现的令人难以置信的新功能,这使很多同事感到懊恼 (因为似乎并不是每个人都喜欢,别人消费自己的午餐休息来谈论 ES6 模块的)。</p> <p>ES6中一个很不错的新功能是带来了一组大量新的元编程工具形式,提供了一组低级 hooks 到代码的方法。因为我的同事名们已经在写(ES6)的并不多,所以我想我会做一个很细致的帖子给他们(顺便说一下:因为我很懒,这篇文章已经在我的草稿箱里呆了三个月,完成 90% ,从和他们说了以后,我写的稍微多了些, <a href="/misc/goto?guid=4959728765100837415" rel="nofollow,noindex">已经写的</a> ):</p> <p>第一部分: Symbols (这个帖子) <a href="/misc/goto?guid=4959728765206393337" rel="nofollow,noindex">第二部分: Reflect</a> <a href="/misc/goto?guid=4959728765300882123" rel="nofollow,noindex">第三部分: Proxies</a></p> <h2>元编程( Metaprogramming)</h2> <p>首先, 让我们快速带过,发现元编程的奇妙世界。元编程是所有关于机器语言的基本机制,而不是“高级”数据建模或业务逻辑。如果编程可以被形容为为“制造程序”, 元编程就应该被描述为“让程序制作程序” 或者其他东西。你也许每天都在使用元编程,只是可能没有注意到它。</p> <p>元编程有一些“亚种” , 一个是 <em>代码生成 (Code Generation)</em> , 又名 eval , JavaScript自从成立以来就存在 (JS 在ES1就有 eval , 甚至在 try / catch 或 switch 声明之前)。 几乎所有你今天可以使用的其他语言都有 <em>Code Generation</em> 功能。</p> <p>元编程的另一个特点是 <em>反射 (Reflection)</em> :找出并调整应用程序的语义和结构。JavaScript 有相当多与反射相关的方法。如 Functions 中的 Function#name 、 Function#length , 以及 Function#bind 、 Function#call 和 Function#apply . Object上的所有可用方法都是 <em>Reflection</em> , 例如: Object.getOwnProperties (另外,不修改代码但反而收集有关它的信息的 <em>Reflection</em> 工具通常称为 <em>Introspection</em> ).我们也有 <em>Reflection</em> / <em>Introspection</em> 操作符, 比如 typeof 、 instanceof 和 delete .</p> <p>反射是元编程中很酷的一部分,因为他允许你改变应用内部的工作原理。以Ruby举个例子,在Ruby中,你可以指定运算符作为方法,它允许你覆盖这些运算符在类中使用时的工作方式(有时称为“运算符重载”)。</p> <pre> <code class="language-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!</code></pre> <p>与其他语言(如Ruby或Python)相比,Javascript的元编程特性是不够高级的,尤其是在操作符重载等工具方面,但ES6开始确实在这方面有所改进。</p> <p>ES6 中的元编程</p> <p>ES6中有三种新类型的API: Symbol , Reflect 和 Proxy 。 第一眼看可能有点迷惑,三个单独的API都用于元编程的么?但是当你分开看每一个的时候,确实会让你有这种感觉。</p> <ul> <li><em>Symbols</em> 都是关于 <em>Reflection within implementation</em> -- 把它们用在你现有的类和对象上来改变行为。</li> <li><em>Reflect</em> 都是关于 <em>Reflection through introspection</em> -- 用于发现你代码中非常低级的信息。</li> <li><em>Proxy</em> 都是关于 <em>Reflection through intercession</em> -- 包装对象和通过“圈套”截取他们的行为。</li> </ul> <p>所以它们是怎样工作的呢?它们怎么有用?这篇文章将讲述 Symbols ,而其余的两篇文章将分别讲述 Reflect 和 Proxy 。</p> <h2>Symbols - Reflection within Implementation</h2> <p>Symbols 是一个新的基本类型. 就像 Number , String , 和 Boolean 基本类型, Symbols 有一个 Symbol 方法可以用于创建它。不同于其他基本类型,Symbols 没有字符串语法(例如字符换有“"”),唯一的使用方法是Symbol构造函数,而且不能创建实例。</p> <pre> <code class="language-javascript">Symbol(); // symbol console.log(Symbol()); // prints "Symbol()" to the console assert(typeof Symbol() === 'symbol') new Symbol(); // TypeError: Symbol is not a constructor</code></pre> <p>Symbols 具有内置的可调试性</p> <p>Symbols 可以给出一个描述,这实际上只是用于调试,这样打印日志到控制台会使开发更容易。</p> <pre> <code class="language-javascript">console.log(Symbol('foo')); // prints "Symbol(foo)" to the console. assert(Symbol('foo').toString() === 'Symbol(foo)');</code></pre> <p>Symbols 可以作为 Object的keys 使用</p> <p>Symbols与对象密切相关。 他们可以作为keys 分配给对象(类似于String 的keys),这意味着你可以分配无数个唯一的 Symbols 到一个对象,并保证这些 Symbols 不会与字符串键或其他唯一符号冲突,这是 Symbols 真正有意思的地方。</p> <pre> <code class="language-javascript">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');</code></pre> <p>除此之外, Symbols 不会展示在使用 for in 、 for of 或者 Object.getOwnPropertyNames 的对象上,在对象中获取符号的唯一方法是 Object.getOwnPropertySymbols :</p> <pre> <code class="language-javascript">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);</code></pre> <p>这意味着Symbol给对象一个全新的感觉,它们为对象提供了一种隐藏属性,不能迭代,不能使用已经存在的反射工具获取,并保证不与对象中的其他属性冲突!</p> <p>Symbols 是完全唯一的</p> <p>默认情况下,每个新的符号具有完全唯一的值。如果你创建一个了 symbol( var mysym = Symbol() ),它会在 JavaScript引擎创建一个全新的值。如果你没有Symbol的引用,你就不能使用它。这也意味着两个symbols 将永远不会等于相同的值,即时他们有相同的描述。</p> <pre> <code class="language-javascript">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);</code></pre> <p>除非他们不是</p> <p>嗯,这有一个小小的警告,因为还有另一种方法使 Symbols,可以很容易抓取和重新使用: Symbol.for() 。这个方法在 global Symbol registry 创建了一个 Symbols 。顺便说一下:这个 registry 是跨域的,这意味着来自同一个框架或者服务工作的 Symbol 将与从现有框架生成的 Symbol 相同。</p> <pre> <code class="language-javascript">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!</code></pre> <p>拥有全局 Symbols 使事情更复杂,但好的方面是,我们会(容易)获得它。现在你们有些人可能会说“哎呀!?我怎么知道哪个 Symbols 是唯一的Symbols,哪个不是?对于这个问题,我觉得“没关系,不会有什么坏事情发生,因为我们有 Symbol.keyFor() ”</p> <pre> <code class="language-javascript">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'));</code></pre> <p>Symbols 是什么,Symbols 不是什么</p> <p>所以我们对于什么是 Symbols得到了一个不错的概念,并且了解了它是如何工作的。但同样重要的是知道对于Symbols 什么是好的,什么是不好的,这样很容易假定他们不是什么:</p> <ul> <li> <p>Symbols永远不会与对象字符串键冲突.这让他们非常适合集成你已经给出的对象(例如 作为函数的 param)而不会以明显的方式影响对象。</p> </li> <li> <p>Symbols 不能使用已有的反射工具读取。你需要新的 Object.getOwnPopertySymbols() 来存取一个对象的Symbols,这让Symbols 更好的存储一块你不想让别人通过正常运行得到的信息。使用 Object.getOwnPropertySymbols() 是一个非常特殊的用例。</p> </li> <li> <p>Symbols 不是私有的。另一方面,所有的对象中的Symbols,都可以使用 Object.getOwnSymbols() 获取到,对于真正私有的值并不是非常有效。不要尝试在对象中使用 symbol存储你想要真正私有的信息,他是可以被获取的。</p> </li> <li> <p>能使用像 Object.assign 这样的新方法把可枚举的 Symbols复制给其他对象 。如果你尝试调用 Object.assign(newObject, objectWithSymbols) ,第二个参数( objectWithSymbols )中所有的(可枚举)的Symbols 将被复制到第一个参数 ( newObject )。如果过你不想这种情况发生,用 Object.defineProperty 使他们不可枚举,</p> </li> </ul> <ul> <li> <p>Symbols 不强制转换为原始值。如果你尝试转换一个Symbol变成原始值( +Symbol() , ''+Symbol() , Symbol() + 'foo' ),将会抛出一个异常,这可以防止在将其设置为属性名称时意外对其进行字符串化。</p> </li> <li> <p>Symbols 通常不是唯一的。如上所述, Symbol.for() 返回一个非唯一的Symbol。不要总是假设你拥有的符号是独一无二的,除非你自己做。</p> </li> <li> <p>*Symbols 不像Ruby Symbols。他们有一些相似之处,比如有一个中央Symbol 注册表,只是这样而已。但是他们被当做 Rumby symbols 一样使用。</p> </li> </ul> <h2>好吧,但什么是Symbols真正好的呢</h2> <p>实际上,Symbols 只是稍微不同的方式来将属性附加到对象上。你可以轻松地使用内置的Symbols作为标准方法,就像 Object.prototype.hasOwnProperty ,它出现在从 Object 继承的所有东西(基本上是一切)。事实上,其他语言,比如在 Python中: Symbol.iterator 等价于 __iter__ 、 Symbol.hasInstance 等价于 __instancecheck__ ,我猜 Symbol.toPrimitive 大概与 __cmp__ 类似。Python的方式可以说是一个更糟糕的方法,虽然,因为JavaScript中的 Symbols 不需要任何奇怪的语法,用户不会莫名其妙地与这些特殊方法冲突。</p> <p>Symbols,在我看来,可以在以下两种情况使用:</p> <p>1. 通常用在一个正常的 String 或 Integer 类型的唯一值使用的地方:</p> <p>假设你有一个日志类库,它包含多个像日志级别,比如 logger.levels.DEBUG 、 logger.levels.INFO 、 logger.levels.WARN 等等。在 ES5的代码中,你需要使这些作为字符串(所以 logger.levels.DEBUG ==='debug' )或数字( logger.levels.DEBUG === 10 )。这两个都不理想,因为这些值不是唯一的值,但Symbols 是!所以 logger.levels 很容易改成这样:</p> <pre> <code class="language-javascript">log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn'), }; log(log.levels.DEBUG, 'debug message'); log(log.levels.INFO, 'info message');</code></pre> <p>2. 用在一个对象中需要存放元数据的地方</p> <p>你也可以用它们来存储一些对实际对象来说不那么重要的自定义元数据属性。这可以被认为是一个额外的不可枚举的层(毕竟,不可枚举的键仍然出现在 Object.getOwnProperty 中)。下面让我们新建一个可信赖的 Collection 类,并且添加一个隐藏在Symbol语句中的size 引用(只要记住 <strong>Symbols 不是私有的</strong> ,你可以,而且应该,只在你不介意被其他应用改变的地方使用。)</p> <pre> <code class="language-javascript">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]);</code></pre> <p>3. 赋予开发者通过你的API为他们的对象添加 hooks (钩子的)能力</p> <p>好吧,这听起来可能有点怪异,但是听我说。让我们假设我们有一个 console.log 样式的实用函数 ,这个函数可以接受任何对象,并将其记录到控制台。 对于如何在控制台中显示给定的对象,这个函数有自己的规则。 但作为一名使用这个API的开发人员,可以通过使用 inspect Symbol(检查常量) 在 hook 下提供一个方法来覆盖原有的规则:</p> <pre> <code class="language-javascript">// 从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`</code></pre> <p>按照这个想法实现的 inspect hook 看起来可能像这样:</p> <pre> <code class="language-javascript">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'); }</code></pre> <p>澄清一下,这并是说你应该修改代码给对象提供什么扩展。这样肯定是不对的,为此,请查看 <a href="/misc/goto?guid=4959728765387835157" rel="nofollow,noindex">WeakMaps</a> ,它可以提供辅助对象,以便您在对象上收集您自己的元数据。 <a href="/misc/goto?guid=4959728765478424821" rel="nofollow,noindex"> Node.js 的 已经为 console.log 提供了类似的实现 </a> 。它使用了 String ( 'inspect' )而不是一个 Symbol,这意味着你可以设置 x.inspect = function(){} ,但这可能与你的类本身的方法发生冲突,并发生意外。因而使用 Symbols 是实现这种行为非常有效的一种方式。 这种使用Symbols方式的意义是深远的,以至于随着我们Symbols 越来越了解,使之成为 javascript 语言的一部分。</p> <p>内置的Symbols</p> <p>使 Symbols 有用的关键部分是一组 Symbols 常量,称为“内置的 Symbols”。这些实际上是在Symbol类上的一组静态属性,它们在其他本地对象(如Arrays,Strings)和JavaScript引擎内部实现。这是真正的“实现中的反射”部分发生的地方,因为这些内置的 Symbols 改变了JavaScript内部的行为。 下面我详细介绍他们都是干嘛的 ?为什么他们让人如此惊叹!</p> <p>Symbol.hasInstance: instanceof</p> <p>Symbol.hasInstance 是一个驱动 instanceof 行为的Symbol。当一个符合ES6的引擎在表达式中看到 instanceof 运算符时,它调用 Symbol.hasInstance 。例如: lho instanceof rho 将调用 rho[Symbol.hasInstance](lho) (这里的 rho 是right hand operand(右边的操作),而 lho 是 the left hand operand(左边的操作))。然后根据方法来确定它是否继承自该特定实例,你的实现可能像这样:</p> <pre> <code class="language-javascript">class MyClass { static [Symbol.hasInstance](lho) { return Array.isArray(lho); } } assert([] instanceof MyClass);</code></pre> <p>(上面代码中, MyClass 是一个类, new MyClass() 会返回一个实例。该实例的 Symbol.hasInstance 方法,会在进行 instanceof 运算时自动调用,判断左侧的运算子是否为 Array 的实例。)</p> <p>Symbol.iterator</p> <p>如果你听说过关于 Symbols 的事情,你可能听说过 Symbol.iterator ,在 ES6 带来了新的语法, for of 循环,它调用了右边操作的Symbol.iterator以获取值进行迭代,换句话说使用 iterator 和 for of 是等价的。</p> <pre> <code class="language-javascript">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); }</code></pre> <p>Symbol.iterator 允许你重写 of 操作,这意味着你让一个 library 使用这个方法,开发者会很爱你:</p> <pre> <code class="language-javascript">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 }</code></pre> <p>Symbol. isConcatSpreadable</p> <p>Symbol.isConcatSpreadable 是一个非常特殊的 Symbol ,它驱动 Array#concat 的行为。你可以看到, Array#concat 能接受多个参数,如果是数组, 则本身将作为concat操作的一部分被展开(或扩展)。(数组的默认行为是可以展开)。参考下面的代码:</p> <pre> <code class="language-javascript">x = [1, 2].concat([3, 4], [5, 6], 7, 8); assert.deepEqual(x, [1, 2, 3, 4, 5, 6, 7, 8]); //true</code></pre> <p>ES6 将使用 Symbol.isConcatSpreadable 判定是否每个参数都是可以被展开的。这更常用于说,你继承于 Array 的类, 对 Array#concat 不会友好(不会被 Array#concat 展开),而不是被展开:</p> <pre> <code class="language-javascript">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>]);</code></pre> <p>Symbol.unscopables</p> <p>这个 Symbol 有一点有趣的历史。大体上,在开发ES6的时候,TC(译者注: <a href="/misc/goto?guid=4959728765574797058" rel="nofollow,noindex">tc39</a> ?不太确定是不是这个)在流行的JS库中发现了一些旧代码,这些库执行了这样的事情:</p> <pre> <code class="language-javascript">var keys = []; with(Array.prototype) { keys.push('foo'); }</code></pre> <p>这段代码在ES5及之前的老代码中工作的很好,但是ES6 现在有 Array#keys ,这意味着当你执行 with(Array.prototype) , keys 现在是 Array#keys 方法,不是你设置的变量。所以这有三个解决方案:</p> <ol> <li> <p>尝试获取所有网站中使用这段代码的库,并改变/更新他们的代码。(这不可能)。</p> </li> <li> <p>移除 Array#keys 方法,然后祈祷删除以后不会出现其他的bug(这不是真正的解决了问题)</p> </li> <li> <p>写一个hack包住这些代码,防止一定范围内的 with 语句。</p> </li> </ol> <p>TC 选择了第三个方案,所以 Symbol.unscopables 就诞生了,它定义了一个对象中不可见的值,当在 with 语句中使用时不应该设置。你可能永远都不需要使用它,也不会再日常 Javascript 中遇到它,但它演示了一些 Symbols 的实用功具,下面是完整的使用:</p> <pre> <code class="language-javascript">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!! }</code></pre> <p>Symbol.match</p> <p>这是另一个特定于函数的 Symbol。 String#match 函数现在将使用它来确定给定的值是否可以用来匹配它。 所以,你可以提供自己的匹配实现来使用,而不是使用正则表达式:</p> <pre> <code class="language-javascript">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']);</code></pre> <p>Symbol.replace</p> <p>和 Symbol.match 一样, Symbol.replace 也被允许添加自定义类到 String#replace 中你通常使用正则表达式的地方:</p> <pre> <code class="language-javascript">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');</code></pre> <p>Symbol.search</p> <p>是的,就像 Symbol.match 和 Symbol.replace , Symbol.search 的存在是为了支持 String#search ,允许自定义类替代正则表达式:</p> <pre> <code class="language-javascript">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);</code></pre> <p>Symbol.split</p> <p>好的,最后一个 String symbols, Symbol.split 是为了支持 String#split ,像这样使用:</p> <pre> <code class="language-javascript">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', '']);</code></pre> <p>Symbol.species</p> <p>Symbol.species 是一个非常聪明的Symbol,它指向一个类的构造函数值,它允许类在方法中创建一个属于自己的新版本。以 Array#map 为例,他从每一个返回值的回调中创建了一个新的数组,在 ES5 中 Array#map 的代码可能看起来像这样:</p> <pre> <code class="language-javascript">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; }</code></pre> <p>在ES6 中 Array#map 随着所有其没有改变的数组方法已经升级,使用 Symbol.species 属性创建对象,因此ES6 中 Array#map 的代码看起来更像这样:</p> <pre> <code class="language-javascript">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; }</code></pre> <p>如果你要一个类 Foo 继承 Array( class Foo extends Array ), 当你每当调用 Foo#map 时,他将返回一个新的数组(无聊),你必须编写自己的Map实现 Foo's 而不是 Array's ,现在 Foo#map 返回一个 Foo ,感谢 Symbol.species :</p> <pre> <code class="language-javascript">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);</code></pre> <p>你可能会问“为什么不使用 this.constructor 替代 this.constructor[Symbol.species] 呢?” 好吧, Symbol.species 提供了一个 <em>可定制的</em> 入口点来创建类型 , 你可能不总是想要子类,并且有方法创建你的子类 - 比如下面的例子:</p> <pre> <code class="language-javascript">class TimeoutPromise extends Promise { static get [Symbol.species]() { return Promise; } }</code></pre> <p>可以创建此 TimeoutPromise 以执行超时的操作,但是你当然不想一个Promise的超时影响到一整条 Promise 链,所以 Symbol.species 可以用来告诉 TimeoutPromise 从他的原型方法返回 Promise 。相当方便。</p> <p>Symbol.toPrimitive</p> <p>这个 Symbol 是我们最重要的事情,我们必须重载抽象等式运算符(简写 == )。基本上, Symbol.toPrimitive 被用在当Javascript 引擎需要转换你的对象为原始值的时候。比如</p> <ul> <li>如果你使用 +object ,那么js将调用 object[Symbol.toPrimitive]('number');</li> <li>如果你使用 ''+object' ,那么js将调用 object[Symbol.toPrimitive]('string')</li> <li>如果你执行 if(object) 这样的东西之后会调用 object[Symbol.toPrimitive]('default') 在这之前,我们有 valueOf 和 toString 来改变他们,但都这都是愚蠢的,你应该永远不会得到他们想要的行为。 Symbol.toPrimitive 可以像这样实现:</li> </ul> <pre> <code class="language-javascript">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';</code></pre> <p>Symbol.toStringTag</p> <p>好的,这是内置的Symbol值的最后一个。继续吧,你都看了这么多了,你可以的! Symbol.toStringTag 其实是非常酷的一个内置的Symbol值。如果你曾经试图为 typeof 运算符实现自己的替换,你可能会碰到 Object#toString() ,并且如何返回这个古怪的 '[object Object]' 或 '[object Array]' 字符串。在ES6之前,这种行为是在你规范的缝隙中定义的,但是今天,在“富饶”的ES6大陆上我们有了专门为此的Symbol!任何对象通过 Object#toString() 将被检查他们是否有 [Symbol.toStringTag] ,他应该是一个字符串,如果他是在,则会被用于生成字符串,例如:</p> <pre> <code class="language-javascript">class Collection { get [Symbol.toStringTag]() { return 'Collection'; } } var x = new Collection(); Object.prototype.toString.call(x) === '[object Collection]'</code></pre> <p>为此,如果你使用 <a href="/misc/goto?guid=4959728765681824257" rel="nofollow,noindex">Chai</a> 进行测试,现在支持使用下面的符号类型检测,所以你可以用在你的测试(提供 x 在有 Symbol.toStringTag 属性像上面,哦,你正在浏览器运行 Symbol.toStringTag 代码)中使用 expect(x).to.be.a('Collection') ,</p> <h3>缺失的内置Symbol:Symbol.isAbstractEqual</h3> <p>你现在可能想,还是算了吧,但我真的喜欢 Symbols 中有关反射的想法。对于我来说,它还是缺失了一个让我很兴奋的Symbol, Symbol.isAbstractEqual 。有了 Symbol.isAbstractEqual 这个内置 Symbol 可以使抽象等式运算符( == )回到主流的使用。这个内置Symbol可以让你的类以自己的方式使用 == ,就像在Ruby、Python和co 中一样。允许类重写 == 意味着当你看到类似于 lho == rho 这样的代码,可以转化成 rho[Symbol.isAbstractEqual](lho) 。这可以通过定义所有当前原始原型(例如 Number.prototype )的默认值,并整理规范的一部分,同时给开发人员一个理由使 == 替换回来的方式完成向后兼容。</p> <h2>结论</h2> <p>你对Symbols有什么看法?仍然困惑?还是说想骂谁?我的推ter <a href="/misc/goto?guid=4959728765772079464" rel="nofollow,noindex">@keithamu</a> ,感觉不错就和我联系,也许有一天我可能会占用你的整个午饭时间告诉你很多关于ES6中很我喜欢的新功能。</p> <p>现在你已经通读了所有关于Symbols的说明,你应该看 <a href="/misc/goto?guid=4959728765206393337" rel="nofollow,noindex">第二部分 - Reflect</a> 了。</p> <p> </p> <p>来自:http://www.zcfy.cc/article/metaprogramming-in-es6-symbols-and-why-they-re-awesome-1883.html</p> <p> </p>
本文由用户 ecfire 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!