深入理解ES6--6.符號與符號屬性

你聽___發表於2018-05-07

深入理解ES6--6.符號與符號屬性

主要知識點:建立符號值、使用符號值、共享符號值、符號值轉換。檢索符號值屬性以及知名符號

符號與符號屬性的知識點

1. Symbol基礎

1.1 建立符號值

在 JS 已有的基本型別(字串、數值、布林型別、 nullundefined ) 之外, 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. 總結

  1. 雖然符號型別的屬性不是真正的私有屬性,但它們難以被無意修改,因此在需要提供保護以防止開發者改動的場合中,它們非常合適;
  2. 為符號提供描述資訊以便更容易地辨識它們的值。當需要在不同程式碼片段中共享符號,可以是用Symbol.for()在全域性符號登錄檔中共享符號;
  3. Object.keys()Object.getOwnPropertyNames() 不會返回符號值,因此 ES6 新增了一個Object.getOwnPropertySymbols()方法,允許檢索符號型別的物件屬性。同時依然可以使用Object.defineProperty()Object.defineProperties() 方法對符號型別的屬性進行修改;
  4. “知名符號”使用了全域性符號常量(例如 Symbol.hasInstance ) ,為常規物件定義了一些功能,而這些功能原先僅限內部使用。這些符號按規範使用 Symbol. 的字首,允許開發者通過多種方式去修改常規物件的行為。

相關文章