1. 程式人生 > 實用技巧 >詳解Symbol(自定義值,內建值)

詳解Symbol(自定義值,內建值)

ES6 引入了一種新的原始資料型別 Symbol,表示獨一無二的值。它是
JavaScript 語言的第七種資料型別

Symbol 特點:

1.Symbol 的值是唯一的,用來解決命名衝突的問題,即使引數相同

 1 // 沒有引數的情況
 2 let name1 = Symbol();
 3 let name2 = Symbol();
 4 name1 === name2  // false
 5 name1 === name2 // false
 6 
 7 // 有引數的情況
 8 let name1 = Symbol('flag');
 9 let name2 = Symbol('flag');
10 name1 === name2  //
false 11 name1 === name2 // false

2.Symbol 值不能與其他資料進行運算

 - 數學計算:不能轉換為數字
 - 字串拼接:隱式轉換不可以,但是可以顯示轉換
 - 模板字串

3) Symbol 定義的物件屬性不參與 for…in/of 遍歷,但是可以使用
Reflect.ownKeys / Object.getOwnPropertySymbols()來獲取物件的所有鍵名

 1 let sy = Symbol();
 2 let obj = {
 3     name:"zhangsan",
 4     age:21
 5 };
 6 obj[sy] = "symbol";
7 console.log(obj); //{name: "zhangsan", age: 21, Symbol(): "symbol"} 8 9 for(let key in obj) { 10 console.log(key); 11 } //只輸出了name,age 12 13 14 15 Object.getOwnPropertySymbols(obj); //[Symbol()] 16 Reflect.ownKeys(obj); //["name", "age", Symbol()] 17 18 Object.keys(obj); //
["name", "age"] 19 Object.getOwnPropertyNames(obj) //["name", "age"] 20 Object.keys(obj) //["name", "age"] 21 Object.values(obj) //["zhangsan", 21] 22 JSON.stringify(obj) //{"name":"zhangsan","age":21}

注: 遇到唯一性的場景時要想到 Symbol

Symbol的方法:

1.Symbol.for()
作用:用於將描述相同的Symbol變數指向同一個Symbol值,這樣的話,就方便我們通過描述(標識)區分開不同的Symbol了,閱讀起來方便

Symbol.for("foo"); // 建立一個 symbol 並放入 symbol 登錄檔中,鍵為 "foo"
Symbol.for("foo"); // 從 symbol 登錄檔中讀取鍵為"foo"的 symbol


Symbol.for("bar") === Symbol.for("bar"); // true,證明了上面說的
Symbol("bar") === Symbol("bar"); // false,Symbol() 函式每次都會返回新的一個 symbol


var sym = Symbol.for("mario");
sym.toString(); 
// "Symbol(mario)",mario 既是該 symbol 在 symbol 登錄檔中的鍵名,又是該 symbol 自身的描述字串

Symbol()和Symbol.for()的相同點:

  • 它們定義的值型別都為"symbol";

Symbol()和Symbol.for()的不同點:

  • Symbol()定義的值不放入全域性 symbol 登錄檔中,每次都是新建,即使描述相同值也不相等;

  • 用 Symbol.for() 方法建立的 symbol 會被放入一個全域性 symbol 登錄檔中。Symbol.for() 並不是每次都會建立一個新的 symbol,它會首先檢查給定的 key 是否已經在登錄檔中了。假如是,則會直接返回上次儲存的那個。否則,它會再新建一個。

2.Symbol.keyFor()
作用: 方法用來獲取 symbol 登錄檔中與某個 symbol 關聯的鍵。如果全域性登錄檔中查詢到該symbol,則返回該symbol的key值,形式為string。如果symbol未在登錄檔中,返回undefined

// 建立一個 symbol 並放入 Symbol 登錄檔,key 為 "foo"
var globalSym = Symbol.for("foo"); 
Symbol.keyFor(globalSym); // "foo"

// 建立一個 symbol,但不放入 symbol 登錄檔中
var localSym = Symbol(); 
Symbol.keyFor(localSym); // undefined,所以是找不到 key 的

Symbol的屬性

Symbol.prototype.description
description 是一個只讀屬性,它會返回 Symbol 物件的可選描述的字串。

 1 // Symbol()定義的資料
 2 let a = Symbol("acc");
 3 a.description  // "acc"
 4 Symbol.keyFor(a);  // undefined
 5 
 6 // Symbol.for()定義的資料
 7 let a1 = Symbol.for("acc");
 8 a1.description  // "acc"
 9 Symbol.keyFor(a1);  // acc
10 
11 // 未指定描述的資料
12 let a2 = Symbol();
13 a2.description  // undefined
14 
15 
16 Symbol('desc').toString();   // "Symbol(desc)"
17 Symbol('desc').description;  // "desc"
18 Symbol('').description;      // ""
19 Symbol().description;        // undefined

