ES6 Symbol 及其屬性

symbol 是一種新的原始數據類型, 用於建立須要在必定程度上被保護的屬性. 在以前, 不管是什麼屬性都須要使用字符串類型的名稱來訪問; 在 Symbol 出現以後, 能夠將屬性名定義成 Symbol 類型了, 儘管這樣定義的屬性不是徹底私有的, 可是比較難以被意外的改變.函數

建立一個 Symbol 的語法

其餘原始數據類型都有各自的字面量形式, 例如數字類型的 123 , 因此能夠經過字面量的形式來建立這個類型的實例, 好比經過字面量形式建立一個數值類型和一個字符串類型的變量:ui

let age = 99;
let name = 'Ross';
複製代碼

然而 Symbol 沒有本身的字面量形式, 因此沒法像這樣建立一個 Symbol 實例. 要使用全局的函數 Symbol() 來作這件事, 例如建立一個名爲 name 的Symbol:this

let name  = Symbol();  // 建立名爲 name 的Symbol

let person = {};
person[name] = 'Ross';  // 使用這個 Symbol

// 輸出以 Symbol 爲屬性名的屬性值
console.log(person[name]);  // Ross
複製代碼

注意: 不能使用 new 關鍵字加上 Symbol() 函數來建立, 這樣會報錯. 雖然其餘原始類型均可以使用 new 加上構造函數來建立一個對應類型的變量, 可是 Symbol() 不能當作構造函數來使用.spa

建立一個帶描述性文字的 Symbol

在建立一個 Symbol 時能夠加上一段描述性的字符串, 用來標記這個 Symbol 是幹嗎的. 當使用 console.log 打印出這個 Symbol 時, 會將標記同時打印出來. 例如:prototype

let firstName = Symbol('first name'); // 標記是 'first name'

let person = {};
person[firstName] = 'Ross';  // 使用這個 Symbol
console.log(person[firstName]);  // Ross

// 輸出這個 Symbol
console.log(firstName);  // "Symbol(first name)"
複製代碼

使用 typeof 來準確判斷 Symbol 類型

對 Symbol 使用 typeof 會返回 "symbol", 因此能夠經過 typeof 操做符準確判斷 Symbol 的類型:code

console.log(typeof firstName);  // symbol
複製代碼

這是首選的判斷 Symbol 類型的方式.對象

Symbol 的使用場合--全部使用可計算屬性名的地方均可以使用 Symbol

不只能夠經過中括號調用屬性名的方法來使用 Symbol, 它還能夠用在全部可計算屬性名的地方, 例如可計算對象字面量屬性名、Object.defineProperty()方法和Object.defineProperties()方法的調用過程當中.ip

let firstName = Symbol('first name');
let lastName = Symbol('last name');

// 可計算對象字面量屬性
let person = {
    [firstName]: 'Ross',
    [lastName]: 'Geller'
};

// 在 Object.defineProperty() 方法設置以 Symbol:firstName 爲屬性名的屬性值的屬性, 將其屬性值設置成只讀
Object.defineProperty(person, firstName, { writable: false });

// 在 Object.defineProperties() 方法設置以 Symbol:lastName 爲屬性名的屬性值的屬性, 將其屬性值設置成只讀
Object.defineProperties(person, {
    [lastName]: {
        writable: false
    }
});
複製代碼

共享的 Symbol

有時咱們但願在不一樣的代碼中共享同一個 Symbol, 例如在不一樣的文件中使用同一個 Symbol. 若是代碼規模較大, 像這樣就比較困難了, 所幸 Symbol 帶有一個共享體系, 能夠供咱們在全局中建立並使用全局共享的 Symbol.原型鏈

建立共享 Symbol 的語法

在全局中存在一個註冊表, 用來保存全局的共享 Symbol 的名單, 經過向這個註冊表中增長元素來設置一個 Symbol 爲全局共享的.開發

