快速了解ES6的Symbol

JavaScript Symbol.png

Symbol 是 ECMAScript 6 新增的基本數(shù)據(jù)類型。Symbol 提供的實例是唯一、不可變的。它的用途可以確保對象屬性使用唯一標(biāo)識符,不會發(fā)生屬性沖突的危險。

Symbol 提供 Symbol() 函數(shù)來返回 Symbol 類型的值。語法如下所示:

Symbol(desc)
  • desc 可選的字符串類型的參數(shù)。是用來描述 Symbol 的。

通過 Symbol() 函數(shù)初始化,可以通過 typeof 運算符識別 Symbol 類型,當(dāng)然也可以傳入?yún)?shù)對 Symbol 進行描述,但與其 Symbol 定義或標(biāo)識完全無關(guān)。

let s1 = Symbol();
console.log(typeof s1);    // symbol
let s2 = Symbol('symbol instance');
let s3 = Symbol('symbol instance');
console.log(s1 == s2);    // false
console.log(s2 == s3);    // false

注意,Symbol 不能通過 new 關(guān)鍵字像構(gòu)造函數(shù)一樣創(chuàng)建實例,并拋出 TypeError 錯誤。這樣做是為了避免創(chuàng)建 Symbol 包裝對象。

let s = new Symbol();    // TypeError: Symbol is not a constructor

但是,可以使用 Object() 函數(shù)創(chuàng)建一個 Symbol 包裝器對象。

let s = Symbol("symbol instance");
console.log(typeof s);    // symbol
let o = Object(s);
console.log(typeof o);    // object

全局共享的 Symbol

Symbol() 函數(shù)創(chuàng)建的 Symbol 實例不是全局可用的 Symbol 類型。如果需要全局可用的 Symbol 實例,可以使用 Symbol.for(key) 方法和 Symbol.keyFor(key) 方法。

Symbol.for() 方法創(chuàng)建的 Symbol 會被放入一個全局的 Symbol 注冊表中。Symbol.for() 并不是每次都會創(chuàng)建一個新的 Symbol 實例,它會首先檢查給定的 key 是否已經(jīng)在注冊表中了。如果是,則會直接返回上次存儲的Symbol 值。否則,它會在新鍵一個。

let gs1 = Symbol.for('gloal symbol');       //  創(chuàng)建新符號 
let gs2 = Symbol.for('global symbol');  //  重用已有符號
console.log(gs1 === gs2);  // true 

可以使用 Symbol.keyFor() 來查詢?nèi)肿员?,如果查到與 key 對應(yīng)的 Symbol 實例,則返回該 Symbol 實例的 key 值,否則返回 undefined。

//  創(chuàng)建全局符號 
let s = Symbol.for('foo'); 
console.log(Symbol.keyFor(s));   // foo 
//  創(chuàng)建普通符號 
let s2 = Symbol('bar'); 
console.log(Symbol.keyFor(s2));  // undefined 

如果傳給 Symbol.keyFor() 的不是符號,則該方法拋出 TypeError

Symbol.keyFor(123); // TypeError: 123 is not a symbol 

Symbol 作為屬性

Symbol 可以作為屬性來使用。如下所示:

let s1 = Symbol("property one");

let o = {
    [s1]: 'symbol value one'
};
console.log(o);    // { [Symbol(property one)]: 'symbol value one' }

Object.getOwnPropertySymbols() 方法會讓你在查找給定對象的 Symbol 屬性時返回一個 Symbol 類型的數(shù)組。

let s1 = Symbol("property one"),
    s2 = Symbol("property two");

let o = {
    [s1]: 'symbol value one',
    [s2]: 'symbol value two',
    name: 'symbol property sample',
    index: 12
};
console.log(Object.getOwnPropertySymbols(o));    // [ Symbol(property one), Symbol(property two) ]

使用 Object.getOwnPropertyNames() 正好相反。如下所示:

console.log(Object.getOwnPropertyNames(o));    // [ 'name', 'index' ]

Symbol 原型

所有 Symbol 都繼承自 Symbol.prototype,而它也提供了 constructordescription 兩個屬性。constructor 屬性會返回實例原型的函數(shù),默認為 Symbol 函數(shù);而 description 屬性是一個只讀字符串,返回 Symbol 對象的描述。

Symbol.prototype 也提供了 toString()valueOf() 方法用于返回 Symbol 描述符的字符串方法和對象的原始值。

Symbol.prototype[@@toPrimitive] 會將 Symbol 對象轉(zhuǎn)換為原始值。語法如下:

Symbol()[Symbol.toPrimitive](hint)

Symbol[@@toPrimitive]() 方法返回該 Symbol 對象原始值作為 Symbol 數(shù)據(jù)形式。hint 參數(shù)未被使用。

