ES6引入Symbol的原因:防止屬性名的衝突(ES5的物件屬性名都是字串,容易造成屬性名的衝突)。
ES6引入一種新的原始資料型別:Symbol,表示獨一無二的值。它是javascript語言的第七種資料型別,前六種是:Undefined
、Null
、Boolean
、String
、Number
、Object
。
Symbol值是通過Symbol
函式生成的。
let s = Symbol();
typeof s //"symbol"
注:Symbol函式前不能使用new命令,否則會報錯。報錯的原因就是:生成的Symbol是一個原始型別的值,而不是物件,所以不能新增屬性。它是一種類似於字串的資料型別。
Symbol
函式可以接受一個字串作為引數,表示對Symbol例項的描述,主要是為了在控制檯顯示,或者轉為字串時,比較容易區分。
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
注:Symbol函式的引數只是表示對當前Symbol值的描述,因此相同引數的Symbol函式的返回值是不相等的。
// 沒有引數的情況
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有引數的情況
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
Symbol值不能與其他型別的值進行運算。
var sym = Symbol('My symbol');
"your symbol is " + sym;
// Error: Cannot convert a Symbol value to a string
但是Symbol值可以轉為字串。
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
2、作為屬性名的Symbol
由於每一個Symbol值都是不相等的,這意味著Symbol值可以作為識別符號,用於物件的屬性名,就能保證不會出現同名的屬性。這對於一個物件由多個模組構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋。
var mySymbol = Symbol();
// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
var a = {
[mySymbol]: 'Hello!'
};
// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
Object.defineProperty
用來將物件的屬性名指定為一個Symbol值。
注: Symbol值作為物件屬性名時,不能用點運算子。
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
上面程式碼中,因為點運算子後面總是字串,所以不會讀取mySymbol作為標識名所指代的那個值,導致a的屬性名實際上是一個字串,而不是一個Symbol值。
同理,在物件的內部,使用Symbol值定義屬性時,Symbol值必須放在方括號之中。
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
//等同於
let obj = {
[s](arg) { ... }
};
obj[s](123);
上面程式碼中,如果s不放在方括號中,該屬性的鍵名就是字串s,而不是s所代表的那個Symbol值。
Symbol型別還可以定義一組常量,使這組常量的值不想等。
var log = {};
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
};
console.log(log.levels.DEBUG, 'debug message');//Symbol(debug) 'debug message'
console.log(log.levels.INFO, 'info message');//Symbol(info) 'info message'
需要注意的是:Symbol值作為屬性名時,該屬性還是公開屬性,不是私有屬性。
3、屬性名的遍歷
Symbol作為屬性名,該屬性不會出現在for...in
、for...of
迴圈中,也不會被Object.keys()
、Object.getOwnPropertyNames()
返回。但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols
方法,可以獲取指定物件的所有Symbol屬性名。
注:這裡涉及到兩個物件方法,它們的作用是:
Object.getOwnPropertyNames
:獲取所有的屬性名,不包括prototy中的屬性,返回一個陣列。
var o = Object.create({
"say": function () {
alert(this.name);
},
"name":"Byron"
});
Object.defineProperties(o, {
'age': {
value: 24,
writable: true,
enumerable: true,
configurable: true
},
'sex': {
value: 'male',
writable: false,
enumerable: false,
configurable: false
}
});
console.log(o); //結果如下圖
console.log(Object.getOwnPropertyNames(o));//["age", "sex"]
上面程式碼console.log(o)的結果
從上面的例子中可以看到prototype中的name屬性沒有獲取到。
Object.keys()
和getOwnPropertyNames
方法類似,但是獲取所有的可列舉的屬性,返回一個陣列。
console.log(Object.keys(o)); //["age"]
Object.getOwnPropertySymbols
方法返回一個陣列,成員是當前物件的所有用作屬性名的Symbol值。
var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
for (var i in obj) {
console.log(i); // 無輸出
//但是直接在最新版本的chrome裡卻是輸出了{Symbol(foo): "foobar"}
}
Object.getOwnPropertyNames(obj)
// []
Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]
有一個新的APIReflect.ownKeys
方法可以返回所有型別的鍵名,包括常規的和Symbol。
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
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)
都無法獲取它。這就造成了一種非私有的內部方法的效果。
4、Symbol.for(),Symbol.keyFor()Symbol.for
方法:接受一個字串作為引數,並且建立的Symbol值是全域性的,如果我們已經有一個Symbol值了,並且想重新使用,那麼就可以利用這個方法來獲取。
由於Symbol.for
建立的Symbol值是全域性的,所以可以在不同的iframe和service worker中取到同一個值,例如:
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
Symbol.for
的工作原理就是:搜尋有沒有以該引數作為名稱的Symbol值,如果有,就返回這個Symbol值,否則就新建並返回一個以該字串為名稱的Symbol值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
下面我們來對比一下:Symbol.for()
與 Symbol()
:
相同點:都會生成新的Symbol。
區別:
Item | 全域性搜尋 | 每次呼叫返回一個新的Symbol值 |
---|---|---|
Symbol.for() | 是 | 會先檢測給定的key是否存在,如果不存在則建立新的Symbol值 |
Symbol() | 否 | 是 |
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
Symbol.keyFor
方法:返回一個已登記的Symbol值的key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
5、內建的Symbol值
除了自定義的Symbol值外,ES6還提供了11個內建的Symbol值,它們都是物件的屬性,指向語言內部使用的方法。
(1)、Symbol.hasInstance
該物件使用instanceof
運算子時,會呼叫這個方法,判斷該物件是否為某個建構函式的例項。
比如:foo instanceof Foo
在語言內部,實際呼叫的是Foo[Symbol.hasInstance](foo)
。
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
} //對這一段程式碼不是很理解
}
var o = new MyClass();
o instanceof Array // false
o instanceof MyClass // true
instanceof
運算子可以用來判斷某個建構函式的prototype
屬性是否存在另外一個要檢測物件的原型鏈上。用法:
object instanceof constructor
引數:object
:要檢測的物件constructor
:某個建構函式function C(){} //定義建構函式 function D(){} var o = new C(); o instanceof C; //true,因為:Object.getPrototypeOf(o) === C.prototype o instanceof D; // false,因為D.prototype不在o的原型鏈上 o instanceof Object; // true,因為Object.prototype.isPrototypeOf(o)返回true C.prototype instanceof Object // true,同上 C.prototype = {}; var o2 = new C(); o2 instanceof C; // true o instanceof C; // false,C.prototype指向了一個空物件,這個空物件不在o的原型鏈上. D.prototype = new C(); var o3 = new D(); o3 instanceof D; // true o3 instanceof C; // true
(2)、Symbol.isConcatSpreadable
物件的Symbol.isConcatSpreadable
屬性等於一個布林值,表示該物件使用Array.prototype.concat()
時,屬性值為true即為可以展開,false則不允許展開。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e'] 我執行的結果表示這個屬性沒有起作用
類似陣列的物件,它的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 {
[Symbol.isConcatSpreadable]() {
return true;
}
}
class A2 extends Array {
[Symbol.isConcatSpreadable]() {
return 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]]
// 我的執行結果:[ 1, 2, A1 { '0': 3, '1': 4 }, A2 { '0': 5, '1': 6 } ]
(3)、Symbol.species
該物件作為建構函式創造例項時,會呼叫這個方法。即如果this.constructor[Symbol.species]
存在,就會使用這個屬性作為建構函式,來創造新的例項物件。
(4)、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
上面程式碼,'e'.match(new MyMatcher())
執行後,Symbol.match
方法被呼叫,所以會返回字母e的位置時1。
(5)、Symbol.replace
當該物件被String.prototype.replace
方法呼叫時,會返回該方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同於
searchValue[Symbol.replace](this, replaceValue)
class MyReplace{
constructor(val1,val2){
this.val1 = val1;
this.val2 = val2;
}
[Symbol.replace](string){
return string.replace(new RegExp(this.val1),this.val2);
}
}
'hello world'.replace(new MyReplace('world','jumei')); // hello jumei
(6)、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
(7)、Symbol.split
當該物件被String.prototype.split
方法呼叫時,會返回該方法的返回值。
String.prototype.split(separator, limit)
// 等同於
separator[Symbol.split](this, limit)
class MySplit{
constructor(val1,val2){
this.val1 = val1;
this.val2 = val2;
}
[Symbol.split](string){
return string.split(this.val1,this.val2);
}
}
'How are you ?'.split(new MySplit(" ",3));
//[ 'How', 'are', 'you' ]
(8)、Symbol.iterator
物件的Symbol.iterator
屬性,指向該物件的預設遍歷器方法,即該物件進行for...of
迴圈時,會呼叫這個方法,返回該物件的預設遍歷器,詳細介紹參見《Iterator和for...of迴圈》一章。
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
(9)、Symbol.toPrimitive
該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值。
JavaScript中的原始型別包括數字,字串和布林值。
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 我的執行結果:NaN
3 + obj // '3default' 我的執行結果:3[object Object]
obj === 'default' // true 我的執行結果:false
String(obj) // 'str' 我的執行結果:[object Object]
看來這個屬性現在或許還不支援吧,才造成我的執行結果是這樣的。
(10)、Symbol.toStringTag
在該物件上面呼叫Object.prototype.toString
方法時,如果這個屬性存在,它的返回值會出現在toString
方法返回的字串之中,表示物件的型別。
也就是說,這個屬性可以用來定製[object Object]
或[object Array]
中object後面的那個字串。
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
//等同於
class Collection {
get [Symbol.toStringTag]() {
return 'Foo';
}
}
var x = new Collection();
Object.prototype.toString.call(x) // "[object Foo]"
ES6新增內建物件的Symbol.toStringTag
屬性值如下。
JSON[Symbol.toStringTag]:'JSON'
Math[Symbol.toStringTag]:'Math'
Module物件M[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'
(11)、Symbol.unscopables
該物件指定了使用with
關鍵字時,哪些屬性會被with
環境排除。
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'keys']
上面程式碼說明,陣列有6個屬性,會被with
命令排除。
// 沒有unscopables時
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有unscopables時
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}