使用 Symbol.for(description) 來向註冊表中添加一個 Symbol, 其中 description 是個字符串, 起到描述的做用, 只要兩個全局 Symbol 的描述相同, 那麼他們就是同一個 Symbol.

Symbol.for(description) 方法先在註冊表中尋找這個 Symbol 有沒有被註冊, 也就是找註冊表中有沒有這個 description, 若是有就直接返回, 不然就用這個description新建一個, 保存以後將這個 Symbol 返回. 這樣這個Symbol 就是全局共享的了. 例如:

// 新建兩個描述符都是 dog 的共享 Symbol
let wangcai = Symbol.for('dog');
let dahuang = Symbol.for('dog');

console.log(wangcai === dahuang);  // true
複製代碼

這裏可能會有個疑問: 若是兩個普通 Symbol的描述字符串同樣, 那麼他們是同一個 Symbol 嗎? 答案爲不是, 經過實驗能夠得出:

// 建立兩個描述同樣的非共享的 Symbol 
let wangcai = Symbol('dog');
let dahuang = Symbol('dog');

console.log(wangcai === dahuang);  // false
複製代碼

經過 Symbol.keyFor() 獲得共享的 Symbol 的描述文字

若是有了一個共享的 Symbol, 可是不知道他的描述, 能夠經過 Symbol.keyFor() 將他的描述性文字取出來, 例如把上面例子中的 wangcai和dahuang的描述文字取出來:

console.log(Symbol.keyFor(wangcai));  // dog
console.log(Symbol.keyFor(dahuang));  // dog
複製代碼

Symbol 的強制類型轉換

JavaScript 的一個比較讓人頭疼的語言特性是強制類型轉換, 可是對於 Symbol 來講就顯得比較簡單了.

雖然在使用 console.log 輸出一個 Symbol 時會調用這個 Symbol 的 toString() 方法, 也可使用 String(symbol)來得到它的有關信息, 可是在其餘狀況下卻並不會這樣, 例如想把一個Symbol用加號操做符 "+" 轉換爲字符串就會報錯, 若是想將它經過除法操做符轉換成一個數值型變量則也會報錯.

因此不要把 Symbol 強制轉換成數值和字符串類型, 緣由不只如此, 也是由於 Symbol 的出現就是在某些場合下來替代字符串做爲屬性名的, 在不恰當的時候把他轉化爲字符串就違背了添加 Symbol 的本意了.

遍歷全部的 Symbol 屬性

對象的通常屬性可使用 Object.getOwnPropertyNames() 方法和 Object.keys() 方法來遍歷, 兩者的區別是前者返回全部屬性, 然後者只返回對象中可枚舉的屬性名. 可是這兩個方法都不支持 Symbol 屬性, 因此ES6 中新增了一個專門用來檢索 Symbol 屬性的方法 Object.getOwnPropertySymbols(), 這個方法返回一個對象中全部的 Symbol 屬性名. 使用方法以下:

let firstName = Symbol.for('first name');
let lastName = Symbol('last name');

let person = {
    [firstName]: 'Ross',
    [lastName]: 'Geller'
};

let symbolPropertyNames = Object.getOwnPropertySymbols(person);

console.log(symbolPropertyNames.length, symbolPropertyNames[0], person[symbolPropertyNames[0]]);
// 2 Symbol(first name) Ross
複製代碼

幾個經過 well-known Symbol 暴露出來的內部操做

背景: 從 ES5 開始, JavaScript 語言就嘗試將其提供的一些自建函數的內部邏輯展現出來並容許開發者本身修改. 在ES6 中因爲 Symbol 的出現, 增長了在原型鏈上定義的與 Symbol 相關的屬性來暴露更多的內部邏輯.

ES6經過Symbol對象的一些屬性暴露了語言中一些方法的內部實現, 例如使用 Symbol.hasInstance 來暴露使用 instanceof 操做符時具體的工做流程; 使用 Symbol.toPrimitive 來暴露將一個對象轉換爲原始類型時會調用的方法.

