帶你重學ES6 | Symbol(不單單只是一個新的數據類型)

前言

這篇文章說實話,在寫完的那一刻真的對Symbol這個類型肅然起敬,平時真的不用這個數據類型,也沒有想過會用它,以前仍是停留在只是知道這個單詞的階段,在寫完後才發覺它的強大。前端

Symbol,ES6 中新增的數據類型,爲何要增長這麼一個數據類型?當初一個面試官這麼問的我,當時年少輕狂的我,心裏的 os 是,我哪知道 🤣!其實仍是對 Symbol 這個數據類型不熟悉。git

在 ES6 以前,對象的鍵只能是字符串類型,可是這樣有個問題,就是會形成鍵名命名衝突,後者覆蓋前者,這個時候就須要一個惟一值來充當鍵名,Symbol 橫空出世。es6

一、概念

symbol 是一種基本數據類型,Symbol()函數會返回 symbol 類型的值,該類型具備靜態屬性和靜態方法。可是它不是構造函數,不能用 new Symbol()來建立。github

let symbol = Symbol();
typeof symbol; // "symbol" 複製代碼

Symbol 做爲對象屬性時,當在對象內部時,必需要用方括號括起來,不用方括號括起來表明的是字符串。面試

let s = Symbol();
let obj = {  [s]: "Jack", }; obj[s]; // "Jack" obj.s; // undefined 複製代碼

並且當要取該屬性的值時,不能用點運算符,由於點運算符後面一樣是字符串類型。正則表達式

建立 Symbol 數據類型時,都是 Symbol()這麼建立的,當打印出來時,都爲 Symbol(),這樣很難區別各個 Symbol 類型的變量是什麼意思。因此在 Symbol 函數內能夠接收一個字符串的參數,表示該定義 Symbol 類型變量的描述。express

let s1 = Symbol("a");
console.log(s1); // Symbol(a) s1.toString(); // "Symbol(a)" 複製代碼

若是 Symbol 類型接收的一個對象類型的話,那就會先調用其內部的 toString 方法,將其變爲一個字符串,而後才生成一個 Symbol 值。編程

let arr = [1, 2, 3];
let s1 = Symbol(arr); console.log(s1); // Symbol(1,2,3) let obj = {  toString: () => "abc", }; let s2 = Symbol(obj); console.log(s2); // Symbol(abc) 複製代碼

Symbol 類型的變量是不能和其餘變量參與運算的,並且其只能轉爲 String 類型和 Boolean 類型。數組

let s = Symbol();
console.log("1" + s); // TypeError: Cannot convert a Symbol value to a string s.toString(); // "Symbol()" Boolean(s); // true Number(s); // TypeError: Cannot convert a Symbol value to a number 複製代碼

二、Symbol.prototype.description

當給 Symbol 添加描述時,能夠經過 Symbol.prototype.description 來獲取該描述。markdown

let s = Symbol("Jack");
s.description; // 'Jack' 複製代碼

三、Symbol.for(key)和 Symbol.keyFor(sym)

最開始看到這兩個方法時,我覺得是兩個遍歷的方法 😅。

  1. Symbol.for(key):使用給定的 key 搜索現有的 symbol,若是找到則返回該 symbol。不然將使用給定的 key 在全局 symbol 註冊表中建立一個新的 symbol。
  2. Symbol.keyFor(sym):從全局 symbol 註冊表中,爲給定的 symbol 檢索一個 key。
let s1 = Symbol.for("foo");
let s2 = Symbol.for("foo"); s1 === s2; // true 複製代碼

Symbol.for 會搜索有沒有以該參數做爲名稱的 Symbol 值。若是有,就返回這個 Symbol 值,不然就新建一個以該字符串爲名稱的 Symbol 值,並將其註冊到全局。因此由其建立的兩個相同描述的值會相等。這種建立就和普通的 Symbol()有着大相徑庭的結果了:

let s1 = Symbol("foo");
let s2 = Symbol("foo"); s1 === s2; // false 複製代碼

由於無論怎樣 Symbol()返回的都是一個全新的值,換句話說 Symbol()生成的值沒有註冊在全局中,因此返回的值都是全新的,而 Symbol.for()會在先在全局中查找,有就返回這個值,沒有則建立新的值,但新的值也是掛載在全局中的。

