[譯]ES6 中的元編程:第一部分 —— Symbol,了不得的 Symbol

你已經據說過 ES6 了,是吧?這是一個在多方面表現卓著的 JavaScript 的新版本。每當在 ES6 中發現使人驚歎的新特性,我就會開始對個人同事口若懸河起來(可是所以佔用了別人的午休時間並非全部人樂意的)。html

一系列優秀的 ES6 的新特性都來自於新的元編程工具,這些工具將底層鉤子(hooks)注入到了代碼機制中。目前,介紹 ES6 元編程的文章寥寥,因此我認爲我將撰寫 3 篇關於它們的博文(附帶一句,我太懶了,這篇完成度 90% 的博文都在個人草稿箱裏面躺了三個月了,自打我說了要撰文以後,更多內容都已在這裏完成):前端

第一部分:Symbols(本篇文章)、第二部分:Reflect第三部分: Proxiesnode

元編程

首先,讓咱們快速認識一下元編程,去探索元編程的美妙世界。元編程(籠統地說)是全部關於一門語言的底層機制,而不是數據建模或者業務邏輯那些高級抽象。若是程序能夠被描述爲 「製做程序」,元編程就能被描述爲 「讓程序來製做程序」。你可能已經在平常編程中不知不覺地使用到了元編程。react

元編程有一些 「子分支(subgenres)」 —— 其中之一是 代碼生成(Code Generation),也稱之爲 eval —— JavaScript 在一開始就擁有代碼生成的能力(JavaScript 在 ES1 中就有了 eval,它甚至早於 try/catchswitch 的出現)。目前,其餘一些流行的編程語言都具備 代碼生成 的特性。android

元編程另外一個方面是反射(Reflection) —— 其用於發現和調整你的應用程序結構和語義。JavaScript 有幾個工具來完成反射。函數有 Function#nameFunction#length、以及 Function#bindFunction#callFunctin#apply。全部 Object 上可用的方法也算是反射,例如 Object.getOwnProperties。JavaScript 也有反射/內省運算符,如 typeofinstancesof 以及 deleteios

反射是元編程中很是酷的一部分,由於它容許你改變應用程序的內部工做機制。以 Ruby 爲例,你能夠聲明一個運算符做爲方法,從而重寫該運算符針對這個類的工做機制(這一手段一般稱爲 「運算符重載」):git

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 開始幫助 JavaScript 在元編程上遇上其餘語言。es6

ES6 下的元編程

ES6 帶來了三個全新的 API:SymbolReflect、以及 Proxy。剛看到它們時會有些疑惑 —— 這三個 API 都是服務於元編程的嗎?若是你分開看這幾個 API,你不難發現它們確實頗有意義:github

  • Symbols 是 實現了的反射(Reflection within implementation)—— 你將 Symbols 應用到你已有的類和對象上去改變它們的行爲。
  • Reflect 是 經過自省(introspection)實現反射(Reflection through introspection) —— 一般用來探索很是底層的代碼信息。
  • Proxy 是 經過調解(intercession)實現反射(Reflection through intercession) —— 包裹對象並經過自陷(trap)來攔截對象行爲。

因此,它們是怎麼工做的?它們又是怎麼變得有用的?這邊文章將討論 Symbols,然後續兩篇文章則分別討論反射和代理。正則表達式

Symbols —— 實現了的反射

Symbols 是新的原始類型(primitive)。就像是 NumberString、和 Boolean 同樣。Symbols 具備一個 Symbol 函數用於建立 Symbol。與別的原始類型不一樣,Symbols 沒有字面量語法(例如,String 有 '')—— 建立 Symbol 的惟一方式是使用相似構造函數而又非構造函數的 Symbol 函數:

Symbol(); // symbol
console.log(Symbol()); // 輸出 "Symbol()" 至控制檯
assert(typeof Symbol() === 'symbol')
new Symbol(); // TypeError: Symbol is not a constructor複製代碼

Symbols 擁有內置的 debug 能力

