ES6之Symbol详解
目錄
- 一、什么是Symbol?
- 二、Symbol的作用
- 三、Symbol的語法規范
- 1. 基本語法
- 2. Symbol屬性的遍歷
- 3. Symbol.for(),Symbol.keyFor()
- 四、內置的Symbol值
- 1. Symbol.hasInstance
- 2. Symbol.isConcatSpreadable
- 3. Symbol.species
- 4. Symbol.match/replace/search/split
- 5. Symbol.iterator
- 6. Symbol.toPrimitive
- 7. Symbol.toStringTag
- 8. Symbol.unscopables
- 總結
一、什么是Symbol?
Symbol是ES6中引入的一種新的基本數據類型,用于表示一個獨一無二的值。它是JavaScript中的第七種數據類型,與undefined、null、Number(數值)、String(字符串)、Boolean(布爾值)、Object(對象)并列。
你可以這樣創建一個Symbol值:
const a = Symbol();console.log(a); //Symbol()使用Symbol函數可以生成一個Symbol類型的值,但是你不能在調用Symbol時使用new關鍵字,因為Symbol是基本數據類型,而不是對象。比如下面的寫法是錯誤的:
//報錯,Symbol is not a constructor const a = new Symbol();使用Symbol()創建一個Symbol類型的值并賦值給a變量后,你就得到了一個在內存中獨一無二的值。現在除了通過變量a,任何人在任何作用域內都無法重新創建出這個值。例如當你這樣寫:
const b = Symbol();盡管a和b都是使用Symbol()創建出來的,但是它們在內存中看起來卻是這樣的:
實際上,a變量拿到了內存中某塊內存的唯一引用(這里所說的引用,其實就是該內存的地址)。如果不借助a變量,你不可能再得到這個地址。因此:
這種行為看似難以理解,但其實它與對象遵循相同的規則,如:
但是對于同為基本數據類型的字符串來說,它不遵循類似的規則。
比如:
var a = "123"; var b = "123";a === b; //返回true。兩者在常量區引用同一個字符串
我們首先通過變量a在內存中創建了字符串“123”,然后在不借助變量a的情況下,又通過var b = "123"拿到了對“123”這個字符串的引用,兩者指向內存中的同一塊內存地址。
因此我們說,a無法確保別的變量無法拿到它保存的地址(前提是不通過a)。但是對于var a = Symbol()這樣的語句,a變量內保存的值是唯一的,因為除了借助a變量,你永遠無法得到a中保存的值。這也是Symbol的本質。
可能很多人比較奇怪,一個Symbol類型的變量里面到底保存了什么呢?
我們看兩行代碼:
var a = Symbol();console.log(a); //Symbol()我們試圖輸出a的值,但js引擎輸出了Symbol()。顯然它不能說明a的值是字符串,因為:
typeof a === "symbol";所以說如果你想問js引擎a的值是多少,引擎只會告訴你它是一個Symbol類型的值。也就是說,Symbol真正存儲了什么并不重要,重要的是它的值永遠不會與別的值相等。Symbol的中文釋義為“標志,符號”,一個Symbol類型的變量只是為了標記一塊唯一的內存而存在的。也正是因為這樣,Symbol類型的值不參與運算。
二、Symbol的作用
上面我們說到,Symbol只是標記一塊內存,不能與其他數據類型進行運算,那么新增這樣一種數據類型有什么用呢?
舉個例子:
//文件A.js var a = {name: "夕山雨",getName(){return this.name;} } exports default a;//文件B.js var a = require("A.js"); a.getName = function(){return "xxx"; }由于getName這個鍵本質上只是個字符串,而無論在哪個模塊或作用域內,都可以直接引用到“getName”這個字符串,因此字符串類型的屬性很容易被意外覆蓋。
但是如果a中的屬性是使用Symbol類型的變量作為鍵,那么它就無法被篡改:
//模塊A.js var s = Symbol(); var a = {name: "夕山雨",//s是個變量,因此需要用中括號包裹起來[s]: function(){ return this.name;} } exports default a;//模塊B.js var a = require("A.js"); var s = Symbol();a[s] = function(){... //它不會對A模塊中的[s]屬性造成任何影響,因為兩個模塊的[s]不是同一個屬性 }現在,我們使用一個Symbol類型的變量作為對象屬性的鍵。由于s是一個變量,而不是字符串,因此需要使用中括號括起來(否則它會被當做字符串對待)。
在模塊B中我們使用同樣的語句var s = Symbol();創建了一個同名變量s,“企圖”通過為a[s]重新賦值覆蓋a對象上原來的[s]屬性,但這并不能生效,因為模塊A中的變量s和模塊B中的變量s是各自獨立的Symbol,他們并不相等,因此無法覆蓋。
根本原因在任何情況下都滿足:
"getName" === "getName" //而 Symbol() !== Symbol() //該行為類似{} !== {}通過把對象的屬性的鍵值設置為Symbol類型,我們有效避免了對象屬性被修改,在模塊化開發中,對象本身也就更安全。
現在,在模塊A中,我們可以像訪問普通屬性一樣用a[s]訪問該屬性,在其他模塊中,由于引用不到變量s,因此不可以直接訪問該屬性。此時的內存引用情況大致如下:
內存形成過程為:
var s = Symbol();語句在內存中創建了一個Symbol類型的變量,并將地址保存在變量s中。
var a = {
[s]: function(){ … }
}
該語句為對象a添加了一個[s]屬性。在內存中,js引擎首先需要開辟一塊內存保存這個匿名函數,然后在對象a中添加一對鍵值對,它的鍵是變量s所指向的內存地址,而值是上述匿名函數在內存中的地址。
通常來說,如果想要修改對象的某個屬性,那么你首先需要獲得這個屬性的鍵,參考上面的內存圖,實際上就是獲得這個鍵在內存中的地址(也就是變量s指向的那個內存區)。
傳統方式下,我們以字符串作為對象屬性的鍵。這樣,我們只要能得到這個字符串在內存中的地址,就可以訪問該屬性。由于同一個字符串只會在常量區生成一次,因此我們可以在任何時候通過以下方式得到“getName”這個字符串在內存中的地址:
var x = "getName";a[x] //即使不借助中間變量,也可以拿到該字符串在內存中的地址 a["getName"] a.getName這樣在不同模塊下,修改該屬性就變得很簡單。
而使用Symbol類型數據作為鍵,該鍵的內存地址只會被當前作用域的變量s引用,在其他作用域由于無法引用到這里的變量s,也就無法訪問對象的這個屬性。
除了上面最常見的用法,Symbol還可以用于消除“魔術字符串”。所謂“魔術字符串”,就是與代碼緊密耦合在一起的某個具體的字符串(或者數字,因為往往難以解釋它為什么出現,以及代表什么含義,所以被稱為魔術字符串),如:
... switch (shape) {case 'Triangle': // 魔術字符串area = .5 * options.width * options.height;break;/* ... more code ... */}這樣的“魔術字符串”會造成代碼難以維護。常見的解決辦法是使用一個變量來代替,如:
const shapeType = {triangle: 'Triangle' };switch (shape) {case shapeType.triangle: //消除了一個魔術字符串area = .5 * options.width * options.height;break;/* ... more code ... */}實際上,變量triangle的值等于什么并不重要,我們的真正目的是比較switch內的變量shape與shapeType.triangle是否相等,而不在乎它們的值是否都是“Triangle”。因此上述對象shapeType可以用下面的對象代替:
const shapeType = {triangle: Symbol() };只要shapeType的所有屬性值都不相等,就不需要對代碼做其他修改。
三、Symbol的語法規范
1. 基本語法
上面介紹到,使用如下語法即可創建一個Symbol變量:
var s = Symbol();由于Symbol不是繼承自Object,因此不可以使用new關鍵字來生成Symbol變量。使用上述語句創建的變量s,在控制臺中進行輸出時會顯示為Symbol()。假如有另一個變量:
var b = Symbol();console.log(s); //Symbol() console.log(b); //Symbol()變量s和變量b并不是同一個值,但它們在控制臺的輸出卻是一樣的,這樣不利于我們區分兩個變量。為此,我們可以在調用Symbol的時候傳入一個字符串作為對當前Symbol變量的描述:
var s = Symbol("symbol1"); var b = Symbol("symbol2");console.log(s); //Symbol("symbol1") console.log(b); //Symbol("symbol2")現在我們可以在控制臺中區分開變量s和變量b了。
需要注意的是,使用相同描述符的兩個Symbol并不相等:
var s = Symbol("s"); var b = Symbol("s");s !== b;打個比方,即使兩個碗都被叫做碗,它們仍然不是同一個碗。同理,描述符也僅僅是對Symbol變量的一個描述而已。
如果你希望得到一個Symbol的描述符,可以借助Symbol原型上的description屬性(Symbol.prototype.description):
const s = Symbol("symbol");console.log(s.description); //symbolSymbol還可以顯式的轉化為字符串或布爾值,但是不能轉化為數值:
let sym = Symbol('My symbol'); String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)'let sym2 = Symbol(); Boolean(sym2) // true2. Symbol屬性的遍歷
以Symbol類型的變量作為對象屬性時,該屬性不會出現在for … in、for … of循環中,也不會被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
但該屬性并不是私有屬性,它可以被專門的Object.getOwnPropertySymbols()方法遍歷出來。該方法返回一個數組,包含了當前對象的所有用作屬性名的Symbol值:
var s1 = Symbol('a'); var s2 = Symbol('b');var a = {name: "夕山雨",[s1]: 24,[s2]: function(){} }var s = Object.getOwnPropertySymbols(a); //[Symbol(a), Symbol(b)] a[s[0]] = 24; //返回的數組元素不是字符串,而是實際的Symbol值,//因此可以通過它引用到對象的該屬性因此遍歷該方法的返回值即可遍歷所有的Symbol屬性。
另外,ES6新增的Reflect.ownKeys()方法可以遍歷出所有的常規鍵名和Symbol鍵名。語法為:
Reflect.ownKeys(a); //["name", Symbol(a), Symbol(b)]3. Symbol.for(),Symbol.keyFor()
Symbol提供了一種可以創建相同Symbol的機制,就是使用Symbol.for()方法進行注冊。通過該方法生成的Symbol會根據描述符進行全局注冊,之后再次通過Symbol.for()傳入相同的描述符時,就可以得到相同的Symbol值。如:
var s1 = Symbol.for('symbol'); //向全局注冊了以"symbol"為描述符的Symbol //由于描述符"symbol"已被注冊到全局,因此這里創建的Symbol與上面是同一個 var s2 = Symbol.for('symbol'); s1 === s2;這里指的全局不單指該變量所在的作用域,它在各個iframe甚至service worker中都是有效的,因此這是一種允許不同作用域創建相同Symbol的機制。
如果你想得到一個全局注冊的Symbol的描述符,可以使用Symbol.keyFor()方法:
Symbol.keyFor(s1); //"symbol"它輸出了變量s1的全局注冊標識符“symbol”。
四、內置的Symbol值
上面講到的Symbol的用法都是自定義的Symbol,在ES6中還定義了11個內置的Symbol,用于指向語言內部使用的方法。
1. Symbol.hasInstance
當使用instanceof運算符判斷某個對象是否為某個構造函數的實例時,就是在調用該構造函數上的靜態方法[Symbol.hasInstance],它是js引擎預先定義好的。如:
[] instanceof Array; //true//瀏覽器實際上是在調用下面的方法 Array[Symbol.hasInstance]([]);如果你想要看一下瀏覽器是如何實現該方法的,非常抱歉,你只會得到這樣的輸出:
< Array[Symbol.hasInstance];> ? [Symbol.hasInstance]() { [native code] }native code表示當前函數是使用本地代碼實現的,通常是C++或C,因此瀏覽器不會直接輸出它的源代碼。
實際上,instanceof右側不要求一定是構造函數,也可以是一個普通的對象,只要該對象實現了[Symbol.hasInstance]方法即可。如:
//1. 使用構造函數 function F(){this[Symbol.hasInstance] = function(obj){return obj.constructor === F;} } var f = new F(); f instanceof new F(); //true//2. 使用class class MyClass {[Symbol.hasInstance](foo) {return foo instanceof Array;} }[1, 2, 3] instanceof new MyClass() // true//3. 直接使用一個實現了Symbol.hasInstance的對象 var a = {[Symbol.hasInstance](foo) {return foo instanceof Array;} }[1, 2, 3] instanceof a // true總的來說,instanceof的行為就是,遇到a instanceof b這樣的語句,就調用b[Symbol.hasInstance](a),該函數的返回值就是該語句的返回值。這里如果b是構造函數,就調用它的靜態方法,如果是對象,就調用它的實例方法或原型方法。
不過,如果instanceof右側不包含[Symbol.hasInstance]方法,那么瀏覽器會拋出這樣的錯誤:Right-hand side of ‘instanceof’ is not callable,表示右側不可被instanceof運算符調用。
2. Symbol.isConcatSpreadable
該屬性決定了當前對象作為concat的參數時是否可以展開。通常:
var obj = {age: 24}; [1].concat(obj); //[1, {age: 24}]obj被傳入concat后會直接作為一個元素添加到數組中。通過將obj的Symbol.isConcatSpreadable屬性設置為true,obj會在執行concat時嘗試展開,如果該對象無法展開,obj不會被拼接到數組中去。所謂的可展開,指的是obj是否為數組或類數組結構。如果obj是數組,顯然是可展開的,如果它有length屬性,并且有"0","1"這樣的屬性鍵,那么它就是類數組,也是可以展開的:
//設置了該對象需要展開,但它無法展開,因此最終結果為[] var obj = {age: 24, [Symbol.isConcatSpreadable]: true}; [].concat(obj); //[]//這是一個類數組對象,它是可展開的 var obj = {length: 2, "0": 24, "1": 25, name: "夕山雨",[Symbol.isConcatSpreadable]: true }; //name屬性被丟棄了,因為它無法被obj[index]的方式引用到 [].concat(obj); //[24, 25]3. Symbol.species
該屬性用于在繼承的時候指定一個類的類別。如:
class T1 extends Promise { }class T2 extends Promise {static get [Symbol.species]() {return Promise;} }new T1() instanceof T1 // true new T2() instanceof T2 // false對于T1,由它構造出的實例默認都是T1的實例。而在T2中我們為該類定義了[Symbol.species]方法,它始終返回Promise,因此由T2構造出的實例都不再被認為是T2的實例,而是Promise的實例。
該方法允許我們在定義衍生對象時,人為指定由它構造出的實例的構造函數。
4. Symbol.match/replace/search/split
這四個方法允許我們以對象的方式自定義String的match、replace、search、split方法。以match為例,我們通常這樣調用它:
var s = "hello"; s.match(RegExp); //匹配一個正則表達式假如我們需要為當前的字符串s定制一個自己的match方法,但是又不希望修改String原型上的match方法(因為這樣會影響到其他的字符串調用match方法)。Symbol.match就為我們提供了這種能力。
對于上面的例子,如果傳入的對象具有[Symbol.match]方法,那么js引擎就會修改match方法默認的行為,去調用定義的[Symbol.match]方法。如:
var a = {[Symbol.match](){return true;} }"hello".match(a); //true當調用字符串的match方法并傳入具有[Symbol.match]屬性的對象時,js引擎就會調用對象的這個方法。
上面的寫法等同于下面的寫法:
a[Symbol.match]("hello"); //truereplace、search和split也是相同的原理。下面分別給一個簡單的例子:
replace:
由于replace的第一個參數有[Symbol.replace]方法,因此js引擎會調用這個方法,并把調用者‘Hello’和第二個參數‘World’作為參數傳遞給該方法。這樣,上面的寫法就等同于:
x[Symbol.replace]("Hello", "world");search:
var a = {[Symbol.match](){return true;} }"hello".search(a); //true原理同match。
split:
原理也與match相同。
5. Symbol.iterator
定義一個對象的遍歷器方法。凡是具有[Symbol.iterator]方法的對象都是可遍歷的,可以使用for … of循環依次輸出對象的每個屬性。數組和類數組,以及ES6新增的Map、Set等都原生部署了該方法,因此它們都可遍歷。如:
for(var item of [1,2,3]){console.log(item); //依次輸出1,2,3 }任何一個數組都具備這個原生的遍歷器方法:
> [][Symbol.iterator]< ? values() { [native code] } //C++實現普通對象默認不具有該遍歷器方法,因此無法用for … of循環遍歷出對象所有的屬性值。如果你希望讓普通對象可遍歷,可以手動為該對象定義遍歷器方法,如:
var a = {name: "夕山雨",age: 24,[Symbol.iterator]: function* (){yield this.name;yield this.age;} }這里為了簡單,使用了ES6的Generator函數,它定義該遍歷器先輸出name屬性,再輸出age屬性。因此當你用for … of來輸出a的屬性值時,就可以得到結果:
for(var item of a){console.log(item); //依次輸出:"夕山雨" 24 }iterator是ES6非常重要的概念,我們后續會有專門的文章來介紹它。
6. Symbol.toPrimitive
該方法定義了一個對象如何被轉化為一個基本數據類型。通常對象是不能直接與基本數據類型的變量進行運算的,但是如果你為它定義了[Symbol.toPrimitive]方法,它就可以按照你所指定的規則轉化為基本數據類型。它接收一個字符串,表示需要轉換成的數據類型:
let obj = {[Symbol.toPrimitive](hint) {switch (hint) {case 'number':return 123;case 'string':return 'str';case 'default':return 'default';default:throw new Error();}} };2 * obj // 246 3 + obj // '3default' obj == 'default' // true String(obj) // 'str'這里表示,如果對象需要轉化為數字,就返回123;如果需要轉化為字符串,就轉化為’str’;如果沒有指定要轉化的類型,那就返回字符串’Default’。
由于乘法運算*只能對數值操作,因此js引擎會調用[Symbol.toPrimitive]并傳入"number",將obj轉化為數字。而加法既可以對數值生效,也可以對字符串生效,因此js引擎傳入了"default"。該方法默認只接受number、string和default這三個值。
7. Symbol.toStringTag
可以自定義對象的toString()方法。通常對象的toString方法會返回一個類似[object Object]的字符串,表示該對象的類型,如:
var a = {};a.toString(); //"[object Object]"但是如果你修改了對象的Symbol.toStringTag方法,返回值就會發生變化:
a[Symbol.toStringTag] = function(){return "xxx"; } a.toString(); //"[object xxx]"可以看到,我們定義的返回值覆蓋了之前的字符串中的后半部分“Object”,因此該方法可以用于定制對象的toString()的返回值。
8. Symbol.unscopables
該方法用于with語句。它指定在使用with語句時,哪些屬性不屬于with環境。舉個例子:
var author = {name: "夕山雨",age: 24,stature: "179",weight: 65 }var name = "張三"; var age = "28";with(author){console.log(name); //“夕山雨”console.log(age); //24console.log(stature); //"179"console.log(weight); //65 }默認情況下,對于with語句內引用的變量,js引擎會優先去with的作用對象上查找對應的屬性,如果找不到,才認為是外部變量。但是你可以人為指定哪些屬性不應該去作用對象上查找,如:
var author = {name: "夕山雨",age: 24,stature: "179",weight: 65,get [Symbol.unscopables](){return { name: true, age: true }} }var name = "張三"; var age = "28"; var stature = "153"; var weight = 80;with(author){console.log(name); //“張三”console.log(age); //28console.log(stature); //"179"console.log(weight); //65 }可以看到,由于我們認為指定了name和age兩個屬性不作用域with環境,因此這里的name和age輸出的是外部的變量,而stature和weight輸出的仍然是author的屬性值。
總結
Symbol作為一種新的數據類型,有著與String相似的特性,與String不同的是它是獨一無二的,因此適合作為對象屬性的鍵值,防止該屬性被覆蓋。除了自定義的Symbol值外,靈活掌握內置的Symbol,對ES6的學習有帶來極大幫助,特別是Symbol.iterator,它是ES6中的一個非常重要的概念,之后會繼續探討。
總結
以上是生活随笔為你收集整理的ES6之Symbol详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QAQ ORZ
- 下一篇: 数值分析龙贝格matlab,MATLAB