JavaScript 調(diào)用 [@@toPrimitive]() 方法將一個對象轉(zhuǎn)換為原始值表示。你不需要自己調(diào)用 [@@toPrimitive]() 方法;當(dāng)對象需要被轉(zhuǎn)換為原始值時,JavaScript 會自動地調(diào)用該方法。

內(nèi)置的 Symbol 屬性

除了自己創(chuàng)建的 Symbol,JavaScript 還內(nèi)建了一些在 ECMAScript 5 之前沒有暴露給開發(fā)者的 Symbol,它們代表了內(nèi)部語言行為。這些 Symbol 最重要的用途之一是重新定義它們,從而改變原生結(jié)構(gòu)的行為。如重新定義 Symbol.iterator 屬性的值,來改變 for-of 在迭代對象時的行為。

注意 ,在提到 ECMAScript 規(guī)范時,經(jīng)常會引用符號在規(guī)范中的名稱,前綴為 @@。比如,@@iterator 指的就是 Symbol.iterator。

Symbol.iterator

該屬性返回一個對象默認的迭代器,被 for-of 使用。下面自定義一個迭代器。

class List {
    constructor() {
        this.index = 0;
        this.data = arguments;
    }
    *[Symbol.iterator]() {
        while(this.index < this.data.length) {
            yield this.data[this.index++];
        }
    }
}
function iter() {
    let list = new List("小玲", "小霞", "小星", "小民");
    for (const v of list) {
        console.log(v);
    }
}
iter();
// 小玲
// 小霞
// 小星
// 小民

如上所示,通過 for-of 循環(huán)對一個對象進行迭代時,@@iterator 方法在不傳參的情況下被調(diào)用,返回的迭代器用于獲取要迭代的值。

Symbol.asyncIterator

一個返回對象默認的異步迭代器的方法。被 for await of 使用。

該方法返回一個對象默認的異步迭代器,由 for-await-of 語句使用。

class List {
    constructor() {
        this.index = 0;
        this.data = arguments;
    }
    async *[Symbol.asyncIterator]() {

        while(this.index < this.data.length) {
            yield new Promise((resolve) => resolve(this.data[this.index++]));
        }
    }
}
async function asyIter() {
    let list = new List("小玲", "小霞", "小星", "小民");
    for await(const  v of list) {
        console.log(v);
    }
}
asyIter();
// 小玲
// 小霞
// 小星
// 小民

如上所示,for-await-of 循環(huán)會利用 List 類中的函數(shù)執(zhí)行異步迭代操作。

注意,Symbol.asyncIteratorES2018 規(guī)范定義的,只有新版本的瀏覽器支持它。

Symbol.match

該方法指定了用正則表達式去匹配字符串。String.prototype.match() 方法會調(diào)用此函數(shù)。

console.log('It\'s a real horror story'.match(/horror/));
/*
[
  'horror',
  index: 12,
  input: "It's a real horror story",
  groups: undefined
]
*/

給這個方法傳入非正則表達式值會導(dǎo)致該值被轉(zhuǎn)換為RegExp對象。如果想改變這種行為,讓方法直接使用參數(shù),則重新定義 Symbol.match 函數(shù)以取代默認對正則表達式求值的行為,從而讓match()方法使用非正則表達式實例。Symbol.match 函數(shù)接收一個參數(shù),就是調(diào)用 match()方法的字符串實例。返回的值沒有限制:

class StringMatcher {
    static [Symbol.match](target) {
        return target.includes('horror');
    }
    constructor(str) {
        this.str = str;
    }
    [Symbol.match](target) {
        return target.includes(this.str);
    }
}
console.log('It\'s a real horror story'.match(StringMatcher)); // true
console.log('It\'s a real relaxing story'.match(StringMatcher)); // false
console.log('It\'s a real horror story'.match(new StringMatcher('horror'))); // true
console.log('It\'s a real horror story'.match(new StringMatcher('relaxing'))); // false

Symbol.replace

該屬性指定了當(dāng)一個字符串替換所匹配字符串時所調(diào)用的方法。供 String.prototype.replace() 方法調(diào)用此方法,用于替換子字符串。

console.log('It\'s a real horror story'.replace(/horror/, 'relaxing'));
// It's a real relaxing story

而正則表達式的原型上默認由 Symbol.replace 函數(shù)定義。給 String.prototype.replace 方法傳入非正則表達式值會導(dǎo)致該值被轉(zhuǎn)換為 RegExp 對象。

可以重新定義 Symbol.replace 函數(shù),用來取代默認行為。兩種定義 Symbol.replace 函數(shù)的方式如下所示:

