- 原文地址:JavaScript Symbols: But Why?
- 原文做者:Thomas Hunter II
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:xionglong58
- 校對者:EdmondWang, Xuyuey
做爲最新的基本類型,Symbol 爲 JavaScript 語言帶來了不少好處,特別是當其用在對象屬性上時。可是,相比較於 String 類型,Symbol 有哪些 String 沒有的功能呢?javascript
在深刻探討 Symbol 以前,讓咱們先看看一些許多開發人員可能都不知道的 JavaScript 特性。前端
JavaScript 中有兩種數據類型:基本數據類型和對象(對象也包括函數),基本數據類型包括簡單數據類型,好比 number(從整數到浮點數,從 Infinity 到 NaN 都屬於 Number 類型)、boolean、string、undefined
、null
(注意儘管 typeof null === 'object'
,null
仍然是一個基本數據類型)。java
基本數據類型的值是不能夠改變的,即不能更改變量的原始值。固然能夠從新對變量進行賦值。例如,代碼 let x = 1; x++;
,雖然你經過從新賦值改變了變量 x
的值,可是變量的原始值 1
仍沒有被改變。node
一些語言,好比 C 語言,有按引用傳遞和按值傳遞的概念。JavaScript 也有相似的概念,它是根據傳遞數據的類型推斷出來的。若是將值傳入一個函數,則在函數中從新對它賦值不會修改它在調用位置的值。可是,若是你修改的是基本數據的值,那麼修改後的值會在調用它的地方被修改。android
考慮下面的例子:ios
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
除外)老是與另外一個具備相同值的基本數據類型徹底相等。以下:git
const first = "abc" + "def";
const second = "ab" + "cd" + "ef";
console.log(first === second); // true
複製代碼
然而,構造兩個值相同的非基本數據類型則獲得不相等的結果。咱們能夠看到發生了什麼:github
const obj1 = { name: "Intrinsic" };
const obj2 = { name: "Intrinsic" };
console.log(obj1 === obj2); // false
// 可是,當二者的 .name 屬性爲基本數據類型時 console.log(obj1.name === obj2.name); // true
複製代碼
對象在 JavaScript 中扮演着重要的角色,幾乎全部地方能夠見到它們的身影。對象一般是鍵/值對的集合,然而這種形式的最大限制是:對象的鍵只能是字符串,直到 Symbol 出現這一限制才獲得解決。若是咱們使用非字符串的值做爲對象的鍵,該值會被強制轉換成字符串。在下面的程序中能夠看到這種強制轉換:web
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
數據結構的部分緣由是爲了在鍵不是字符串的狀況下容許鍵/值方式存儲。編程
如今既然咱們已經知道了基本數據類型是什麼,也就終於能夠定義 Symbol。Symbol 是不能被從新建立的基本數據類型。在這種狀況下,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('xxyy');
console.log(s1 === str); // false
console.log(s1 === s2); // false
console.log(s1); // Symbol(debug)
複製代碼
symbols 還有另外一個重要的用法,它們能夠被看成對象中的鍵!下面是一個在對象中使用 symbol 做爲鍵的例子:
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']
複製代碼
注意,symbols 鍵不會被在 Object.keys()
返回。這也是爲了知足向後兼容性。舊版本的 JavaScript 沒有 symbol 數據類型,所以不該該從舊的 Object.keys()
方法中被返回。
乍一看,這就像是能夠用 symbols 在對象上建立私有屬性!許多其餘編程語言能夠在其類中有私有屬性,而 JavaScript 卻遺漏了這種功能,長期以來被視爲其語法的一種缺點。
不幸的是,與該對象交互的代碼仍然能夠訪問對象那些鍵爲 symbols 的屬性。甚至是在調用代碼本身沒法訪問 symbol 的狀況下也有可能發生。 例如,Reflect.ownKeys()
方法可以獲得一個對象的全部鍵的列表,包括字符串和 symbols:
function tryToAddPrivate(obj) {
obj[Symbol('Pseudo Private')] = 42;
}
const obj = { prop: 'hello' };
tryToAddPrivate(obj);
console.log(Reflect.ownKeys(obj));
console.log(obj[Reflect.ownKeys(obj)[1]]); // 42
複製代碼
注意:目前有些工做旨在處理在 JavaScript 中向類添加私有屬性的問題。這個特性就是 Private Fields 雖然這不會對全部對象都有好處,但會對類實例的對象有好處。Private Fields 從 Chrome 74 開始可用。
Symbol 類型可能會對獲取 JavaScript 中對象的私有屬性不利。它們之因此有用的另外一個理由是,當不一樣的庫但願向對象添加屬性時 symbols 能夠避免命名衝突的風險。
若是有兩個不一樣的庫但願將某種元數據附加到一個對象上,二者可能都想在對象上設置某種標識符。僅僅使用兩個字符串類型的 id
做爲鍵來標識,多個庫使用相同鍵的風險就會很高。
function lib1tag(obj) {
obj.id = 42;
}
function lib2tag(obj) {
obj.id = 369;
}
複製代碼
應用 symbols,每一個庫均可以經過實例化 Symbol 類生成所需的 symbols。而後無論何時,均可以在相應的對象上檢查、賦值 symbols 對應的鍵值。
const library1property = Symbol('lib1');
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = Symbol('lib2');
function lib2tag(obj) {
obj[library2property] = 369;
}
複製代碼
基於這個緣由 symbols 確實有益於 JavaScript。
然而,你可能會懷疑,爲何每一個庫不能在實例化時簡單地生成一個隨機字符串,或者使用一個特殊的命名空間?
const library1property = uuid(); // 隨機方法
function lib1tag(obj) {
obj[library1property] = 42;
}
const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach
function lib2tag(obj) {
obj[library2property] = 369;
}
複製代碼
你有多是正確的,上面的兩種方法與使用 symbols 的方法很類似。除非兩個庫使用了相同的屬性名,不然不會有衝突的風險。
在這一點上,機靈的讀者會指出,這兩種方法並不徹底相同。具備惟一名稱的屬性名仍然有一個缺點:它們的鍵很是容易找到,特別是當運行代碼來迭代鍵或以其餘方式序列化對象時。請考慮如下示例:
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}'
複製代碼
若是咱們爲對象的屬性名使用了一個 symbol,那麼 JSON 的輸出將不包含 symbol 對應的值。爲何會這樣?由於僅僅是 JavaScript 支持了 symbols,並不意味着 JSON 規範也改變了!JSON 只容許字符串做爲鍵,而 JavaScript 不會嘗試在最終的 JSON 負載中呈現 symbol 屬性。
咱們能夠經過使用 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
複製代碼
經過將字符串鍵的可枚舉描述符設置爲 false 來「隱藏」字符串鍵的行爲很是相似於 symbol 鍵。它們經過 Object.keys()
遍歷也看不到,但能夠經過 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)); // {}
複製代碼
在這一點上,咱們幾乎從新建立了 symbols。隱藏的字符串屬性和 symbols 都對序列化程序隱身。這兩種屬性均可以使用 Reflect.ownKeys()
方法提取,所以實際上並非私有的。假設咱們對字符串屬性使用某種命名空間/隨機值,那麼咱們就消除了多個庫意外發生命名衝突的風險。
可是,仍然有一個微小的差別。因爲字符串是不可變的,Symbol 始終保證是惟一的,所以仍有可能生成相同的字符串併產生衝突。從數學角度來講,意味着 symbols 確實提供了咱們沒法從字符串中得到的好處。
在 Node.js 中,檢查對象時(例如使用 console.log()
),若是遇到對象上名爲 inspect
的方法,則調用該函數,並將輸出表示成對象的日誌。能夠想象,這種行爲並非每一個人都指望的,一般命名爲 inspect
的方法常常與用戶建立的對象發生衝突。如今有 symbol 可用來實現這個功能,而且能夠在 require('util').inspection.custom 中使用。inspect
方法在 Node.js v10 中被廢棄,在 v11 中徹底被忽略。如今沒有人會由於意外改變 inspect 的行爲!
這裏有一個有趣的方法,咱們可使用它來模擬對象上的私有屬性。這種方法將利用另外一個 JavaScript 的特性:proxy。proxy 本質上是封裝了一個對象,並容許咱們與該對象進行不一樣的交互。
proxy 提供了許多方法來攔截對對象執行的操做。咱們所感興趣的是在嘗試讀取對象的鍵時,proxy 會有哪些動做。我不會去詳細解釋 proxy 是如何工做的,若是你想了解更多信息,請查看咱們的另外一篇文章:JavaScript Object Property Descriptors, Proxies, and Preventing Extension.
咱們可使用 proxy 來謊報對象上可用的屬性。在本例中,咱們將建立一個 proxy,它用於隱藏咱們的兩個已知隱藏屬性,一個是字符串 _favColor
,另外一個是分配給 favBook
的 symbol:
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
字符串很簡單:只需讀取庫的源代碼便可。此外,動態鍵能夠(例如以前講的 uuid
示例)能夠經過暴力找到。可是,若是不是直接引用 symbol,任何人都沒法從 proxy
對象中訪問到值 metro 2033
。
Node.js 聲明:Node.js 中的一個特性破壞了 proxy 的隱私性。此功能不存在於 JavaScript 語言自己,也不適用於其餘狀況,例如 web 瀏覽器。這一特性容許在給定 proxy 時得到對底層對象的訪問權。如下是一個使用此功能破壞上述私有屬性的示例:
const [originalObject] = process
.binding('util')
.getProxyDetails(proxy);
const allKeys = Reflect.ownKeys(originalObject);
console.log(allKeys[3]); // Symbol(fav book)
複製代碼
咱們如今須要修改全局 Reflect
對象,或是修改 util
進程綁定,以防止它們在特定的 node.js 實例中被使用。但那倒是一個新世界的大門,若是你想了解其中的奧祕,看看咱們的其餘博客:Protecting your JavaScript APIs。
這篇文章是我和 Thomas Hunter II 一塊兒寫的。我在一家名爲 Intricsic 的公司工做(順便說一下,咱們正在招聘!),專門編寫用於保護 Node.js 應用程序的軟件。咱們目前有一個產品應用 Least Privilege 模型來保護應用程序。咱們的產品主動保護 Node.js 應用程序不受攻擊者的攻擊,並且很是容易實現。若是你正在尋找保護 Node.js 應用程序的方法,請在 hello@inherin.com 上聯繫咱們。
橫幅照片的做者 Chunlea Ju
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。