Symbol 的做用

Symbols 的出現是爲了什麼呢?

  • 翻譯自 medium
  • Symbols 是 JavaScript 最新推出的一種基本類型,它被當作對象屬性時特別有用,可是有什麼是它能作而 String 不能作的呢?
  • 在咱們開始探索 Symbols 功能以前,咱們先來看一下被不少開發者忽略 JavaScript 的特性。

背景:

  • JavaScript 有兩種值類型,一種是 基本類型 (primitives),一種是 對象類型 (objects,包含 function 類型),基本類型包括數字 number (包含 integer,float,Infinity,NaN),布爾值 boolean,字符串 string,undefined,null,儘管 typeof null === 'object',null 仍然是一個基本類型。
  • 基本類型的值是不可變的,固然了,存放基本類型值得變量是能夠被從新分配的,例如當你寫 let x = 1; x++,變量 x 就被從新分配值了,可是你並無改變原來的1.
  • 一些語言,例如 c 語言有引用傳遞和值傳遞的概念,JavaScript 也有相似的概念,儘管它傳遞的數據類型須要推斷。當你給一個 function 傳值的時候,從新分配值並不會修改該方法調用時的參數值。然而,假如你修改一個非基本類型的值,修改值也會影響原來的值。
  • 考慮下下面的例子:
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
  • 然而,非基本類型的值即便內容同樣,但也不相等,看看這裏:
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
  • 對象扮演了一個 JavaScript 語言的基本角色,它們被處處使用,它們常被用在鍵值對的存儲。然而這樣使用有一個很大的限制:在 symbols 誕生以前,對象的鍵只能是字符串。假如咱們試着使用一個非字符串當作對象的鍵,就會被轉換爲字符串,以下所示:
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 數據結構被建立的目的就是爲了應對存儲鍵值對中,鍵不是字符串的狀況。

symbols 是什麼?

  • 如今咱們知道了什麼是基本類型,終於準備好如何定義什麼是 symbols 了。symbols 是一種沒法被重建的基本類型。這時 symbols 有點相似與對象建立的實例互相不相等的狀況,但同時 symbols 又是一種沒法被改變的基本類型數據。這裏有一個例子:
const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false
  • 當你初始化一個帶有一個接收可選字符串參數的 symbols 時,咱們能夠來 debug 看下,除此以外看看它會否影響自身。
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 做爲對象的屬性

  • symbols 有另外一個很重要的用途,就是用做對象的 key。這兒有一個 symbols 做爲對象 key 使用的例子:
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。
  • 看第一眼,咱們可能會以爲 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版本可用。

阻止對象屬性名衝突

  • symbols 可能對對象的私有屬性沒有直接好處,可是它有另一個用途,它在不知道對象原有屬性名的狀況下,擴展對象屬性頗有用。
  • 考慮一下當兩個不一樣的庫要讀取對象的一些原始屬性時,或許它們都想要相似的標識符。若是隻是簡單的使用字符串 id 做爲 key,這將會有很大的風險,由於它們的 key 徹底有可能相同。
function lib1tag(obj) {
  obj.id = 42;
}
function lib2tag(obj) {
  obj.id = 369;
}
  • 經過使用 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(); // 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
  • 相似於 symbols,對象經過設置 enumerable 標識符來隱藏字符串 key,它們都會被 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() 來獲取,所以他們並不算私有屬性。假設咱們使用命名空間、隨機字符串等字符串做爲對象的屬性名,咱們就能夠避免多個庫重名的風險。
  • 可是仍然有一點細微的不一樣,字符串是不可變的,而 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)
  • 咱們如今要麼修改全局的 Reflect 對象,要麼修改 util 的方法綁定,來組織他們被某個 Node.js 實例訪問。但這是一個無底洞,若是你有興趣深挖,能夠看這篇文章:Protecting your JavaScript APIs
相關文章
相關標籤/搜索