[譯] JavaScript 中爲何會有 Symbol 類型?

做爲最新的基本類型,Symbol 爲 JavaScript 語言帶來了不少好處,特別是當其用在對象屬性上時。可是,相比較於 String 類型,Symbol 有哪些 String 沒有的功能呢?javascript

在深刻探討 Symbol 以前,讓咱們先看看一些許多開發人員可能都不知道的 JavaScript 特性。前端

背景

JavaScript 中有兩種數據類型:基本數據類型和對象(對象也包括函數),基本數據類型包括簡單數據類型,好比 number(從整數到浮點數,從 Infinity 到 NaN 都屬於 Number 類型)、boolean、string、undefinednull(注意儘管 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 也是基本數據類型,由於它不能被改變。下面是 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)
複製代碼

Symbol 做爲對象屬性

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 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索