Symbol是javascript最新的基本資料類型(primitive),并帶來了一些好處,特别是作為object的辨別符(properties)的時候。可是,這個和用string作為辨別符有什麼特别的呢?
在我們研究Symbol之前,我們先看看許多開發者沒有注意到的javascript的一些特性。
背景
Javascript主要可以分為兩大類。第一類是基本資料類型(primitives),第二類是對象(Object)(包含函數)。基本資料類型包括數字(Number)(所有的整數,小數,
Infinity
和
NaN
),布爾值(Boolean),字元串(String),
undefined
,
null
(注意⚠️: 雖然
typeof null === 'object'
,但
null
仍是基本資料類型)。
基本資料類型是不可修改的。當然一個被基本資料指派的變量(variable)可以重新被指派。例如,
let x = 1; x++;
,這裡是重新對變量
x
指派,并沒有修改基本類型數字
1
。
像一些語言(如C語言)一樣,javascript也有引用傳遞(pass-by-reference)和值傳遞(pass-by-value)的概念。當你給一個函數傳值的時候,在函數内重新指派(reassign)時不會修改原來那個值,然而修改(modify)一個非基本資料類型(non-primitive)時,原來那個數值也會被修改。可以看下面例子:
function primitiveMutator(val) {
val = val + 1;
}
let x = 1;
primitiveMutator(x);
console.log(x); // 1
function objectMutator(val) {
val.prop = val.prop + 1;
}
let obj = { prop: 1 };
objectMutator(obj);
console.log(obj.prop); // 2
複制
當基本資料類型的值相等時,這個變量總是完全相等的(除了
NaN
)。
const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); // true
console.log(NaN === NaN); // false
複制
然而,非基本資料類型(non-primitive)卻不是這樣。
const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };
console.log(obj1 === obj2); // false
// 他們 name 屬性是 基本資料類型
console.log(obj1.name === obj2.name); // true
複制
在javascript中對象(Object)是一個重要的角色,在任何地方都能見到它的身影。Object通常包含了許多鍵值(key/value)。當symbols還沒出現的時候,Object的鍵(key)隻能是字元串(String),這個給Object帶來了一些限制。當使用一個非字元串(non-string)作為Object的key時,這個值會被轉成字元串。例如:
const obj = {};
obj.foo = 'foo';
obj['bar'] = 'bar';
obj[2] = 2;
obj[{}] = 'someobj';
console.log(obj);// { '2': 2, foo: 'foo', bar: 'bar', '[object Object]': 'someobj' }
複制
注意⚠️: Map
可以允許 key 不是字元串。
Symbol 是什麼
現在我們已經知道基本資料類型(primitive)是什麼了。一個Symbol是不能重複建立的基本資料類型。也就是說,一個symbol可以類似一個對象的執行個體,是不會相等的。就是說每一個symbol是唯一的基本資料類型。例如:
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
複制
當初始化一個symbol時可以傳一個字元串對象。這個字元串可以在調試的時候使用,并不會影響symbol的唯一性。
const s1 = Symbol('debug');
const str = 'debug';
const s2 = Symbol('debug');
console.log(s1 === str); // false
console.log(s1 === s2); // false
console.log(s1); // Symbol(debug)
複制
注意⚠️: Symbol.for
是全局建立,它會首先檢查給定的 key 是否已經在系統資料庫中了。假如是,則會直接傳回上次存儲的那個。
Symbols 作為對象屬性的辨別符
Symbols有個很重要的用法,他們可以作為對象的keys。例如:
const obj = {};
const sym = Symbol();
obj[sym] = 'foo';
obj.bar = 'bar';
console.log(obj); // { bar: 'bar' }
console.log(sym in obj); // true
console.log(obj[sym]); // foo
console.log(Object.keys(obj)); // ['bar']
複制
注意到
Object.keys()
不會傳回Symbol。第一眼看上去,symbols可以作為Object的私有變量。許多語言有私有變量,而javascript沒有。可惜的是,還是有辦法通路到symbol作為Object的key的值。例如,
Reflect.ownKeys()
方法可以列出Object的包括string和symbol的所有key。
function tryToAddPrivate(o) {
o[Symbol('Pseudo Private')] = 42;
}
const obj = { prop: 'hello' };
tryToAddPrivate(obj);
console.log(Reflect.ownKeys(obj)); // [ 'prop', Symbol(Pseudo Private) ]
console.log(obj[Reflect.ownKeys(obj)[1]]); // 42
複制
防止辨別符沖突
Symbols可能不能直接給Object添加私有屬性。但是,在給Object加屬性辨別符可以防止名字沖突,這起到非常重要的作用。例如,有兩個庫想要給一個對象加一個自己的唯一辨別符号,這兩個庫都用字元串
id
作為key,這裡會産生很大的危險許多庫用着同樣的key.
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}
複制
使用symbol作為key就不會有這個沖突了。
const library1property = Symbol('lib1');
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = Symbol('lib2');
function lib2tag(obj) {
obj[library2property] = 369;
}
複制
如果我們用symbol作為一個對象的key,
JSON.stringify
不會包含這個值。這是因為javascript支援symbol并不意味着JSON也支援。JSON隻允許用字元串作為key的對象。但定義
enumerable
為false時,當字元串的key也會被隐藏,就像symbol一樣。他們都會在
Object.keys
和
JSON.stringify
中隐藏,在
Reflect.ownKeys()
會列出。例如:
const obj = {};
obj[Symbol()] = 1;
Object.defineProperty(obj, 'foo', {
enumberable: false,
value: 2
});
console.log(Object.keys(obj)); // []
console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ]
console.log(JSON.stringify(obj)); // {}
複制
小結
每個從Symbol()傳回的symbol值都是唯一的。一個symbol值能作為對象屬性的辨別符,這個值在
Object.keys
和
JSON.stringify
是隐藏的,在
Reflect.ownKeys()
會列出。