前言
ES5 的對象屬性名都是字符串晰韵,這容易造成屬性名沖突的問題发乔。比如,你使用別人的模塊/對象, 又想為之添加新的屬性,這就容易使得新屬性名與原有屬性名沖突雪猪。這就是 ES6 引入Symbol的原因栏尚,symbol能夠保證每個屬性的名字獨一無二。
一只恨、Symbol介紹
Symbol是 ES6 引入了一種新的原始數據類型译仗,它是一種特殊的桶蝎、不可變的數據類型坐儿,可以作為對象屬性的標識符使用,表示獨一無二的值。凡是屬性名屬于 Symbol 類型雹顺,就都是獨一無二的他炊,可以保證不會與其他屬性名產生沖突盯孙。
二羽利、語法
Symbol 值通過Symbol() 函數生成,Symbol()函數前不能使用new命令序苏,否則會報錯手幢。這是因為生成的 Symbol 是一個原始類型的值,不是對象忱详。也就是說围来,由于 Symbol 值不是對象,所以不能添加屬性踱阿」芮基本上,它是一種類似于字符串的數據類型软舌。
Symbol([description])
description :可選的字符串才漆。表示對symbol的描述,可用于調試但不訪問符號本身的符號的說明佛点。如果不加參數醇滥,在控制臺打印的都是Symbol,不利于區(qū)分超营。
let s = Symbol();
typeof s // "symbol"
// 上面代碼中鸳玩,變量s就是一個獨一無二的值。
// typeof運算符的結果演闭,表明變量s是Symbol數據類型不跟,而不是字符串之類的其他類型。
let s1 = Symbol()
let s2 = Symbol()
s1 // Symbol()
s2 // Symbol()
// s1和s2是兩個 Symbol 值米碰。如果不加參數窝革,它們在控制臺的輸出都是Symbol(),不利于區(qū)分吕座。
let s3 = Symbol('foo');
let s4 = Symbol('bar');
s3 // Symbol(foo)
s4 // Symbol(bar)
// 有了參數以后虐译,就等于為它們加上了描述,輸出的時候就能夠分清吴趴,到底是哪一個值漆诽。
s3.toString() // "Symbol(foo)"
s4.toString() // "Symbol(bar)"
// Symbol函數的參數只是表示對當前Symbol值的描述,因此相同參數的Symbol函數的返回值是不相等的。
// 沒有參數的情況
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有參數的情況
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
三厢拭、Symbol值的類型轉換與運算
【3.1】Symbol不能與其他類型的值進行運算兰英,會報錯
let sym = Symbol('My symbol');
console.log("your symbol is " + sym) // TypeError: can't convert symbol to string
console.log(`your symbol is ${sym}`) // TypeError: can't convert symbol to string
console.log(2 + sym) // TypeError: can't convert symbol to number
【3.2】 Symbol 值可以顯式轉為字符串,也可以轉為布爾值蚪腐,但是不能轉為數值箭昵。
let sym = Symbol('My symbol');
// 轉字符串
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
// 轉布爾值
Boolean(sym) // true
!sym // false
// 轉數字
Number(sym) // TypeError
sym + 2 // TypeError
【3.3】Symbol.prototype.description
上面代碼中税朴,我們可以將Symbol值顯示轉換字符串回季,但是這種用法不是很方便,ES2019提供了一個實例屬性description正林,直接返回 Symbol 的描述泡一。
const sym = Symbol('foo');
sym.description // "foo"
// sym的描述就是字符串foo
四、Symbol應用場景
【4.1】作為屬性名的使用
由于每一個 Symbol 值都是不相等的觅廓,這意味著 Symbol 值可以作為標識符鼻忠,用于對象的屬性名,就能保證不會出現同名的屬性杈绸。這對于一個對象由多個模塊構成的情況非常有用帖蔓,能防止某一個鍵被不小心改寫或覆蓋。注意瞳脓,Symbol 值作為對象屬性名時塑娇,不能用點運算符劫侧。
let mySymbol = Symbol();
// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
let a = {
[mySymbol]: 'Hello!'
};
// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, {
value: 'Hello!'
});
// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
// 不能用點運算符
const sym = Symbol();
const b = {};
b.sym = 'Hello!';
b[sym] // undefined
b['sym'] // "Hello!" 字符串
【4.1】定義常量
Symbol 類型還可以用于定義一組常量,保證這組常量的值都是不相等的烧栋。
const RED = Symbol();
const BLACK = Symbol();
function getComplement(color) {
switch (color) {
case RED:
return '紅色';
case BLACK:
return '黑色';
default:
throw new Error('不存在');
}
}
getComplement(RED) // 紅色
【4.3】Symbol類型的屬性具有一定的隱藏性
let name = Symbol('name');
let obj = {
age: 22,
[name]: 'Joh'
};
console.log(Object.keys(obj)); // ["age"], 打印不出類型為Symbol的[name]屬性
// 使用for-in也打印不出 類型為Symbol的[name]屬性
for (let k in obj) {
console.log(k); // age
}
// 使用 Object.getOwnPropertyNames 同樣打印不出 類型為Symbol的[name]屬性
console.log(Object.getOwnPropertyNames(obj)); ["age"]
// 使用 Object.getOwnPropertySymbols 可以
let key = Object.getOwnPropertySymbols(obj)[0];
console.log(obj[key]); // Joh
【4.4】Symbol.for 的應用
有時,我們希望重新使用同一個 Symbol 值珍特,Symbol.for方法可以做到這一點。它接受一個字符串作為參數魔吐,然后搜索有沒有以該參數作為名稱的 Symbol 值扎筒。如果有,就返回這個 Symbol 值画畅,否則就新建一個以該字符串為名稱的 Symbol 值砸琅,并將其注冊到全局。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
// 上面代碼中轴踱,s1和s2都是Symbol值症脂,但是它們都是由同樣參數的Symbol.for方法生成的,所以實際上是同一個值。
let s3 = Symbol('foo');
let s4 = Symbol('foo');
s3 === s4 // false
// 由于Symbol()寫法沒有登記機制诱篷,所以每次調用都會返回一個不同的值壶唤。
Symbol.for() 與 Symbol() 這兩種寫法,都會生成新的 Symbol棕所。它們的區(qū)別是:
- Symbol.for() 會被登記在全局環(huán)境中供搜索闸盔,Symbol() 不會
- Symbol.for() 不會每次調用就返回一個新的 Symbol 類型的值,而是會先檢查給定的 key 是否已經存在琳省,如果不存在才會新建一個值迎吵。比如,如果你調用Symbol.for("cat") 30 次针贬,每次都會返回同一個 Symbol 值击费,但是調用Symbol("cat") 30 次,會返回 30 個不同的 Symbol 值桦他。
【4.5】Symbol.keyFor的應用
Symbol.keyFor()方法返回一個已登記的 Symbol 類型值的key蔫巩。注意,Symbol.keyFor()在函數內部運行快压,但是生成的 Symbol 值也是登記在全局環(huán)境的圆仔。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
// 上面代碼中,變量s2屬于未登記的 Symbol 值蔫劣,所以返回undefined
function bar() {
return Symbol.for('bar');
}
const x = bar();
const y = Symbol.for('bar');
console.log(x === y); // true
// 上面代碼中坪郭,Symbol.for('bar')是函數內部運行的,但是生成的 Symbol 值是登記在全局環(huán)境的
// 所以拦宣,第二次運行Symbol.for('bar')可以取到這個 Symbol 值截粗。
五、內置的Symbol值
除了定義自己使用的 Symbol 值以外鸵隧,ES6 還提供了 11 個內置的 Symbol 值绸罗,指向語言內部使用的方法。
【5.1】Symbol.hasInstance
對象的Symbol.hasInstance 屬性豆瘫,指向一個內部方法珊蟀。當其他對象使用instanceof運算符,判斷是否為該對象的實例時外驱,會調用這個方法育灸。比如昵宇,foo instanceof Foo 在語言內部瓦哎,實際調用的是FooSymbol.hasInstance ???????
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
// 上面代碼中柔逼,MyClass是一個類愉适,new MyClass()會返回一個實例维咸。
// 該實例的Symbol.hasInstance方法癌蓖,會在進行instanceof運算時自動調用疆股,判斷左側的運算子是否為Array的實例旬痹。
【5.2】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時省有,也有展開的效果蠢沿,和undefined一樣
// 該屬性等于false時,不展開
類似數組的對象正好相反恤磷,默認不展開。它的Symbol.isConcatSpreadable???????屬性設為true精绎,才可以展開代乃。
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設為true才可以展開
【5.3】Symbol.species
對象的Symbol.species屬性仿粹,指向一個構造函數。創(chuàng)建衍生對象時堕仔,會使用該屬性摩骨。
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray // true
c instanceof MyArray // true
// 上面代碼中朗若,子類MyArray繼承了父類Array,a是MyArray的實例灾馒,b和c是a的衍生對象遣总。
// 你可能會認為旭斥,b和c都是調用數組方法生成的,所以應該是數組(Array的實例)董饰,但實際上它們也是MyArray的實例卒暂。
Symbol.species屬性就是為了解決這個問題而提供的÷μ現在近速,我們可以為myArray設置Symbol.species屬性堪旧。
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
// 上面代碼中淳梦,由于定義了Symbol.species屬性昔字,創(chuàng)建衍生對象時就會使用這個屬性返回的函數作郭,作為構造函數夹攒。
// 這個例子也說明,定義Symbol.species屬性要采用get取值器
const a = new MyArray();
const b = a.map(x => x);
b instanceof MyArray // false
b instanceof Array // true
// 上面代碼中压语,a.map(x => x)生成的衍生對象b无蜂,就不是MyArray的實例蒙谓,而直接就是Array的實例累驮。
【5.4】Symbol.match
對象的 Symbol.match 屬性谤专,指向一個函數午绳。當執(zhí)行 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
【5.5】Symbol.replace
對象的Symbol.replace屬性,指向一個方法勇吊,當該對象被String.prototype.replace方法調用時汉规,會返回該方法的返回值。Symbol.replace方法會收到兩個參數膏燕,第一個參數是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锯玛。
【5.6】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
【5.7】Symbol.split
對象的Symbol.split屬性泰涂,指向一個方法负敏,當該對象被String.prototype.split方法調用時其做,會返回該方法的返回值。
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
// 下面方法使用Symbol.split方法驹沿,重新定義了字符串對象的split方法的行為
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let 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'
【5.8】Symbol.iterator
對象的Symbol.iterator屬性渊季,指向該對象的默認遍歷器方法
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
【5.9】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'
【5.10】Symbol.toStringTag
對象的Symbol.toStringTag屬性,指向一個方法窑眯。在該對象上面調用Object.prototype.toString方法時屏积,如果這個屬性存在,它的返回值會出現在toString方法返回的字符串之中磅甩,表示對象的類型炊林。也就是說,這個屬性可以用來定制[
object Object]
或[o
bject Array]
中object后面的那個字符串卷要。
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
【5.11】Symbol.unscopables
對象的Symbol.unscopables屬性渣聚,指向一個對象独榴。該對象指定了使用with關鍵字時,那些屬性會被with環(huán)境排除奕枝。
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; }
}
let foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有 unscopables 時
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
let foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
// 上面代碼通過指定Symbol.unscopables屬性症歇,使得with語法塊不會在當前作用域尋找foo屬性设塔,即foo將指向外層作用域的變量。