Symbols 能夠指定一個描述,這在 debug 時頗有用,當咱們可以輸出更有用的信息到控制檯時,咱們的編程體驗將更爲友好:

console.log(Symbol('foo')); // 輸出 "Symbol(foo)" 至控制檯
assert(Symbol('foo').toString() === 'Symbol(foo)');複製代碼

Symbols 能被用做對象的 key

這是 Symbols 真正有趣之處。它們和對象緊密的交織在一塊兒。Symbols 能用做對象的 key (相似字符串 key),這意味着你能夠分配無限多的具備惟一性的 Symbols 到一個對象上,這些 key 保證不會和現有的字符串 key 衝突,或者和其餘 Symbol key 衝突:

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 key 沒法經過 for infor of 或者 Object.getOwnPropertyNames 得到 —— 得到它們的惟一方式是 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);複製代碼

這意味着 Symbols 可以給對象提供一個隱藏層,幫助對象實現了一種全新的目的 —— 屬性不可迭代,也不可以經過現有的反射工具得到,而且能被保證不會和對象任何已有屬性衝突。

Symbols 是徹底惟一的......

默認狀況下,每個新建立的 Symbol 都有一個徹底惟一的值。若是你新建立了一個 Symbol(var mysym = Symbol()),在 JavaScript 引擎內部,就會建立一個全新的值。若是你不保留 Symbol 對象的引用,你就沒法使用它。這也意味着兩個 Symbol 將毫不會等同於同一個值,即便它們有同樣的描述:

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);複製代碼

......等等,也有例外

稍安勿躁,這有一個小小的警告 —— JavaScript 也有另外一個建立 Symbol 的方式來輕易地實現 Symbol 的得到和重用:Symbol.for()。該方法在 「全局 Symbol 註冊中心」 建立了一個 Symbol。額外注意的一點:這個註冊中心也是跨域的,意味着 iframe 或者 service worker 中的 Symbol 會與當前 frame Symbol 相等:

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

// 不是惟一的:
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');

// 跨域
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!複製代碼

全局 Symbol 會讓東西變得更加複雜,但咱們又捨不得它好的方面。如今,大家當中的一些人可能會說:「我要怎樣知道哪些 Symbol 是惟一的,哪些不是?」,對此,我會說 「別擔憂,咱們還有 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 是什麼,又不是什麼?

上面咱們對於 Symbol 是什麼以及它們如何工做有一個概覽,但更重要的是,咱們得知道 Symbol 適合和不適合什麼場景,若是認識寥寥,極可能會對 Symbol 產生誤區:

  • Symbols 毫不會與對象的字符串 key 衝突。這一特性讓 Symbol 在擴展已有對象時表現卓著(例如,Symbol 做爲了一個函數參數),它不會顯式地影響到對象:

  • Symbols 沒法經過現有的反射工具讀取。你須要一個新的方法 Object.getOwnPropertySymbols() 來訪問對象上的 Symbols,這讓 Symbol 適合存儲那些你不想讓別人直接得到的信息。使用 Object.getOwnPropertySymbols() 是一個很是特殊的用例,通常人可不知道。

  • Symbols 不是私有的。做爲雙刃劍的另外一面 —— 對象上全部的 Symbols 均可以直接經過 Object.getOwnPropertySymbols() 得到 —— 這不利於咱們使用 Symbol 存儲一些真正須要私有化的值。不要嘗試使用 Symbols 存儲對象中須要真正私有化的值 —— Symbol 總能被拿到。

  • 可枚舉的 Symbols 可以被複制到其餘對象,複製會經過相似這樣的 Object.assign 新方法完成。若是你嘗試調用 Object.assign(newObject, objectWithSymbols),而且全部的可迭代的 Symbols 做爲了第二個參數(objectWithSymbols)傳入,這些 Symbols 會被複制到第一個參數(newObject)上。若是你不想要這種狀況發生,就用 Obejct.defineProperty 來讓這些 Symbols 變得不可迭代。

  • Symbols 不能強制類型轉換爲原始對象。若是你嘗試強制轉換一個 Symbol 爲原始值對象(+Symbol()-Symbol()Symbol() + 'foo'),將會拋出一個錯誤。這防止你將 Symbol 設置爲對象屬性名時,不當心字符串化了(stringify)它們。(譯註:經 @Raoul1996 測試,Symbol 能夠被轉化爲 bool 值(typeof !!Symbol('') === 'boolean'),所以原文做者在此的描述稍顯武斷)

  • Symbols 不老是惟一的。上文中就提到過了,Symbol.for() 將爲你返回一個不惟一的 Symbol。不要總認爲 Symbol 具備惟一性,除非你本身可以保證它的惟一性。

  • Symbols 與 Ruby 的 Symbols 不是一回事。兩者有一些共性,例如都有一個 Symbol 註冊中心,但僅僅如此。JavaScript 中 Symbol 不能當作 Ruby 中 Symbol 去使用。

