ES6中關於數據類型的拓展:Symbol類型

  ES5中包含5種原始類型:字符串、數值、布爾值、null、undefined。ES6引入了第6種原始類型——Symbol。jquery

  ES5的對象屬性名都是字符串,很容易形成屬性名衝突。好比,使用了一個他人提供的對象,想爲這個對象添加新的方法,新方法的名字就有可能與現有方法產生衝突。若是有一種機制保證每一個屬性的名字都是獨一無二的,這樣就從根本上防止了屬性名衝突。這就是ES6引入Symbol的緣由數組

1、建立

  Symbol 值經過Symbol函數生成。這就是說,對象的屬性名能夠有兩種類型:一種是字符串,另外一種是Symbol類型。凡是屬性名屬於 Symbol 類型,就都是獨一無二的,能夠保證不會與其餘屬性名產生衝突app

let firstName = Symbol(); let person = {}; person[firstName] = "huochai"; console.log(person[firstName]); // "huochai"

  注意:Symbol函數前不能使用new命令,不然會報錯。由於生成的 Symbol 是一個原始類型的值,不是對象函數

  Symbol函數接受一個可選參數,能夠添加一段文原本描述即將建立的Symbol,這段描述不可用於屬性訪問,可是建議在每次建立Symbol時都添加這樣一段描述,以便於閱讀代碼和調試Symbol程序ui

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]]this

  【類型檢測】編碼

  Symbol是原始值,ES6擴展了typeof操做符,返回"symbol"。因此能夠用typeof來檢測變量是否爲symbol類型spa

let symbol = Symbol("test symbol"); console.log(typeof symbol); // "symbol"

2、使用

  因爲每個Symbol值都是不相等的,這意味着Symbol值能夠做爲標識符,用於對象的屬性名,就能保證不會出現同名的屬性。這對於一個對象由多個模塊構成的狀況很是有用,能防止某一個鍵被不當心改寫或覆蓋prototype

  全部使用可計算屬性名的地方,均可以使用Symbol3d

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,須要創建一個體系

3、共享體系

  有時但願在不一樣的代碼中共享同一個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"或其餘相似的鍵

4、類型轉換

  類型轉換是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

5、屬性檢索

  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)都沒法獲取它。這就形成了一種非私有的內部方法的效果

6、內置Symbol

  除了定義本身使用的Symbol值之外,ES6還提供了11個內置的Symbol值,指向語言內部使用的方法

  一、Symbol.haslnstance:一個在執行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:一個在調用Object.prototype.toString()方法時使用的字符串,用於建立對象描述

  十一、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)

六、【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被調用時,會接受一個字符串參數,表示當前運算的模式,一共有三種模式

  一、Number:該場合須要轉成數值

  二、String:該場合須要轉成字符串

  三、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環境排除。

相關文章
相關標籤/搜索