class StringReplacer {
    static [Symbol.replace](target, replacement) {
        return target.split('horror').join(replacement);
    }
    constructor(str) {
        this.str = str;
    }
    [Symbol.replace](target, replacement) {
        return target.split(this.str).join(replacement);
    }
}
console.log('It\'s a real horror story'.replace(StringReplacer, 'qux'));    // It's a real qux story

console.log('It\'s a real horror story'.replace(new StringReplacer('horror'), 'relaxing'));    // It's a real relaxing story

Symbol.replace 函數(shù)接收兩個參數(shù),即調(diào)用 replace() 方法的字符串實例和替換字符串。

Symbol.search

該方法會返回正則表達式在字符串中匹配的索引。String.prototype.search() 方法會調(diào)用此方法,用于查找索引。

console.log('It\'s a real horror story'.search(/horror/));    // 12

可以重新定義 Symbol.search 函數(shù),用來取代默認行為,從而讓 search() 方法使用非正則表達式的實例。如下所示:

class StringSearcher {
    static [Symbol.search](target) {
        return target.indexOf('qux');
    }
    constructor(str) {
        this.str = str;
    }
    [Symbol.search](target) {
        return target.indexOf(this.str);
    }
}

console.log('It\'s a real horror story'.search(StringSearcher)); // -1
console.log('It\'s a real qux story'.search(StringSearcher)); // 12
console.log('qux, It\'s a real horror story'.search(StringSearcher)); // 0

console.log('It\'s a real horror story'.search(new StringSearcher('qux'))); // -1
console.log('It\'s a real horror story'.search(new StringSearcher('horror'))); // 12
console.log('It\'s a real horror story'.search(new StringSearcher('s'))); // 3

Symbol.search 函數(shù)接收一個參數(shù),就是調(diào)用 search() 方法的字符串實例。

Symbol.split

該方法會通過一個正則表達式的索引,來分隔字符串。String.prototype.split() 方法會調(diào)用此方法。

console.log('It\'s a real horror story'.split(/ /)); // 正則匹配的是空格
// [ "It's", 'a', 'real', 'horror', 'story' ]

可以重新定義 Symbol.split 函數(shù),用來取代默認行為,從而讓 split() 方法使用非正則表達式實例。

class StringSplitter {
    static [Symbol.split](target) {
        return target.split('qux');
    }
    constructor(str) {
        this.str = str;
    }
    [Symbol.split](target) {
        return target.split(this.str);
    }
}
console.log('It\'s a real qux story'.split(StringSplitter));    // [ "It's a real ", ' story' ]
console.log('It\'s a real horror story'.split(new StringSplitter(' ')));    // [ "It's", 'a', 'real', 'horror', 'story' ]

Symbol.split 函數(shù)接收一個參數(shù),就是調(diào)用 split() 方法的字符串實例。

Symbol.hasInstance

該屬性用于判斷某對象是否為某構(gòu)造器對象的實例??梢杂?Symbol.hasInstance 函數(shù)自定義 instanceof 在某個類上的行為。

instanceof 的場景如下:

class Instance {}

let ist = new Instance();
console.log(ist instanceof Instance);    // true

ES6 中,instanceof 會使用 Symbol.hasInstance 屬性來確定關(guān)系。

console.log(Instance[Symbol.hasInstance](ist));    // true

Symbol.hasInstance 屬性定義在 Function 的原型上,默認所有函數(shù)和類都可以調(diào)用。由于 instanceof 會在原型鏈上尋找這個屬性定義,就跟在原型鏈上尋找其他屬性一樣,因此可以在繼承的類上通過靜態(tài)方法重新定義這個屬性:

class Instance {}
class SubInstance extends Instance {
    static [Symbol.hasInstance]() {
        return false;
    }
}
let sist = new SubInstance();
console.log(Instance[Symbol.hasInstance](sist)); // true
console.log(sist instanceof Instance);           // true
console.log(SubInstance[Symbol.hasInstance](sist)); // false
console.log(sist instanceof SubInstance);           // false

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 用于配置某對象作為 Array.prototype.concat() 方法的參數(shù)時是否展開其數(shù)組元素。

const arr1 = ['a', 'b', 'c'];
const arr2 = [1, 2, 3];
let arr3 = arr1.concat(arr2);
console.log(arr3);    // [ 'a', 'b', 'c', 1, 2, 3 ]

這是在 Symbol.isConcatSpreadable = true 時的情形,如果設(shè)置為 false,就不會展開數(shù)組。

arr2[Symbol.isConcatSpreadable] = false;
let arr4 = arr1.concat(arr2);
console.log(arr4);
/*
[
  'a',
  'b',
  'c',
  [ 1, 2, 3, [Symbol(Symbol.isConcatSpreadable)]: false ]
]

*/