Symbols 真正適合的是什麼?

現實中,Symbols 只是一個略有不一樣綁定對象屬性的方式 —— 你可以輕易地提供一些著名的 Symbols(例如 Symbols.iterator) 做爲標準方法,正如 Object.prototype.hasOwnProperty 這個方法就出如今了全部繼承自 Object 的對象(繼承自 Object,基本上也就意味着一切對象都有 hasOwnProperty 這個方法了)。實際上,例如 Python 這樣的語言是這樣提供標準方法的 —— 在 Python 中,等同於 Symbol.iterator 的是 __iter__,等同於 Symbole.hasInstance 的是 __instancecheck__,而且我猜 __cmp__ 也相似於 Symbole.toPrimitive。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. 做爲一個對象中放置元信息(metadata)的場所

你也能夠用 Symbol 來存儲一些對於真實對象來講較爲次要的元信息屬性。把這看做是不可迭代性的另外一層面(畢竟,不可迭代的 keys 仍然會出如今 Object.getOwnProperties 中)。讓咱們建立一個可靠的集合類,併爲其添加一個 size 引用來得到集合規模這一元信息,該信息藉助於 Symbol 不會暴露給外部(只要記住,Symbols 不是私有的 —— 而且只有當你不在意應用的其餘部分會修改到 Symbols 屬性時,再使用 Symbol):

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 中爲對象添加鉤子(hook)的能力

這聽起來有點奇怪,但你們不妨多點耐心,聽我解釋。假定咱們有一個 console.log 風格的工具函數 —— 這個函數能夠接受 任何 對象,並將其輸出到控制檯。它有本身的機制去決定如何在控制檯顯示對象 —— 可是你做爲一個使用該 API 的開發者,得益於 inspect Symbol 實現的一個鉤子,你可以提供一個方法去重寫顯示機制 :

// 從 API 的 Symbols 常量中得到這個充滿魔力的 Inspect Symbol
var inspect = console.Symbols.INSPECT;

var myVeryOwnObject = {};
console.log(myVeryOwnObject); // 日誌 `{}`

myVeryOwnObject[inspect] = function () { return 'DUUUDE'; };
console.log(myVeryOwnObject); // 日誌輸出 `DUUUDE`複製代碼

這個審查(inspect)鉤子大體實現以下:

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,它爲你提供了輔助對象來收集你本身在對象上定義的元信息)。

譯註:若是你對 WeakMap 存有疑惑,能夠參看 stackoverflow —— What are the actual uses of ES6 WeakMap?

Node.js 已經在其 console.log 中已經有了相似的實現。其使用了一個字符串('inspect')而不是 Symbol,這意味着你能夠設置 x.inspect = function(){} —— 這不是聰明的作法,由於某些時候,這可能會和你的類方法衝突。而使用 Symbol 是一個很是有前瞻性的方式來防止這樣的狀況發生

