symbol 是一種新的原始資料型別, 用於建立需要在一定程度上被保護的屬性. 在之前, 無論是什麼屬性都需要使用字串型別的名稱來訪問; 在
Symbol
出現之後, 可以將屬性名定義成 Symbol 型別了, 儘管這樣定義的屬性不是完全私有的, 但是比較難以被意外的改變.
建立一個 Symbol 的語法
其他原始資料型別都有各自的字面量形式, 例如數字型別的 998 , 所以可以通過字面量的形式來建立這個型別的例項, 比如通過字面量形式建立一個數值型別和一個字串型別的變數:
let age = 99;
let name = 'Ross';
複製程式碼
然而 Symbol 沒有自己的字面量形式, 所以無法像這樣建立一個 Symbol 例項. 要使用全域性的函式 Symbol() 來做這件事, 例如建立一個名為 name 的Symbol:
let name = Symbol(); // 建立名為 name 的Symbol
let person = {};
person[name] = 'Ross'; // 使用這個 Symbol
// 輸出以 Symbol 為屬性名的屬性值
console.log(person[name]); // Ross
複製程式碼
注意: 不能使用 new 關鍵字加上 Symbol() 函式來建立, 這樣會報錯. 雖然其他原始型別都可以使用 new 加上建構函式來建立一個對應型別的變數, 但是 Symbol() 不能當做建構函式來使用.
建立一個帶描述性文字的 Symbol
在建立一個 Symbol 時可以加上一段描述性的字串, 用來標記這個 Symbol 是幹嘛的. 當使用 console.log
列印出這個 Symbol 時, 會將標記同時列印出來. 例如:
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 的型別:
console.log(typeof firstName); // symbol
複製程式碼
這是首選的判斷 Symbol 型別的方式.
Symbol 的使用場合--所有使用可計算屬性名的地方都可以使用 Symbol
不僅可以通過中括號呼叫屬性名的方法來使用 Symbol, 它還可以用在所有可計算屬性名的地方, 例如可計算物件字面量屬性名、Object.defineProperty()方法和Object.defineProperties()方法的呼叫過程中.
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 的函式定義了一個非原始型別的物件在轉換為原始型別值時的行為, 之前寫過一篇討論強制型別轉換的文章, 正片文章就可以歸結為這個函式的行為. 簡單來說這個函式可以根據傳入的偏好來決定將一個怎麼轉換成一個原始型別的值, 對於大多數物件, 這個函式定義的轉換機制是這樣的:
- 先呼叫這個物件的
valueOf()
方法, 如果結果是原始型別, 就直接返回, 否則 - 呼叫這個物件的
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 來暴露出一些語言內部機制的具體實現, 我們可以通過這些實現來加深對於這門語言的瞭解, 這是有必要的.