由上可知,對于數(shù)組對象,使用 concat 在默認情況下會將數(shù)組中元素展開進行連接。重置 Symbol.isConcatSpreadable 可以改變默認行為。

Symbol.unscopables

Symbol.unscopables 指定對象值,其對象所有的以及繼承屬性,都會從關(guān)聯(lián)對象的 with 環(huán)境綁定中排除。設(shè)置 Symbol.unscopables 并讓其映射對應(yīng)屬性的鍵值為 true,就可以阻止該屬性出現(xiàn)在 with 環(huán)境綁定中。如下所示:

let o = {
    name: '小玲'
};
with (o) {
    console.log(name); // 小玲
}
o[Symbol.unscopables] = {
    name: true
};
with (o) {
    console.log(name); // ReferenceError: name is not defined
}

注意,不推薦使用 with,因此也不推薦使用 Symbol.unscopables

Symbol.species

Symbol.species 作為創(chuàng)建派生對象的構(gòu)造函數(shù)。用 Symbol.species 定義靜態(tài)的 getter 方法,可以覆蓋新創(chuàng)建實例的原型定義:

class Array1 extends Array {}
class Array2 extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}
let a1 = new Array1();
console.log(a1 instanceof Array); // true
console.log(a1 instanceof Array1);   // true
a1 = a1.concat('species');
console.log(a1 instanceof Array); // true
console.log(a1 instanceof Array1);   // true
let a2 = new Array2();
console.log(a2 instanceof Array); // true
console.log(a2 instanceof Array2);   // true
a2 = a2.concat('species');
console.log(a2 instanceof Array); // true
console.log(a2 instanceof Array2);   // false

Symbol.toPrimitive

Symbol.toPrimitive 是一個內(nèi)置的 Symbol 值,它是作為對象的函數(shù)值屬性存在的,當(dāng)一個對象轉(zhuǎn)換為對應(yīng)的原始值時,會調(diào)用此函數(shù)。

Symbol.toPrimitive 屬性可以將對象轉(zhuǎn)換為相應(yīng)的原始值。很多內(nèi)置操作都會嘗試將值將對象轉(zhuǎn)換為原始值,包括字符串、數(shù)值和未指定的原始類型。如下所示:

class Sample1 {}
let s1 = new Sample1();
console.log(+s1);    // NaN
console.log(`${s1}`);    // [object Object]
console.log(s1 + "");    // [object Object]
class Sample2 {
    constructor() {
        this[Symbol.toPrimitive] = function(hint) {
            switch (hint) {
                case 'number': return 77;
                case 'string': return 'hello world!';
                default: return true;
            }
        }
    }
}
let s2 = new Sample2();
console.log(+s2);    // 77
console.log(`${s2}`);    // hello world!
console.log(s2 + "");    // true

Symbol.toStringTag

Symbol.toStringTag 用于創(chuàng)建對象的默認字符串描述。由 Object.prototype.toString() 調(diào)用。許多內(nèi)置類型已經(jīng)指定了這個值,但自定義類實例可以明確定義:

// 沒有定義 `Symbol.toStringTag` 時
class StringTag {
    constructor() {
    }
}
let a = new StringTag();
console.log(a);   // StringTag {}
console.log(a.toString());    // [object Object]

// 定義 `Symbol.toStringTag` 時
class StringTag {
    constructor() {
        this[Symbol.toStringTag] = 'StringTag';
    }
}
let a = new StringTag();
console.log(a);    // StringTag { [Symbol(Symbol.toStringTag)]: 'StringTag' }
console.log(a.toString());    // [object StringTag]

更多內(nèi)容請關(guān)注公眾號「海人為記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在 ECMAScript 5 及早期版本中,語言包含 5 種原始類型:字符串型、數(shù)字型、布爾型、null 和 un...
    獨木舟的木閱讀 1,753評論 0 1
  • 1.概述 在ES5之前 數(shù)據(jù)類型只有六種 是: undefined ,Object, null,Number,St...
    黑云閱讀 179評論 0 2
  • 前言 該部分為書籍 深入理解ES6 第六章(符號與符號屬性)筆記 創(chuàng)建符號值 符號沒有字面量形式, 這在 JS 的...
    歲月靜好_不負此生閱讀 582評論 0 0
  • 概述 ES5 的對象屬性名都是字符串,這容易造成屬性名的沖突。比如,你使用了一個他人提供的對象,但又想為這個對象添...
    硅谷干貨閱讀 261評論 0 0
  • 概述 ES5的對象屬性名都是字符串,這容易造成屬性名的沖突。比如,你使用了一個他人提供的對象,但又想為這個對象添加...
    oWSQo閱讀 598評論 1 3

友情鏈接更多精彩內(nèi)容