翻譯自 mediumjavascript
Symbols 是 JavaScript 最新推出的一種基本類型,它被當作對象屬性時特別有用,可是有什麼是它能作而 String 不能作的呢?java
在咱們開始探索 Symbols 功能以前,咱們先來看一下被不少開發者忽略 JavaScript 的特性。git
JavaScript 有兩種值類型,一種是 基本類型 (primitives),一種是 對象類型 (objects,包含 function 類型),基本類型包括數字 number (包含 integer,float,Infinity,NaN),布爾值 boolean,字符串 string,undefined,null,儘管 typeof null === 'object'
,null 仍然是一個基本類型。github
基本類型的值是不可變的,固然了,存放基本類型值得變量是能夠被從新分配的,例如當你寫 let x = 1; x++
,變量 x 就被從新分配值了,可是你並無改變原來的1.web
一些語言,例如 c 語言有引用傳遞和值傳遞的概念,JavaScript 也有相似的概念,儘管它傳遞的數據類型須要推斷。當你給一個 function 傳值的時候,從新分配值並不會修改該方法調用時的參數值。然而,假如你修改一個非基本類型的值,修改值也會影響原來的值。json
考慮下下面的例子:api
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
複製代碼
const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); // true
複製代碼
const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };
console.log(obj1 === obj2); // false
// Though, their .name properties ARE primitives:
console.log(obj1.name === obj2.name); // true
複製代碼
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 數據結構被建立的目的就是爲了應對存儲鍵值對中,鍵不是字符串的狀況。瀏覽器
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
複製代碼
const s1 = Symbol('debug');
const str = 'debug';
const s2 = Symbol('xxyy');
console.log(s1 === str); // false
console.log(s1 === s2); // false
console.log(s1); // Symbol(debug)
複製代碼
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() 並無返回 symbols,這是爲了向後兼容性的考慮。老代碼不兼容 symbols,所以古老的 Object.keys() 不該該返回 symbols。bash
看第一眼,咱們可能會以爲 symbols 這個特性很適合做爲對象的私有屬性,許多其餘語言都要相似的類的隱藏屬性,這一直被認爲是 JavaScript 的一大短板。不幸的是,仍是有可能經過 symbols 來取到對象的值,甚至都不用試着獲取對象屬性就能夠獲得對象 key,例如,經過 Reflect.ownKeys() 方法就能夠獲取全部的 key,包括 字符串和 symbols,以下所示:數據結構
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
複製代碼
注意:如今已經有一個旨在解決 JavaScript 私有屬性的提案,叫作 Private Fields,儘管這並不會使全部的對象受益,它仍然對對象的實例有用,Private Fields 在 Chrome 74版本可用。
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}
複製代碼
const library1property = Symbol('lib1');
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = Symbol('lib2');
function lib2tag(obj) {
obj[library2property] = 369;
}
複製代碼
const library1property = uuid(); // random approach
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach
function lib2tag(obj) {
obj[library2property] = 369;
}
複製代碼
你是對的,這種方法確實相似於 symbols 的這一做用,除非兩個庫使用相同的屬性名,那就會有被覆寫的風險。
機敏的讀者已經發現這兩種方案的效果並不徹底相同。咱們獨有的屬性名仍然有一個缺點:它們的 key 很容易被找到,尤爲是當代碼進行遞歸或者系列化對象,考慮以下的例子:
const library2property = 'LIB2-NAMESPACE-id'; // namespaced
function lib2tag(obj) {
obj[library2property] = 369;
}
const user = {
name: 'Thomas Hunter II',
age: 32
};
lib2tag(user);
JSON.stringify(user);
// '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}'
複製代碼
假如咱們使用 symbols 做爲屬性名,json 的輸出將不會包含 symbols,這是爲何呢?由於 JavaScript 支持 symbols,並不意味着 json 規範也會跟着修改。json 只容許字符串做爲 key,JavaScript 並無試圖讓 json 輸出 symbols。
咱們能夠簡單的經過 Object.defineProperty() 來調整對象字符串輸出的 json。
const library2property = uuid(); // namespaced approach
function lib2tag(obj) {
Object.defineProperty(obj, library2property, {
enumerable: false,
value: 369
});
}
const user = {
name: 'Thomas Hunter II',
age: 32
};
lib2tag(user);
// '{"name":"Thomas Hunter II", "age":32,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}'
console.log(JSON.stringify(user));
console.log(user[library2property]); // 369
複製代碼
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)); // {}
複製代碼
在這一點上,咱們至關於重建了 symbols,咱們的隱藏字符串和 symbols 都被序列化器隱藏了,屬性也均可以經過 Reflect.ownKeys() 來獲取,所以他們並不算私有屬性。假設咱們使用命名空間、隨機字符串等字符串做爲對象的屬性名,咱們就能夠避免多個庫重名的風險。
可是仍然有一點細微的不一樣,字符串是不可變的,而 symbols 能夠保證永遠惟一,所以仍然有可能會有人生成重名的字符串。從數學意義上 symbols 提供了一個字符串沒有的優勢。
在 Node.js 裏面,當檢測一個對象(例如使用 console.log()),假如對象上的一個方法叫作 inspect,當記錄對象時,該方法會被調用並輸出。你能夠想象,這種行爲並非每一個人都會這樣作,被用戶建立的 inspect 方法常常會致使命名衝突,如今 require('util').inspect.custom 提供的 symbol 能夠被用在函數上。inspect 方法在 Node.js v10 被放棄,在 v11 版直接被忽略。如今沒人能夠突然就改變 inspect 方法的行爲了。
這裏有一個在對象上模擬私有屬性的有趣的嘗試。使用了另外一個 JavaScript 的新特性:proxy。proxy 會包住一個對象,而後咱們就能夠跟這個對象進行各類各樣的交互。
proxy 提供了不少種攔截對象行爲的方式。這裏咱們感興趣的是讀取對象屬性的行爲。我並不會完整的解釋 proxy 是如何工做的,因此若是你想要了解的更多,能夠查看咱們的另外一篇文章:JavaScript Object Property Descriptors, Proxies, and Preventing Extension
咱們可使用代理來展現對象上可用的屬性。這裏咱們先建立一個 proxy 來隱藏兩個屬性,一個是字符串 _favColor,另外一個是 symbol 叫 favBook。
let proxy;
{
const favBook = Symbol('fav book');
const obj = {
name: 'Thomas Hunter II',
age: 32,
_favColor: 'blue',
[favBook]: 'Metro 2033',
[Symbol('visible')]: 'foo'
};
const handler = {
ownKeys: (target) => {
const reportedKeys = [];
const actualKeys = Reflect.ownKeys(target);
for (const key of actualKeys) {
if (key === favBook || key === '_favColor') {
continue;
}
reportedKeys.push(key);
}
return reportedKeys;
}
};
proxy = new Proxy(obj, handler);
}
console.log(Object.keys(proxy)); // [ 'name', 'age' ]
console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ]
console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ]
console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)]
console.log(proxy._favColor); // 'blue'
複製代碼
發現 _favColor 屬性很簡單,只須要閱讀源碼便可,另外,動態的 key 能夠經過暴力破解方式得到(例如前面的 uuid 例子)。可是對 symbol 屬性,若是你沒有直接的引用,是沒法訪問到 Metro 2033
這個值的。
Node.js 備註:有一個特性能夠破解私有屬性,這個特性不是 JavaScript 的語言特性,也不存在與其餘場景,例如 web 瀏覽器。當使用 proxy 時,你能夠獲取到對象隱藏的屬性。這裏有一個破解上面私有屬性的例子:
const [originalObject] = process
.binding('util')
.getProxyDetails(proxy);
const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)
複製代碼