這樣使用 Symbols 的方式是意義深遠的,這已經成爲了這門語言的一部分,藉此,咱們開始深刻到一些有名的 Symbol 中去。

內置的 Symbols

一個使 Symbols 有用的關鍵部分就是一系列的 Symbol 常量,這些常量被稱爲 「內置的 Symbols」。這些常量其實是一堆在 Symbol 類上的由其餘諸如數組(Array),字符串(String)等原生對象以及 JavaScript 引擎內部實現的靜態方法。這就是真正 「實現了的反射(Reflection within Implementation)」 一部分發生的地方,由於這些內置的 Symbol 改變了 JavaScript 內部行爲。接下來,我將詳述每一個 Symbol 作了什麼以及爲什麼這些 Symbols 是如此的棒。

Symbol.hasInstance: instanceof

Symbol.hasInstance 是一個實現了 instanceof 行爲的 Symbol。當一個兼容 ES6 的引擎在某個表達式中看到了 instanceof 運算符,它會調用 Symbol.hasInstance。例如,表達式 lho instanceof rho 將會調用 rho[Symbol.hasInstance](lho)rho 是運算符的右操做數,而 lho 則是左運算數)。而後,該方法可以決定是否某個對象繼承自某個特殊實例,你能夠像下面這樣實現這個方法:

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

Symbol.iterator

若是你或多或少據說過了 Symbols,你極可能據說的是 Symbol.iterator。ES6 帶來了一個新的模式 —— for of 循環,該循環是調用 Symbol.iterator 做爲右手操做數來取得當前值進行迭代的。換言之,下面兩端代碼是等效的:

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.ierator 將容許你重寫 of 運算符 —— 這意味着若是你使用它來建立一個庫,那麼開發者愛死你了:

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 可以接收多個參數,若是你傳入的參數是多個數組,那麼這些數組會被展平,又在以後被合併。考慮到下面的代碼:

x = [1, 2].concat([3, 4], [5, 6], 7, 8);
assert.deepEqual(x, [1, 2, 3, 4, 5, 6, 7, 8]);複製代碼

在 ES6 下,Array#concat 將利用 Symbol.isConcatSepreadable 來決定它的參數是否可展開。關於此,應該說是你的繼承自 Array 的類不是特別適用於 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(Technical Committees:技術委員會)發如今一些流行的 JavaScript 庫中,有這樣一些老代碼:

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

這個代碼在 ES5 或者更早版本的 JavaSacript 中工做良好,可是 ES6 如今有了一個 Array#keys —— 這意味着當你執行 with(Array.prototype) 時,keys 指代的是 Array 原型上的 keys 方法,即 Array#keys ,而不是 with 外部你定義的 keys。有三個辦法解決這個問題:

  1. 檢索全部使用了該代碼的網站,升級對應的代碼庫。(這基本是不可能的)
  2. 刪除 Array#keys ,並祈禱相似 bug 不會出現。(這也沒有真正解決這個問題)
  3. 寫一個 hack 包裹全部這樣的代碼,防止 keys 出如今 with 語句的做用域中。

技術委員會選擇的是第三種方式,所以 Symbol.unscopables 應運而生,它爲對象定義了一系列 「unscopable(不被做用域的)」 的值,當這些值用在了 with 語句中,它們不會被設置爲對象上的值。你幾乎用不到這個 Symbol —— 在平常的 JavaScript 編程中,你也遇不到這樣的狀況,可是這仍然體現了 Symbols 的用法,而且保障了 Symbol 的完整性:

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

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

// 使用 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 函數將可以自定義 macth 規則流判斷給定的值是否匹配。如今,你可以實現本身的匹配策略,而不是使用正則表達式:

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 也容許傳遞自定義的類來完成字符串的替換,而不只是使用正則表達式:

