[1]創建 [2]使用 [3]共用體系 [4]類型轉換 [5]屬性檢索 [6]內置Symbol ...
前面的話
ES5中包含5種原始類型:字元串、數字、布爾值、null和undefined。ES6引入了第6種原始類型——Symbol
ES5的對象屬性名都是字元串,很容易造成屬性名衝突。比如,使用了一個他人提供的對象,想為這個對象添加新的方法,新方法的名字就有可能與現有方法產生衝突。如果有一種機制,保證每個屬性的名字都是獨一無二的,這樣就從根本上防止了屬性名衝突。這就是ES6引入Symbol
的原因,本文將詳細介紹ES6中的Symbol類型
創建
Symbol 值通過Symbol
函數生成。這就是說,對象的屬性名可以有兩種類型:一種是字元串,另一種是Symbol類型。凡是屬性名屬於 Symbol 類型,就都是獨一無二的,可以保證不會與其他屬性名產生衝突
let firstName = Symbol(); let person = {}; person[firstName] = "huochai"; console.log(person[firstName]); // "huochai"
[註意]Symbol
函數前不能使用new
命令,否則會報錯。因為生成的 Symbol 是一個原始類型的值,不是對象
//Uncaught TypeError: Symbol is not a constructor let firstName = new Symbol();
Symbol函數接受一個可選參數,可以添加一段文本來描述即將創建的Symbol,這段描述不可用於屬性訪問,但是建議在每次創建Symbol時都添加這樣一段描述,以便於閱讀代碼和調試Symbol程式
let firstName = Symbol("first name"); let person = {}; person[firstName] = "huochai"; console.log("first name" in person); // false console.log(person[firstName]); // "huochai" console.log(firstName); // "Symbol(first name)"
Symbol的描述被存儲在內部[[Description]]屬性中,只有當調用Symbol的toString()方法時才可以讀取這個屬性。在執行console.log()時隱式調用了firstName的toString()方法,所以它的描述會被列印到日誌中,但不能直接在代碼里訪問[[Description]]
【類型檢測】
Symbol是原始值,ES6擴展了typeof操作符,返回"symbol"。所以可以用typeof來檢測變數是否為symbol類型
let symbol = Symbol("test symbol"); console.log(typeof symbol); // "symbol"
使用
由於每一個Symbol值都是不相等的,這意味著Symbol值可以作為標識符,用於對象的屬性名,就能保證不會出現同名的屬性。這對於一個對象由多個模塊構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋
所有使用可計算屬性名的地方,都可以使用Symbol
let firstName = Symbol("first name"); // 使用一個需計算字面量屬性 let person = { [firstName]: "huochai" }; // 讓該屬性變為只讀 Object.defineProperty(person, firstName, { writable: false }); let lastName = Symbol("last name"); Object.defineProperties(person, { [lastName]: { value: "match", writable: false } }); console.log(person[firstName]); // "huochai" console.log(person[lastName]); // "match"
在此示例中,首先通過可計算對象字面量屬性語法為person對象創建了個Symbol屬性firstName。後面一行代碼將這個屬性設置為只讀。隨後,通過Object.defineProperties()方法創建一個只讀的Symbol屬性lastName,此處再次使用了對象字面量屬性,但卻是作為object.defineProperties()方法的第二個參數使用
[註意]Symbol 值作為對象屬性名時,不能用點運算符
var mySymbol = Symbol(); var a = {}; a.mySymbol = 'Hello!'; a[mySymbol] // undefined a['mySymbol'] // "Hello!"
由上面結果看出,a.mySymbol和a['mySymbol']里的mySymbol是字元串類型的屬性名,a[mySymbol]里的mySymbol才是Symbol類型的屬性名。雖然都叫mySymbol,但值不相同
儘管在所有使用可計算屬性名的地方,都可以使用Symbol來代替,但是為了在不同代碼片段間有效地共用這些Symbol,需要建立一個體系
共用體系
有時希望在不同的代碼中共用同一個Symbol,例如,在應用中有兩種不同的對象類型,但是希望它們使用同一個Symbol屬性來表示一個獨特的標識符。一般而言,在很大的代碼庫中或跨文件追蹤Symbol非常困難而且容易出錯,出於這些原因,ES6提供了一個可以隨時訪問的全局Symbol註冊表
【Symbol.for()】
如果想創建一個可共用的Symbol,要使用Symbol.for()方法。它只接受一個參數,也就是即將創建的Symbol的字元串標識符,這個參數同樣也被用作Symbol的描述
let uid = Symbol.for("uid"); let object = {}; object[uid] = "12345"; console.log(object[uid]); // "12345" console.log(uid); // "Symbol(uid)"
Symbol.for()方法首先在全局Symbol註冊表中搜索鍵為"uid"的Symbol是否存在。如果存在,直接返回已有的Symbol,否則,創建一個新的Symbol,並使用這個鍵在Symbol全局註冊表中註冊,隨即返回新創建的Symbol
後續如果再傳入同樣的鍵調用Symbol.for()會返回相同的Symbol
let uid = Symbol.for("uid"); let object = { [uid]: "12345" }; console.log(object[uid]); // "12345" console.log(uid); // "Symbol(uid)" let uid2 = Symbol.for("uid"); console.log(uid === uid2); // true console.log(object[uid2]); // "12345" console.log(uid2); // "Symbol(uid)
在這個示例中,uid和uid2包含相同的Symbol並且可以互換使用。第一次調用Symbol.for()方法創建這個Symbol,第二次調用可以直接從Symbol的全局註冊表中檢索到這個Symbol
【Symbol.keyFor()】
還有一個與Symbol共用有關的特性:可以使用Symbol.keyFor()方法在Symbol全局註冊表中檢索與Symbol有關的鍵
let uid = Symbol.for("uid"); console.log(Symbol.keyFor(uid)); // "uid" let uid2 = Symbol.for("uid"); console.log(Symbol.keyFor(uid2)); // "uid" let uid3 = Symbol("uid"); console.log(Symbol.keyFor(uid3)); // undefined
uid和uid2都返回了"uid"這個鍵,而在Symbol全局註冊表中不存在uid3這個Symbol,也就是不存在與之有關的鍵,所以最終返回undefined
[註意]Symbol.for
為Symbol值登記的名字,是全局環境的,可以在不同的 iframe 或 service worker 中取到同一個值
let iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); console.log(iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo'));// true
上面代碼中,iframe 視窗生成的 Symbol 值,可以在主頁面得到
Symbol全局註冊表是一個類似全局作用域的共用環境,也就是說不能假設目前環境中存在哪些鍵。當使用第三方組件時,儘量使用Symbol鍵的命名空間以減少命名衝突。例如,jQuery的代碼可以為所有鍵添加"jquery"首碼,就像"jquery.element"或其他類似的鍵
類型轉換
類型轉換是JS中的一個重要語言特性,然而其他類型沒有與Symbol邏輯等價的值,因而Symbol使用起來不是很靈活
使用console.log()方法來輸出Symbol的內容,它會調用Symbol的String()方法並輸出有用的信息。也可以像這樣直接調用string()方法來獲得相同的內容
let uid = Symbol.for("uid"), desc = String(uid); console.log(desc); // "Symbol(uid)"
String()函數調用了uid.toString()方法,返回字元串類型的Symbol描述里的內容。但是,如果嘗試將Symbol與一個字元串拼接,會導致程式拋出錯誤
let uid = Symbol.for("uid"), desc = uid + ""; // 引發錯誤!
將uid與空字元串拼接,首先要將uid強制轉換為一個字元串,而Symbol不可以被轉換為字元串,故程式直接拋出錯誤
同樣,也不能將Symbol強制轉換為數字類型。將Symbol與每一個數學運算符混合使用都會導致程式拋出錯誤
let uid = Symbol.for("uid"), sum = uid / 1; // 引發錯誤!
嘗試將Symbol除1,程式直接拋出錯誤。而且無論使用哪一個數學操作符,都無法正常運行
[註意]布爾值除外,因為Symbol與JS中的非空值類似,其等價布爾值為true
let uid = Symbol.for("uid"); console.log(uid);//'Symbol(uid)' console.log(!uid);//false console.log(Boolean(uid));//true
屬性檢索
Symbol作為屬性名,該屬性不會出現在for...in、for...of迴圈中,也不會被Object.getOwnPropertyNames()、Object.keys()、JSON.stringify()返回。於是,在ES6中添加了一個Object.getOwnpropertySymbols()方法來檢索對象中的Symbol屬性
Object.getOwnPropertySymbols()方法的返回值是一個包含所有Symbol自有屬性的數組
let uid = Symbol.for("uid"); let object = { [uid]: "12345" }; let symbols = Object.getOwnPropertySymbols(object); console.log(symbols.length); // 1 console.log(symbols[0]); // "Symbol(uid)" console.log(object[symbols[0]]); // "12345"
在這段代碼中,object對象有一個名為uid的Symbol屬性,object.getOwnPropertySymbols()方法返回了包含這個屬性的數組
另一個新的API——Reflect.ownKeys()
方法可以返回所有類型的鍵名,包括常規鍵名和 Symbol 鍵名
let obj = { [Symbol('my_key')]: 1, enum: 2, nonEnum: 3 }; console.log(Reflect.ownKeys(obj));// ["enum", "nonEnum", Symbol(my_key)]
由於以 Symbol 值作為名稱的屬性,不會被常規方法遍歷得到。可以利用這個特性,為對象定義一些非私有的、但又希望只用於內部的方法
var size = Symbol('size'); class Collection { constructor() { this[size] = 0; } add(item) { this[this[size]] = item; this[size]++; } static sizeOf(instance) { return instance[size]; } } var x = new Collection(); Collection.sizeOf(x) // 0 x.add('foo'); Collection.sizeOf(x) // 1 Object.keys(x) // ['0'] Object.getOwnPropertyNames(x) // ['0'] Object.getOwnPropertySymbols(x) // [Symbol(size)]
上面代碼中,對象x的size屬性是一個Symbol值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都無法獲取它。這就造成了一種非私有的內部方法的效果
內置Symbol
除了定義自己使用的Symbol值以外,ES6還提供了11個內置的Symbol值,指向語言內部使用的方法
1、Symbol.haslnstance
一個在執行instanceof時調用的內部方法,用於檢測對象的繼承信息
2、Symbol.isConcatSpreadable
一個布爾值,用於表示當傳遞一個集合作為Array.prototype.concat()方法的參數時,是否應該將集合內的元素規整到同一層級
3、Symbol.iterator
一個返回迭代器的方法
4、Symbol.match
一個在調用String.prototype.match()方法時調用的方法,用於比較字元串
5、Symbol.replace
一個在調用String.prototype.replace()方法時調用的方法,用於替換字元串的子串
6、Symbol.search
一個在調用String.prototype.search()方法時調用的方法,用於在字元串中定位子串
7、Symbol.species
用於創建派生類的構造函數
8、Symbol.split
一個在調用String.prototype.split()方法時調用的方法,用於分割字元串
9、Symbol.toprimitive
一個返回對象原始值的方法
10、Symbol.ToStringTag
一個在調用Object.prototype.toString()方法時使用的字元串,用於創建對象描述
11、Symbol.unscopables
一個定義了一些不可被with語句引用的對象屬性名稱的對象集合
【Symbol.haslnstance】
每個函數都有一個Symbol.haslnstance方法,用於確定對象是否為函數的實例。該方法在Function.prototype中定義,所有函數都繼承了instanceof屬性的預設行為。為了確保Symbol.haslnstance不會被意外重寫,該方法被定義為不可寫、不可配置並且不可枚舉
Symbol.haslnstance方法只接受一個參數,即要檢查的值。如果傳入的值是函數的實例,則返回true
obj instanceof Array;
以上這行代碼等價於下麵這行
Array[Symbol.hasInstance](obj);
本質上,ES6只是將instanceof操作符重新定義為此方法的簡寫語法。現在引入方法調用後,就可以隨意改變instanceof的運行方式了
class MyClass { [Symbol.hasInstance](foo) { return foo instanceof Array; } } console.log([1, 2, 3] instanceof new MyClass()); // true
假設定義一個無實例的函數,就可以將Symbol.haslnstance的返回值硬編碼為false
function MyObject() { // ... } Object.defineProperty(MyObject, Symbol.hasInstance, { value: function(v) { return false; } }); let obj = new MyObject(); console.log(obj instanceof MyObject); // false
只有通過Object.defineProperty()方法才能夠改寫一個不可寫屬性,上面的示例調用這個方法來改寫symbol.haslnstance,為其定義一個總是返回false的新函數,即使obj實際上確實是Myobject類的實例,在調用過object.defineProperty()方法之後,instanceof運算符返回的也是false
當然,也可以基於任意條件,通過值檢查來確定被檢測的是否為實例。例如,可以將1~100的數字定義為一個特殊數字類型的實例,具體實現的代碼如下
function SpecialNumber() { // empty } Object.defineProperty(SpecialNumber, Symbol.hasInstance, { value: function(v) { return (v instanceof Number) && (v >=1 && v <= 100); } }); let two = new Number(2), zero = new Number(0); console.log(two instanceof SpecialNumber); // true console.log(zero instanceof SpecialNumber); // false
在這段代碼中定義了一個symbol.hasInstance方法,當值為Number的實例且其值在1~100之間時返回true。所以即使SpecialNumber函數和變數two之間沒有直接關係,變數two也被確認為specialNumber的實例
如果要觸發Symbol.haslnstance調用,instanceof的左操作數必須是一個對象,如果左操作數為非對象會導致instanceof總是返回false
當然,可以重寫所有內建函數(如Date和Error函數)預設的symbol.haslnstance屬性。但是這樣做的後果是代碼的運行結果變得不可預期且有可能令人感到困惑,所以不推薦這樣做,最好的做法是,只在必要情況下改寫自己聲明的函數的Symbol.haslnstance屬性
【Symbol.isConcatSpreadable】
對象的Symbol.isConcatSpreadable屬性是布爾值,表示該對象使用Array.prototype.concat()時,是否可以展開
let arr1 = ['c', 'd']; ['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e'] arr1[Symbol.isConcatSpreadable] // undefined let arr2 = ['c', 'd']; arr2[Symbol.isConcatSpreadable] = false; ['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
上面代碼說明,數組的預設行為是可以展開。Symbol.isConcatSpreadable
屬性等於undefined或true,都有這個效果
類數組對象也可以展開,但它的Symbol.isConcatSpreadable
屬性預設為false
,必須手動打開
let obj = {length: 2, 0: 'c', 1: 'd'}; ['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e'] obj[Symbol.isConcatSpreadable] = true; ['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
對於一個類來說,Symbol.isConcatSpreadable
屬性必須寫成實例的屬性
class A1 extends Array { constructor(args) { super(args); this[Symbol.isConcatSpreadable] = true; } } class A2 extends Array { constructor(args) { super(args); this[Symbol.isConcatSpreadable] = false; } } let a1 = new A1(); a1[0] = 3; a1[1] = 4; let a2 = new A2(); a2[0] = 5; a2[1] = 6; [1, 2].concat(a1).concat(a2) // [1, 2, 3, 4, [5, 6]]
上面代碼中,類A1
是可展開的,類A2
是不可展開的,所以使用concat
時有不一樣的結果
【Symbol.species】
對象的Symbol.species
屬性,指向當前對象的構造函數。創造實例時,預設會調用這個方法,即使用這個屬性返回的函數當作構造函數,來創造新的實例對象
class MyArray extends Array { // 覆蓋父類 Array 的構造函數 static get [Symbol.species]() { return Array; } }
上面代碼中,子類MyArray
繼承了父類Array
。創建MyArray
的實例對象時,本來會調用它自己的構造函數,但是由於定義了Symbol.species
屬性,所以會使用這個屬性返回的的函數,創建MyArray
的實例
這個例子也說明,定義Symbol.species
屬性要採用get
讀取器。預設的Symbol.species
屬性等同於下麵的寫法
static get [Symbol.species]() { return this; }
下麵是一個例子
class MyArray extends Array { static get [Symbol.species]() { return Array; } } var a = new MyArray(1,2,3); var mapped = a.map(x => x * x); mapped instanceof MyArray // false mapped instanceof Array // true
上面代碼中,由於構造函數被替換成了Array
。所以,mapped
對象不是MyArray
的實例,而是Array
的實例
【Symbol.match】
對象的Symbol.match
屬性,指向一個函數。當執行str.match(myObject)
時,如果該屬性存在,會調用它,返回該方法的返回值
String.prototype.match(regexp) // 等同於 regexp[Symbol.match](this) class MyMatcher { [Symbol.match](string) { return 'hello world'.indexOf(string); } } 'e'.match(new MyMatcher()) // 1
【Symbol.replace】
對象的Symbol.replace
屬性,指向一個方法,當該對象被String.prototype.replace
方法調用時,會返回該方法的返回值
String.prototype.replace(searchValue, replaceValue) // 等同於 searchValue[Symbol.replace](this, replaceValue)
下麵是一個例子
const x = {}; x[Symbol.replace] = (...s) => console.log(s); 'Hello'.replace(x, 'World') // ["Hello", "World"]
Symbol.replace
方法會收到兩個參數,第一個參數是replace
方法正在作用的對象,上面例子是Hello
,第二個參數是替換後的值,上面例子是World
【Symbol.search】
對象的Symbol.search
屬性,指向一個方法,當該對象被String.prototype.search
方法調用時,會返回該方法的返回值
String.prototype.search(regexp) // 等同於 regexp[Symbol.search](this) class MySearch { constructor(value) { this.value = value; } [Symbol.search](string) { return string.indexOf(this.value); } } 'foobar'.search(new MySearch('foo')) // 0
【Symbol.split】
對象的Symbol.split
屬性,指向一個方法,當該對象被String.prototype.split
方法調用時,會返回該方法的返回值
String.prototype.split(separator, limit) // 等同於 separator[Symbol.split](this, limit)
下麵是一個例子
class MySplitter { constructor(value) { this.value = value; } [Symbol.split](string) { var index = string.indexOf(this.value); if (index === -1) { return string; } return [ string.substr(0, index), string.substr(index + this.value.length) ]; } } 'foobar'.split(new MySplitter('foo'))// ['', 'bar'] 'foobar'.split(new MySplitter('bar'))// ['foo', ''] 'foobar'.split(new MySplitter('baz'))// 'foobar'
上面方法使用Symbol.split
方法,重新定義了字元串對象的split
方法的行為
【Symbol.iterator】
對象的Symbol.iterator
屬性,指向該對象的預設遍歷器方法
var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3]
對象進行for...of
迴圈時,會調用Symbol.iterator
方法,返回該對象的預設遍歷器
class Collection { *[Symbol.iterator]() { let i = 0; while(this[i] !== undefined) { yield this[i]; ++i; } } } let myCollection = new Collection(); myCollection[0] = 1; myCollection[1] = 2; for(let value of myCollection) { console.log(value); } // 1 // 2
【Symbol.toPrimitive】
對象的Symbol.toPrimitive
屬性,指向一個方法。該對象被轉為原始類型的值時,會調用這個方法,返回該對象對應的原始類型值
Symbol.toPrimitive
被調用時,會接受一個字元串參數,表示當前運算的模式,一共有三種模式
1、Number:該場合需要轉成數值
2、String:該場合需要轉成字元串
3、Default:該場合可以轉成數值,也可以轉成字元串
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'
【String.toStringTag】
對象的Symbol.toStringTag
屬性,指向一個方法。在該對象上面調用Object.prototype.toString
方法時,如果這個屬性存在,它的返回值會出現在toString
方法返回的字元串之中,表示對象的類型。也就是說,這個屬性可以用來定製[object Object]
或[object Array]
中object
後面的那個字元串
// 例一 ({[Symbol.toStringTag]: 'Foo'}.toString()) // "[object Foo]" // 例二 class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } var x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]"
ES6新增內置對象的Symbol.toStringTag
屬性值如下、
JSON[Symbol.toStringTag]:'JSON' Math[Symbol.toStringTag]:'Math' Module[Symbol.toStringTag]:'Module' ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer' DataView.prototype[Symbol.toStringTag]:'DataView' Map.prototype[Symbol.toStringTag]:'Map' Promise.prototype[Symbol.toStringTag]:'Promise' Set.prototype[Symbol.toStringTag]:'Set' %TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array' WeakMap.prototype[Symbol.toStringTag]:'WeakMap' WeakSet.prototype[Symbol.toStringTag]:'WeakSet' %MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator' %SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator' %StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator' Symbol.prototype[Symbol.toStringTag]:'Symbol' Generator.prototype[Symbol.toStringTag]:'Generator' GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'
【Symbol.unscopables】
對象的Symbol.unscopables
屬性,指向一個對象。該對象指定了使用with
關鍵字時,哪些屬性會被with
環境排除。
Array.prototype[Symbol.unscopables] // { // copyWithin: true, // entries: true, // fill: true, // find: true, // findIndex: true, // includes: true, // keys: true // } Object.keys(Array.prototype[Symbol.unscopables]) // ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
上面代碼說明,數組有7個屬性,會被with
命令排除
// 沒有 unscopables 時 class MyClass { foo() { return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 1 } // 有 unscopables 時 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 2 }
上面代碼通過指定Symbol.unscopables
屬性,使得with
語法塊不會在當前作用域尋找foo
屬性,即foo
將指向外層作用域的變數