description屬性和Symbol.keyFor()方法的區別是:

  • description能返回所有Symbol型別資料的描述,而Symbol.keyFor()只能返回Symbol.for()在全域性註冊過的描述

以上就是Symbol的基本用法,你以為這就完了嗎?上面的只是開胃菜而已,Symbol真正難的地方在於,它玩的都是底層

內建的Symbol值:

除了定義自己使用的 Symbol 值以外,ES6 還提供了 11 個內建的 Symbol 值,指向語言內部使用的方法。可以稱這些方法為魔術方法,因為它們會在特定的場景下自動執行。

內建Symbol的值呼叫時機
Symbol.hasInstance 當其他物件使用 instanceof 運算子,判斷是否為該物件的例項時,會呼叫這個方法
Symbol.isConcatSpreadable 物件的 Symbol.isConcatSpreadable 屬性等於的是一個布林值,表示該物件用於 Array.prototype.concat()時,是否可以展開。
Symbol.species 建立衍生物件時,會使用該屬性
Symbol.match 當執行 str.match(myObject) 時,如果該屬性存在,會呼叫它,返回該方法的返回值。
Symbol.replace 當該物件被 str.replace(myObject)方法呼叫時,會返回該方法的返回值。
Symbol.search 當該物件被 str. search (myObject)方法呼叫時,會返回該方法的返回值。
Symbol.split 當該物件被 str. split (myObject)方法呼叫時,會返回該方法的返回值。
Symbol.iterator 物件進行 for…of 迴圈時,會呼叫 Symbol.iterator 方法,返回該物件的預設遍歷器
Symbol.toPrimitive 該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值。
Symbol. toStringTag 在該物件上面呼叫 toString 方法時,返回該方法的返回值
Symbol. unscopables 該物件指定了使用 with 關鍵字時,哪些屬性會被 with環境排除。

特別的:Symbol內建值的使用,都是作為某個物件型別的屬性去使用

內建值的應用:

Symbol.hasInstance:

物件的Symbol.hasInstance屬性,指向一個內部方法,當其他物件使用instanceof運算子,判斷是否為該物件的例項時,會呼叫這個方法

 1         class Person {}
 2         let p1 = new Person;
 3         console.log(p1 instanceof Person); //true
 4         // instanceof  和  [Symbol.hasInstance]  是等價的
 5         console.log(Person[Symbol.hasInstance](p1)); //true
 6         console.log(Object[Symbol.hasInstance]({})); //true
 7 
 8 
 9         //Symbol內建值得使用,都是作為某個物件型別的屬性去使用
10         class Person {
11             static[Symbol.hasInstance](params) {
12                 console.log(params)
13                 console.log("有人用我來檢測型別了")
14                 //可以自己控制 instanceof 檢測的結果
15                 return true
16                 //return false
17             }
18         }
19         let o = {}
20         console.log(o instanceof Person)    //重寫為true

Symbol.isConcatSpreadable

值為布林值,表示該物件用於Array.prototype.concat()時,是否可以展開

1         let arr = [1, 2, 24, 23]
2         let arr2 = [42, 25, 24, 235]
3         //控制arr2是否可以展開
4         arr2[Symbol.isConcatSpreadable] = false
5         console.log(arr.concat(arr2)) //(5)[1, 2, 24, 23, Array(4)]

Symbol.iterator

ES6 創造了一種新的遍歷命令 for...of 迴圈,Iterator 介面主要供 for...of 消費,這個內建值是比較常見的,也是一個物件可以被for of被迭代的原因,我們可以檢視物件是否存在這個Symbol.iterator值,判斷是否可被for of迭代,擁有此屬性的物件被譽為可被迭代的物件可以使用for…of迴圈

列印{}物件可以發現,不存在Symbol.iterator,所以{}物件是無法被for of迭代的,而[]陣列是可以,因為陣列上面有Symbol.iterator屬性

小提示:原生具備 iterator 介面的資料(可用 for of 遍歷)

  • Array
  • Arguments
  • Set
  • Map
  • String
  • TypedArray
  • NodeList

那麼知道原理了,我們就可以手動的給{}物件加上Symbol.iterator屬性,使其可以被for of遍歷出來

 1     // 讓物件變為可迭代的值,手動加上陣列的可迭代方法
 2         let obj = {
 3             0: 'zhangsan',
 4             1: 21,
 5             length: 2,
 6             [Symbol.iterator]: Array.prototype[Symbol.iterator]
 7         };
 8         for(let item of obj) {
 9             console.log(item);
10         }