Symbol.hasInstance

instanceof 操做符用來判斷一個對象是否是某個類的實例, 例如:

function Person() {
    // ...
}

// 建立一個類的實例
let p1 = new Person();

console.log(p1 instanceof Person); // true , p1 是 Person 類的實例
複製代碼

在 ES6 中, 能夠經過修改一個類的 Symbol.hasInstance 屬性來改變 instanceof 操做符的行爲, 參照下面的實驗:

// 和上面同樣, 定義 Person 類
function Person() {
    // ...
}

// 經過修改 Person 對象的 Symbol.hasInstance 屬性來修改對 Person 類使用 instanceof 的結果
Object.defineProperty(Person, Symbol.hasInstance, {
    value(v){
        return false;  // 修改成不管是否是 Person 的實例, 都返回 false
    }
});

// 建立 Person 類的實例
let p1 = new Person();

// 輸出對 Person 類使用 instanceof 的結果
console.log(p1 instanceof Person); // false , 看到了效果
複製代碼

從上面的例子能夠看出來, 其實在對一個類使用 instanceof 時, 後臺會調用這個類的名爲 Symbol.hasInstance 的函數來進行判斷並返回結果, 因此才能夠經過修改它來修改 instanceof 操做符的行爲.

Symbol.toPrimitive

名爲 Symbol.toPrimitive 的函數定義了一個非原始類型的對象在轉換爲原始類型值時的行爲, 以前寫過一篇討論強制類型轉換的文章, 正片文章就能夠歸結爲這個函數的行爲. 簡單來講這個函數能夠根據傳入的偏好來決定將一個怎麼轉換成一個原始類型的值, 對於大多數對象, 這個函數定義的轉換機制是這樣的:

  1. 先調用這個對象的 valueOf() 方法, 若是結果是原始類型, 就直接返回, 不然
  2. 調用這個對象的 toString() 方法, 若果結果是原始類型, 則返回, 不然拋出一個錯誤.

可是對於 Date 這一個特殊的對象, 上面兩個步驟是反過來的. 至於不一樣種類對象的 valueOf()toString() 方法以前的文章中有討論.

咱們能夠經過本身定義這個函數來修改一個對象轉換成原始值的方式, 例如:

// 建立一個類
function Temprature(degrees) {
    this.degrees = degrees;
}

// 經過修改原型鏈上的 Symbol.toPrimitive 來修改這個類被轉換爲基本數據類型的行爲
Temprature.prototype[Symbol.toPrimitive] = function(hint){
    switch(hint){
        case 'string':
            return this.degrees + '\u00b0';
            break;

        case 'number':
            return this.degrees;
            break;

        default:
            return this.degrees + ' degrees';
            break;
    }
};

// 新建這個類的對象
let temprature = new Temprature(99); 

// 觸發這個類轉換爲基本數據類型的行爲
console.log(temprature + ''); // 觸發默認行爲, 結果是 "99 degrees"
console.log(temprature / 1);  // 觸發轉換爲數值類型的行爲, 結果是 99 
console.log(String(temprature)); // 觸發轉換爲字符串類型的行爲, 結果是 "99°"
複製代碼

由上面的例子能夠看到經過修改一個類的原型上的 Symbol.toPrimitive 方法能夠修改這個類的對象轉換爲原始值的行爲.

其餘 well-known Symbol

除了以上提到的兩個 well-known Symbol 方法以外, 還有許多相似的方法, 例如 Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.split 等等, 詳細能夠參見 MDN

總結

Symbol 主要用來做爲對象的屬性名來使用, 它具有必定的隱私性, 能夠用在全部可計算屬性的地方. 經過一些 well-known Symbol 來暴露出一些語言內部機制的具體實現, 咱們能夠經過這些實現來加深對於這門語言的瞭解, 這是有必要的.

相關文章
相關標籤/搜索