class MyReplacer {
    constructor(value) {
        this.value = value;
    }
    [Symbol.replace](string, replacer) {
        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.search

Symbol.matchSymbol.replace 相似,Symbol.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

如今到了最後一個字符串相關的 Symbol 了 —— 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,它指向了一個類的構造函數,這容許類可以建立屬於本身的、某個方法的新版本。以 Array#map 爲例,其能建立一個新的數組,新數組中的值來源於傳入的回調函數每次的返回值 —— 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,以及其餘全部的不可變 Array 方法(如 Array#filter 等),都已經更新到了使用 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;
}複製代碼

如今,若是你寫了 class Foo extends Array —— 每當你調用 Foo#map,在其返回一個 Array 類型(這並非咱們想要的)的數組以前,你本該撰寫一個本身的 Map 實現來建立 Foo 的類型數組而不是 Array 類的數組,但如今,有了 Sympbol.speciesFoo#map 可以直接返回了一個 Foo 類型的數組:

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;
    }
}複製代碼

這個 timeout promise 能夠建立一個延時的操做 —— 固然,你不但願某個 Promise 會對整個 Prmoise 鏈上的後續的 Promise 形成延時,因此 Symbol.species 可以用來告訴 TimeoutPromise 從其原型鏈方法返回一個 Promise(譯註:若是返回的是 TimeoutPromise,那麼由 Promise#then 串聯的 Promise 鏈上每一個 Promise 都是 TimeoutPromise)。這實在是太方便了。

Symbol.toPrimitive

這個 Symbol 爲咱們提供了重載抽象相等性運算符(Abstract Equality Operator,簡寫是 ==)。基本上,當 JavaScript 引擎須要將你對象轉換爲原始值時,Symbol.toPrimitive 會被用到 —— 例如,若是你執行 +object ,那麼 JavaScript 會調用 object[Symbol.toPrimitive]('number');,若是你執行 ''+object ,那麼 JavaScript 會調用 object[Symbol.toPrimive]('string'),而若是你執行 if(object),JavaScript 則會調用 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 {
            // 大多數類(除了 Date)都默認返回一個數值原始值
            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] 屬性,這個屬性是一個字符串 ,若是有,那麼將使用該字符串做爲 Object#toString() 的結果,例子以下:

class Collection {

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

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

關於此的另外一件事兒是 —— 若是你使用了 Chai 來作測試,它如今已經在底層使用了 Symbol 來作類型檢測,因此,你可以在你的測試中寫 expect(x).to.be.a('Collection')x 有一個相似上面 Symbol.toStringTag 的屬性,這段代碼須要運行在支持該 Symbol 的瀏覽器上)。

缺失的 Symbol:Symbol.isAbstractEqual

你可能已經知曉了 ES6 中的 Symbol 的意義和用法,但我真的很喜歡 Symbol 中有關反射的想法,所以還想再多說兩句。對於我來講,這還缺失了一個我會爲之興奮的 Symbol:Symbol.isAbstractEqual。這個 Symbol 可以讓抽象相等性運算符(==)重現榮光。像 Ruby、Python 等語言那樣,咱們可以用咱們本身的方式,針對咱們本身的類,使用它。當你看見諸如 lho == rho 這樣的代碼時,JavaScript 可以轉換爲 rho[Symbol.isAbstractEqual](lho),容許類重載運算符 == 的意義。這能夠以一種向後兼容的方式實現 —— 經過爲全部如今的原始值原型(例如 Number.prototype)定義默認值,該 Symbol 將使得不少規範更加清晰,並給開發者一個從新拾回 == 使用的理由。

結論

你是怎樣看待 Symbols 的?仍然迷惑不解嗎?想對某人大聲發泄嗎? 我是 Titterverse 上的 @keithamus —— 你能夠在上面隨便叨擾我,說不許某天我就會花上整個午飯時間來告訴你我最喜歡的那些 ES6 新特性。

如今,你已經閱讀完了全部關於 Symbols 的東西,接下來你就該閱讀 第二部分 —— Reflect 了。

最後我也要感謝那些優秀的開發者 @focusaurus@mttshw, @colby_russell@mdmazzola,以及 @WebReflection 對於該文的校對和提高。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索