這裡有個缺陷:因為我們使用的是陣列原型上的Symbol.iterator,所以物件必須是個偽陣列才能遍歷,自定義一個物件上的Symbol.iterator屬性,使其更加通用

 1         let arr = [1, 52, 5, 14, 23, 2]
 2         let iterator = arr[Symbol.iterator]()
 3         console.log(iterator.next())  //{value: 1, done: false}        
 4         console.log(iterator.next())  //{value: 52, done: false}       
 5         console.log(iterator.next())  //{value: 5, done: false}    
 6         console.log(iterator.next())  //{value: 14, done: false}       
 7         console.log(iterator.next())  //{value: 23, done: false}       
 8         console.log(iterator.next())  //{value: 2, done: false}        
 9         console.log(iterator.next())  //{value: undefined, done: true}
10         
11         
12 
13         //自定義[Symbol.iterator],使得物件可以通過for of 遍歷
14         let obj = {
15             name: "Ges",
16             age: 21,
17             hobbies: ["ESgsg", "Sfgse", "Egs", "SEGSg"],
18             [Symbol.iterator]() { 
19                 console.log(this)
20                 let index = 0
21                 let Keyarr = Object.keys(this)
22                 let len = Keyarr.length
23                 return {
24                     next: () => {
25                         if(index >= len) return {
26                             value: undefined,
27                             done: true
28                         }
29                         let result = {
30                             value: this[Keyarr[index]],
31                             done: false
32                         }
33                         index++
34                         return result
35                     }
36                 }
37             }
38         }
39 
40         for(let item of obj) {
41             console.log(item)
42         }

使用generator和yield簡化

 1         //簡潔版
 2         let obj = {
 3             name: "Ges",
 4             age: 21,
 5             hobbies: ["ESgsg", "Sfgse", "Egs", "SEGSg"],
 6             *[Symbol.iterator]() {
 7                 for(let arg of Object.values(this)) {
 8                     yield arg;
 9                 }
10             }
11 
12         }
13 
14         for(let item of obj) {
15             console.log(item)
16         }

這樣實現後,{}物件就變得可以使用for of遍歷了,當然如果掛載到Obejct.prototype上所以物件都可以使用for of 遍歷了
注: 需要自定義遍歷資料的時候,要想到迭代器。

Symbol.toPrimitive

該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值

 1 /*
 2  * 物件資料型別進行轉換:
 3  * 1. 呼叫obj[Symbol.toPrimitive](hint),前提是存在
 4  * 2. 否則,如果 hint 是 "string" —— 嘗試 obj.toString() 和 obj.valueOf()
 5  * 3. 否則,如果 hint 是 "number" 或 "default" —— 嘗試 obj.valueOf() 和 obj.toString()
 6  */
 7 let a = {
 8     value: 0,
 9     [Symbol.toPrimitive](hint) {
10         switch (hint) {
11             case 'number': //此時需要轉換成數值 例如:數學運算`
12                 return ++this.value;
13             case 'string': // 此時需要轉換成字串 例如:字串拼接
14                 return String(this.value);
15             case 'default': //此時可以轉換成數值或字串 例如:==比較
16                 return ++this.value;
17         }
18     }
19 };
20 if (a == 1 && a == 2 && a == 3) {
21     console.log('OK');
22 }

當然自定義一個valueOf/toString都是可以的,資料型別進行轉換時,呼叫優先順序最高的還是Symbol.toPrimitive

 1         //存在[Symbol.toPrimitive] 屬性,優先呼叫
 2         let a = {
 3             value: 0,
 4             [Symbol.toPrimitive](hint) {
 5                 console.log(hint)
 6                 switch(hint) {
 7                     case 'number': //此時需要轉換成數值 例如:數學運算時觸發
 8                         return ++this.value;
 9                     case 'string': // 此時需要轉換成字串 例如:字串拼接時觸發
10                         return String(this.value);
11                     case 'default': //此時可以轉換成數值或字串 例如:==比較時觸發
12                         return ++this.value;
13                 }
14             },
15             valueOf: function() {
16                 console.log("valueOf")
17                 return a.i++;
18             },
19             toString: function() {
20                 console.log("toString")
21                 return a.i++;
22             }
23         };

Symbol.toStringTag

在該物件上面呼叫Object.prototype.toString方法時,如果這個屬性存在,它的返回值會出現在toString方法返回的字串之中,表示物件的型別

1 class Person {
2     get [Symbol.toStringTag]() {
3         return 'Person';
4     }
5 }
6 let p1 = new Person;
7 console.log(Object.prototype.toString.call(p1)); //"[object Person]"

上述只說了五個常見的Symobl內建值的使用,剩下的就不一一敘述了,呼叫時機的清單表已經列出來了,感興趣的可以自己去嘗試研究下

總結:

總的來說Symbol用的最多的地方,還是它作為一個唯一值去使用,但我們需要知道,它不僅僅只是代表一個唯一值,Symbol難的地方在於它的內建值,它玩的都是底層