Symbol.keyFor(sym)是在全局中查找是否有該 Symbol 值,有則返回該描述。

let s1 = Symbol.for("Jack");
Symbol.keyFor(s1); // 'Jack' let s2 = Symbol("Rose"); Symbol.keyFor(s2); // undefined 複製代碼

由於 s2 沒有掛載在全局中,因此 Symbol.keyFor()找不到它,故返回 undefined。

四、內置的 Symbol 屬性

除了定義本身使用的 Symbol 值之外,ES6 還提供了 13(有可能從此會更多 😛) 個內置的 Symbol 值,指向語言內部使用的方法。

4.1 Symbol.asyncIterator

Symbol.asyncIterator 符號指定了一個對象的默認異步迭代器。若是一個對象設置了這個屬性,它就是異步可迭代對象,可用於 for await...of 循環。換句話說一個異步可迭代對象內部必須有 Symbol.asyncIterator 屬性。

const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function* () {  yield "hello";  yield "async";  yield "iteration!"; };  (async () => {  for await (const x of myAsyncIterable) {  console.log(x);  // expected output:  // "hello"  // "async"  // "iteration!"  } })(); 複製代碼

當執行 for await...of 時,就會執行該變量中 Symbol.asyncIterator 屬性值。

4.二、Symbol.hasInstance

Symbol.hasInstance 用於判斷某對象是否爲某構造器的實例。所以你能夠用它自定義 instanceof 操做符在某個類上的行爲。換句話說當判斷一個實例是否爲一個類的實例時,其實就是執行該類裏面的 Symbol.hasInstance 屬性。

class Fu {
 [Symbol.hasInstance](num) {  return num === 1;  } } 1 instanceof new Fu(); // true 2 instanceof new Fu(); // false 複製代碼

4.三、Symbol.isConcatSpreadable

內置的 Symbol.isConcatSpreadable 符號用於配置某對象做爲 Array.prototype.concat()方法的參數時是否展開其數組元素。

// 默認狀況下
let arr = [1, 2, 3]; let brr = [4, 5, 6]; arr.concat(brr); // [1, 2, 3, 4, 5, 6] // 設置了Symbol.isConcatSpreadable後 let arr = [1, 2, 3]; let brr = [4, 5, 6]; brr[Symbol.isConcatSpreadable] = false; arr.concat(brr); // [1, 2, 3, [4, 5, 6]] 複製代碼

將數組的 Symbol.isConcatSpreadable 屬性設置爲 false 後,使用 concat 方法時該數據就不會展開。

對於類數組而言,默認數組使用 concat 方法該類數組是不展開的,咱們能夠給類數組的 Symbol.isConcatSpreadable 設置爲 true,這樣就能夠展開了,而且完成了類數組轉換爲數組,這樣類數組轉數組又多了一個方法。

// 默認狀況下
function foo(x, y) {  let arr = [].concat(arguments);  console.log(arr); //[Arguments(2)] } foo(1, 2); // 設置了Symbol.isConcatSpreadable爲true後 function foo(x, y) {  arguments[Symbol.isConcatSpreadable] = true;  let arr = [].concat(arguments);  console.log(arr); //[1, 2] } foo(1, 2); 複製代碼

4.四、Symbol.iterator

Symbol.iterator 爲每個對象定義了默認的迭代器。該迭代器能夠被 for...of 循環使用。

const myIterable = {};
myIterable[Symbol.iterator] = function* () {  yield 1;  yield 2;  yield 3; };  [...myIterable]; // [1, 2, 3] 複製代碼

對象進行 for...of 循環時,會調用 Symbol.iterator 方法,

4.五、Symbol.match

Symbol.match 指定了匹配的是正則表達式而不是字符串。String.prototype.match() 方法會調用此函數。換句話說就是當 str.match()執行時若是該屬性存在,就會返回該方法的返回值。

class foo {
 [Symbol.match](string) {  return string;  } } "Jack".match(new foo()); // 'Jack' 複製代碼

除上述以外,MDN 還提出了該屬性另一個功能:此函數還用於標識對象是否具備正則表達式的行爲。好比, String.prototype.startsWith(),String.prototype.endsWith() 和 String.prototype.includes() 這些方法會檢查其第一個參數是不是正則表達式,是正則表達式就拋出一個 TypeError。如今,若是 match symbol 設置爲 false(或者一個 假值),就表示該對象不打算用做正則表達式對象。

"/bar/".startsWith(/bar/); // TypeError: First argument to String.prototype.startsWith must not be a regular expression
// 當設置爲false以後 var re = /foo/; re[Symbol.match] = false; "/foo/".startsWith(re); // true "/baz/".endsWith(re); // false 複製代碼

4.六、Symbol.matchAll

Symbol.matchAll 返回一個迭代器,該迭代器根據字符串生成正則表達式的匹配項。此函數能夠被 String.prototype.matchAll() 方法調用。

"abc".matchAll(/a/);
// 等價於 /a/[Symbol.matchAll]("abc"); 複製代碼

4.七、Symbol.replace

Symbol.replace 這個屬性指定了當一個字符串替換所匹配字符串時所調用的方法。String.prototype.replace() 方法會調用此方法。

String.prototype.replace(searchValue, replaceValue);
// 等同於 searchValue[Symbol.replace](this, replaceValue); // 例子 class Replace1 {  constructor(value) {  this.value = value;  }  [Symbol.replace](string) {  return `s/${string}/${this.value}/g`;  } }  console.log("foo".replace(new Replace1("bar"))); // "s/foo/bar/g" 複製代碼

4.八、Symbol.search

Symbol.search 指定了一個搜索方法,這個方法接受用戶輸入的正則表達式,返回該正則表達式在字符串中匹配到的下標,這個方法由如下的方法來調用 String.prototype.search()。

String.prototype.search(regexp);
// 等價於 regexp[Symbol.search](this); // 例子 class Search1 {  [Symbol.search](str) {  return `${str} Word`;  } } "Hello".search(new Search1()); // Hello Word 複製代碼

4.九、Symbol.species

Symbol.species 是個函數值屬性,其被構造函數用以建立派生對象,換句話說 species 訪問器屬性容許子類覆蓋對象的默認構造函數。

咱們舉個例子:

// 默認狀況下
class MyArray extends Array {} let arr = new MyArray(1, 2, 3); let brr = arr.map((item) => item); brr instanceof MyArray; // true brr instanceof Array; // true 複製代碼

類 MyArray 繼承於 Array,arr 爲 MyArray 的實例,brr 爲 arr 的衍生物,因此 brr 是 MyArray 的實例,而且因爲原型鏈的緣故,brr 也是 Array 的實例。若是此時,咱們只想讓 brr 爲 Array 的實例,那 Symbol.species 屬性值就派上用場了。

class MyArray extends Array {
 static get [Symbol.species]() {  return Array;  } } let arr = new MyArray(1, 2, 3); let brr = arr.map((item) => item); brr instanceof MyArray; // false brr instanceof Array; // true // 默認狀況下 class MyArray extends Array {  static get [Symbol.species]() {  return this;  } } 複製代碼

值得注意的是,定義 Symbol.species 屬性時,前面必須聲明是靜態的 static 而且要運用 get 取值器。

4.十、Symbol.split

Symbol.split 指向 一個正則表達式的索引處分割字符串的方法。 這個方法經過 String.prototype.split() 調用。

String.prototype.split(separator, limit);
// 等價於 separator[Symbol.split](this, limit); // 例子 class Split1 {  [Symbol.split](str) {  return `${str} Word`;  } } "Hello".split(new Split1()); // Hello Word 複製代碼

4.十一、Symbol.toPrimitive

Symbol.toPrimitive 是一個內置的 Symbol 值,它是做爲對象的函數值屬性存在的,當一個對象轉換爲對應的原始值時,會調用此函數。該函數在調用時,會傳遞一個字符串參數 hint,表示要轉換到的原始值的預期類型。字符串 hint 的類型有三種:'number', 'string', 'default'。

let obj =
 {  [Symbol.toPrimitive](hint) {  switch (hint) {  case "number":  return 123;  case "string":  return "123";  case "default":  return "default";  default:  throw new Error();  }  },  } + obj; // 123 `${obj}`; // '123' obj + ""; // "default" 複製代碼

4.十二、Symbol.toStringTag

Symbol.toStringTag 是一個內置 symbol,它一般做爲對象的屬性鍵使用,對應的屬性值應該爲字符串類型,這個字符串用來表示該對象的自定義類型標籤,一般只有內置的 Object.prototype.toString() 方法會去讀取這個標籤並把它包含在本身的返回值裏。通俗點講就是在 Object.prototype.toString()去判斷自定義對象的數據類型時,返回的都是 object,能夠經過這個屬性來給自定義對象添加類型標籤。 在我以前寫的【重學 JS 之路】js 基礎類型和引用類型寫到最精確判斷數據類型的方法就是 Object.prototype.toString(),至因而爲何,在這就不過多闡述了,能夠看這篇文章。

Object.prototype.toString.call('123'); // [object String]
...more 複製代碼

另一些對象類型則否則,toString() 方法能識別它們是由於引擎爲它們設置好了 toStringTag 標籤:

Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]" Object.prototype.toString.call(Promise.resolve()); // "[object Promise]" ...more 複製代碼

當咱們本身定義一個類時,調用 Object.prototype.toString()時,因爲沒有內部定義 toStringTag 標籤,因此只能返回"[object Object]"

class Foo {}
Object.prototype.toString.call(new Foo()); // "[object Object]" // 設置Symbol.toStringTag class Foo {  get [Symbol.toStringTag]() {  return "Foo";  } } Object.prototype.toString.call(new Foo()); // "[object Foo]" 複製代碼

4.1三、Symbol.unscopabless

Symbol.unscopables 指用於指定對象值,其對象自身和繼承的從關聯對象的 with 環境綁定中排除的屬性名稱。說白了其屬性就是控制,在 with 詞法環境中哪些屬性會被 with 刪除。

Array.prototype[Symbol.unscopabless];
// { // copyWithin: true, // entries: true, // fill: true, // find: true, // findIndex: true, // includes: true, // keys: true // } 複製代碼

這裏簡單的講解一下 with 函數,with 主要是用來對對象取值的,舉個簡單的例子:

let obj = {};
with (obj) {  let newa = a;  let newb = b;  console.log(newa + newb); } // 等價於 let newa = obj.a; let newb = obj.b; console.log(newa + newb); 複製代碼

with 的 優勢: 當 with 傳入的值很是複雜時,即當 object 爲很是複雜的嵌套結構時,with 就使得代碼顯得很是簡潔。 with 的缺點: js 的編譯器會檢測 with 塊中的變量是否屬於 with 傳入的對象, 上述例子爲例,js 會檢測 a 和 b 是否屬於 obj 對象,這樣就會的致使 with 語句的執行速度大大降低,性能比較差。

迴歸正題,咱們舉個例子看一下 Symbol.unscopables 屬性的做用。

let obj = {
 foo() {  return 1;  } } with(obj) {  foo(); // 1 } // 設置了Symbol.unscopables let obj = {  foo() {  return 1;  },  get [Symbol.unscopables]() {  return {  foo: true  }  } } with(obj) {  foo(); // Uncaught ReferenceError: foo is not defined } 複製代碼

設置後報錯的緣由是由於with已經將obj中的foo方法刪除了。

這次也是對Symbol有了個從新的認識,也但願對你有所幫助。 點個贊吧!💥🧡💖

參考

阮一峯《ECMAScript 6 入門——Symbol》

後語

以爲還能夠的,麻煩走的時候能給點個贊,你們一塊兒學習和探討!

還能夠關注個人博客但願能給個人github上點個Start,小夥伴們必定會發現一個問題,個人全部用戶名幾乎都與番茄有關,由於我真的很喜歡吃番茄❤️!!!

想跟車不迷路的小夥還但願能夠關注公衆號 前端老番茄 或者掃一掃下面的二維碼👇👇👇。

我是一個編程界的小學生,您的鼓勵是我不斷前進的動力,😄但願能一塊兒加油前進。

本文使用 mdnice 排版

相關文章
相關標籤/搜索