主要知識點:建立符號值、使用符號值、共享符號值、符號值轉換。檢索符號值屬性以及知名符號
1. Symbol基礎
1.1 建立符號值
在 JS 已有的基本型別(字串、數值、布林型別、 null
與 undefined
) 之外, ES6 引入了一種新的基本型別:符號(Symbol
) 。 符號起初被設計用於建立物件私有成員,而這也是 JS 開發者期待已久的特性。在符號誕生之前,將字串作為屬性名稱導致屬性可以被輕易訪問,無論命名規則如何。而“私有名稱”意味著開發者可以建立非字串型別的屬性名稱,由此可以防止使用常規手段來探查這些名稱。
建立符號值
let firstName = Symbol('first Name');
let person = {};
person[firstName] = 'hello world';
console.log(person[firstName]); //hello world
console.log(firstName); //Symbol(first Name)
複製程式碼
此程式碼建立了一個符號型別的 firstName
變數,並將它作為 person
物件的一個屬性,而每次訪問該屬性都要使用這個符號值。Symbol
函式還可以接受一個額外的引數用於描述符號值,該描述並不能用來訪問對應屬性,但它能用於除錯。符號的描述資訊被儲存在內部屬性 [[Description]]
中,當符號的 toString()
方法被顯式
或隱式呼叫時,該屬性都會被讀取。在本例中, console.log()
隱式呼叫了 firstName
變數的 toString()
方法,於是描述資訊就被輸出到日誌。此外沒有任何辦法可以從程式碼中直接訪問 [[Description]]
屬性。
1.2 使用符號值
可以在任意使用 需計算屬性名 的場合中使用符號值,比如在物件字面量中可以使用符號值來作為物件屬性,另外還可以在Object.defineProperty()
方法或Object.defineProperties()
方法中使用。
1.3 共享符號值
如果想在不同的程式碼中共享同一個符號值的話,應使用 Symbol.for()
方法而不是 Symbol()
方法。 Symbol.for()
方法僅接受單個字串型別的引數,作為目標符號值的識別符號,同時此引數也會成為該符號的描述資訊。
//共享符號值
let uid = Symbol.for('uid');
let person = {};
person[uid] = 'hello';
let id = Symbol.for('uid');
let car = {};
car[id] = '88888888';
console.log(uid===id); //true
複製程式碼
Symbol.for()
方法首先會搜尋全域性符號登錄檔,看是否存在一個鍵值為 "uid
" 的符號值。
若是,該方法會返回這個已存在的符號值;否則,會建立一個新的符號值,並使用該鍵值將
其記錄到全域性符號登錄檔中,然後返回這個新的符號值。這就意味著此後使用同一個鍵值去呼叫 Symbol.for()
方法都將會返回同一個符號值。
還可以使用 Symbol.keyFor()
方法在全域性符號登錄檔中根據符號值檢索出對應的鍵值:
console.log(Symbol.keyFor(uid)); //uid
console.log(Symbol.keyFor(id)); //uid
複製程式碼
1.4 符號值的轉換
型別轉換是 JS 語言重要的一部分,能夠非常靈活地將一種資料型別轉換為另一種。然而符號型別在進行轉換時非常不靈活,因為其他型別缺乏與符號值的合理等價,尤其是符號值無法被轉換為字串值或數值,在邏輯運算子中會被認為等價於 true
。
例如,可使用String()方法獲取符號值的描述資訊,但是如果使用符號值在字串中拼接,則會報錯:
console.log(String(uid));//Symbol(uid)
console.log(uid+""); //Uncaught TypeError: Cannot convert a Symbol value to a string
複製程式碼
1.5 檢索符號屬性
Object.keys()
與 Object.getOwnPropertyNames()
方法可以檢索物件的所有屬性名稱,前者
返回所有的可列舉屬性名稱,而後者則返回所有屬性名稱,無論是否可以可列舉,然而兩者都不能返回符號型別的屬性。因此,在ES6中新增了
Object.getOwnPropertySymbols()
方法,以便讓你可以檢索物件的符號型別屬性。
let uid = Symbol.for('uid');
let person = {};
person[uid] = 'hello';
let symbols = Object.getOwnPropertySymbols(person);
console.log(symbols.length); //1
console.log(symbols[0]); //Symbol(uid)
複製程式碼
2. 知名符號
ES6 定義了“知名符號”來代表 JS 中一些公共行為,而這些行為此前被認為只能是內部操作,但在ES6中使用了知名符號來暴露了內部方法,提供了更加方便的呼叫這種公用方法的一種方式。每一個知名符號都對應全域性 Symbol
物件的一個屬性,例如 Symbol.create
。
知名符號有:
- Symbol.hasInstance :供 instanceof 運算子使用的一個方法,用於判斷物件繼承關係;
- Symbol.isConcatSpreadable :一個布林型別值,在集合物件作為引數傳遞給 Array.prototype.concat() 方法時,指示是否要將該集合的元素扁平化;
- Symbol.iterator :返回迭代器的一個方法;
- Symbol.match :供 String.prototype.match() 函式使用的一個方法,用於比較字串;
- Symbol.replace :供 String.prototype.replace() 函式使用的一個方法,用於替換子字串;
- Symbol.search :供 String.prototype.search() 函式使用的一個方法,用於定位子字元;
- Symbol.species :用於產生派生物件的構造器;
- Symbol.split :供 String.prototype.split() 函式使用的一個方法,用於分割字串;
- Symbol.toPrimitive :返回物件所對應的基本型別值的一個方法;
- Symbol.toStringTag :供 String.prototype.toString() 函式使用的一個方法,用於建立物件的描述資訊;
- Symbol.unscopables :一個物件,該物件的屬性指示了哪些屬性名不允許被包含在with 語句中;
2.1 Symbol.hasInstance屬性
每個函式都具有一個 Symbol.hasInstance
屬性,用於判斷指定物件是否為本函式的一個例項。這個方法定義在 Function.prototype
上,因此所有函式都繼承了面對 instanceof
運算子時的預設行為。 Symbol.hasInstance
屬性自身是不可寫入、不可配置、不可列舉的,從而保證它不會被錯誤地重寫。
Symbol.hasInstance
方法只接受單個引數,即需要檢測的值。如果該值是本函式的一個例項,則方法會返回 true
obj instanceof Array
//等價於
Array[Symbol.hasInstance](obj)
複製程式碼
2.2 Symbol.isConcatSpreadable屬性
Symbol.isConcatSpreadable
屬性是一個布林型別的屬性,它表示擁有長度length
屬性與數值型別的鍵的類陣列的物件,在呼叫 concat()
方法時,會將數值鍵對應的值分離為單獨項,新增在陣列的後部,完成陣列拼接。
它只出現在特定型別的物件上,用來標示該物件在作為concat()
引數時應如何工作,從而有效改變該物件的預設行為。你可以用它來定義任意型別的物件,讓該物件在參與 concat()
呼叫時能夠表現得像陣列一樣。
//Symbol.isConcatSpreadable屬性
let collection = {
0:'hello',
1:'world',
length:2,
[Symbol.isConcatSpreadable]:true
}
let arr = ['es6'].concat(collection);
console.log(arr); //["es6", "hello", "world"]
複製程式碼
2.3 Symbol.match 、 Symbol.replace 、 Symbol.search與Symbol.split
在 JS 中,字串與正規表示式有著密切的聯絡,尤其是字串具有幾個可以接受正規表示式作為引數的方法:
- match(regex) :判斷指定字串是否與一個正規表示式相匹配;
- replace(regex, replacement) :對正規表示式的匹配結果進行替換;
- search(regex) :在字串內對正規表示式的匹配結果進行定位;
- split(regex) :使用正規表示式將字串分割為陣列
ES6 定義了 4 個符號以及對應的方法,可以將正規表示式作為字串對應方法的第一個引數傳入, Symbol.match
對應 match()
方法, Symbol.replace
對應 replace()
,Symbol.search
對應 search()
, Symbol.split
則對應 split()
。這些符號屬性被定義在 RegExp.prototype
上作為預設實現,以供對應的字串方法使用。
2.4 Symbol.toPrimitive
JS 經常在使用特定運算子的時候試圖進行隱式轉換,以便將物件轉換為基本型別值。例如,當你使用相等(==
) 運算子來對字串與物件進行比較的時候,該物件會在比較之前被轉換為一個基本型別值。到底轉換為什麼基本型別值,在此前屬於內部操作,而 ES6 則通過Symbol.toPrimitive
屬性將其暴露出來,以便讓對應方法可以被修改。
function Temperature(degrees) {
this.degrees = degrees;
}
Temperature.prototype[Symbol.toPrimitive] = function(hint) {
switch (hint) {
case "string":
return this.degrees + "\u00b0"; // 溫度符號
case "number":
return this.degrees;
case "default":
return this.degrees + " degrees";
}
};
let freezing = new Temperature(32);
console.log(freezing + "!"); // "32 degrees!"
console.log(freezing / 2); // 16
console.log(String(freezing)); // "32°"
複製程式碼
這段指令碼定義了一個 Temperature 構造器,並重寫了其原型上的Symbol.toPrimitive
方法。返回值會依據方法的提示性引數而有所不同,可以使用字串模式、數值模式或是預設模式,而該提示性引數會在呼叫時由 JS 引擎自動填寫。字串模式中, Temperature 函式返回的溫度會附帶著 Unicode 溫度符號;數值模式只會返回溫度數值;而預設模式中,返回的溫度會附帶著字串 "degrees" 。
2.5 Symbol.toStringTag
ES6 通過 Symbol.toStringTag
重定義了相關行為,該符號代表了所有物件的一個屬性,定義了 Object.prototype.toString.call()
被呼叫時應當返回什麼值。對於陣列來說,在Symbol.toStringTag
屬性中儲存了 "Array" 值,於是該函式的返回值也就是 "Array" 。
function Person(name){
this.name = name;
}
Person.prototype[Symbol.toStringTag] = 'person';
let person = new Person('hello world');
console.log(person.toString()); //[object person]
複製程式碼
2.6 Symbol.unscopables
Symbol.unscopables
符號在 Array.prototype
上使用,以指定哪些屬性不允許在 with
語句內被繫結。 Symbol.unscopables
屬性是一個物件,當提供該屬性時,它的鍵就是用於忽略with
語句繫結的識別符號,鍵值為 true
代表遮蔽繫結。以下是陣列的 Symbol.unscopables
屬性的預設值:
// 預設內建在 ES6 中
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
});
複製程式碼
3. 總結
- 雖然符號型別的屬性不是真正的私有屬性,但它們難以被無意修改,因此在需要提供保護以防止開發者改動的場合中,它們非常合適;
- 為符號提供描述資訊以便更容易地辨識它們的值。當需要在不同程式碼片段中共享符號,可以是用S
ymbol.for()
在全域性符號登錄檔中共享符號; Object.keys()
或Object.getOwnPropertyNames()
不會返回符號值,因此 ES6 新增了一個Object.getOwnPropertySymbols()
方法,允許檢索符號型別的物件屬性。同時依然可以使用Object.defineProperty()
與Object.defineProperties()
方法對符號型別的屬性進行修改;- “知名符號”使用了全域性符號常量(例如
Symbol.hasInstance
) ,為常規物件定義了一些功能,而這些功能原先僅限內部使用。這些符號按規範使用Symbol.
的字首,允許開發者通過多種方式去修改常規物件的行為。