JavaScript 爲何要有 Symbol 類型?

摘要: 爲何比怎麼用更有意義。javascript

Symbols 是 ES6 引入了一個新的數據類型 ,它爲 JS 帶來了一些好處,尤爲是對象屬性時。 可是,它們能爲咱們作些字符串不能作的事情呢?前端

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

背景

js 中的數據類型整體來講分爲兩種,他們分別是:值類型 和 引用類型git

  • 值類型(基本類型):數值型(Number),字符類型(String),布爾值型(Boolean),null 和 underfined
  • 引用類型(類):函數,對象,數組等

**值類型理解:**變量之間的互相賦值,是指開闢一塊新的內存空間,將變量值賦給新變量保存到新開闢的內存裏面;以後兩個變量的值變更互不影響,例如:github

var a = 10; //開闢一塊內存空間保存變量a的值「10」;
var b = a; //給變量 b 開闢一塊新的內存空間,將 a 的值 「10」 賦值一份保存到新的內存裏;
//a 和 b 的值之後不管如何變化,都不會影響到對方的值;
複製代碼

一些語言,好比 C,有引用傳遞和值傳遞的概念。JavaScript 也有相似的概念,它是根據傳遞的數據類型推斷的。若是將值傳遞給函數,則從新分配該值不會修改調用位置中的值。可是,若是你修改的是引用類型,那麼修改後的值也將在調用它的地方被修改。編程

**引用類型理解:**變量之間的互相賦值,只是指針的交換,而並不是將對象(普通對象,函數對象,數組對象)複製一份給新的變量,對象依然仍是隻有一個,只是多了一個指引~~;例如:小程序

var a = { x: 1, y: 2 }; //須要開闢內存空間保存對象,變量 a 的值是一個地址,這個地址指向保存對象的空間;
var b = a; // 將a 的指引地址賦值給 b,而並不是複製一給對象且新開一塊內存空間來保存;
// 這個時候經過 a 來修改對象的屬性,則經過 b 來查看屬性時對象屬性已經發生改變;
複製代碼

值類型(神祕的 NaN 值除外)將始終與具備相同值的另外一個值類型的徹底相等,以下:segmentfault

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
// 可是,它們的 .name 屬性是基本類型:
console.log(obj1.name === obj2.name); // true
複製代碼

對象在 JavaScript 語言中扮演重要角色,它們的使用無處不在。對象一般用做鍵/值對的集合,然而,以這種方式使用它們有一個很大的限制: 在 symbol 出現以前,對象鍵只能是字符串,若是試圖使用非字符串值做爲對象的鍵,那麼該值將被強制轉換爲字符串,以下:api

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' }
複製代碼

Symbol 是什麼

Symbol() 函數會返回 symbol 類型的值,該類型具備靜態屬性和靜態方法。它的靜態屬性會暴露幾個內建的成員對象;它的靜態方法會暴露全局的 symbol 註冊,且相似於內建對象類,但做爲構造函數來講它並不完整,由於它不支持語法:"new 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 做爲對象屬性

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']
複製代碼

乍一看,這看起來就像可使用 symbol 在對象上建立私有屬性,許多其餘編程語言在其類中有本身的私有屬性,私有屬性遺漏一直被視爲 JavaScript 的缺點。

不幸的是,與該對象交互的代碼仍然能夠訪問其鍵爲 symbol 的屬性。 在調用代碼尚不能訪問 symbol 自己的狀況下,這甚至是可能的。 例如,Reflect.ownKeys() 方法可以獲取對象上全部鍵的列表,包括字符串和 symbol :

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 中向類添加私有屬性的問題。這個特性的名稱被稱爲私有字段,雖然這不會使全部對象受益,但會使類實例的對象受益。私有字段從 Chrome 74 開始可用。

代碼部署後可能存在的 BUG 無法實時知道,過後爲了解決這些 BUG,花了大量的時間進行 log 調試,這邊順便給你們推薦一個好用的 BUG 監控工具 Fundebug

防止屬性名稱衝突

符號可能不會直接受益於 JavaScript 爲對象提供私有屬性。然而,他們是有益的另外一個緣由。當不一樣的庫但願向對象添加屬性而不存在名稱衝突的風險時,它們很是有用。

Symbol 爲 JavaScrit 對象提供私有屬性還有點困難,但 Symbol 還有別外一個好處,就是避免當不一樣的庫向對象添加屬性存在命名衝突的風險。

考慮這樣一種狀況:兩個不一樣的庫想要向一個對象添加基本數據,可能它們都想在對象上設置某種標識符。經過簡單地使用 id 做爲鍵,這樣存在一個巨大的風險,就是多個庫將使用相同的鍵。

function lib1tag(obj) {
    obj.id = 42;
}
function lib2tag(obj) {
    obj.id = 369;
}
複製代碼

經過使用 Symbol,每一個庫能夠在實例化時生成所需的 Symbol。而後用生成 Symbol 的值作爲對象的屬性:

const library1property = Symbol("lib1");
function lib1tag(obj) {
    obj[library1property] = 42;
}
const library2property = Symbol("lib2");
function lib2tag(obj) {
    obj[library2property] = 369;
}
複製代碼

出於這個緣由,Symbol 彷佛確實有利於 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;
}
複製代碼

這種方法是沒錯的,這種方法實際上與 Symbol 的方法很是類似,除非兩個庫選擇使用相同的屬性名,不然不會有衝突的風險。

在這一點上,聰明的讀者會指出,這兩種方法並不徹底相同。咱們使用惟一名稱的屬性名仍然有一個缺點:它們的鍵很是容易找到,特別是當運行代碼來迭代鍵或序列化對象時。考慮下面的例子:

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 輸出將不包含它的值。這是爲何呢? 雖然 JavaScript 得到了對 Symbol 的支持,但這並不意味着 JSON 規範已經改變! JSON 只容許字符串做爲鍵,JavaScript 不會嘗試在最終 JSON 有效負載中表示 Symbol 屬性。

const library2property = "f468c902-26ed-4b2e-81d6-5775ae7eec5d"; // namespaced approach
function lib2tag(obj) {
    Object.defineProperty(obj, library2property, {
        enumerable: false,
        value: 369
    });
}
const user = {
    name: "Thomas Hunter II",
    age: 32
};
lib2tag(user);
console.log(user); // {name: "Thomas Hunter II", age: 32, f468c902-26ed-4b2e-81d6-5775ae7eec5d: 369}
console.log(JSON.stringify(user)); // {"name":"Thomas Hunter II","age":32}
console.log(user[library2property]); // 369
複製代碼

經過將 enumerable 屬性設置爲 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)); // {}
複製代碼

在這點上,咱們幾乎從新建立了 Symbol。隱藏的字符串屬性和 Symbol 都對序列化器隱藏。這兩個屬性均可以使用Reflect.ownKeys()方法讀取,所以它們實際上不是私有的。假設咱們爲屬性名的字符串版本使用某種名稱空間/隨機值,那麼咱們就消除了多個庫意外發生名稱衝突的風險。

可是,仍然有一個微小的區別。因爲字符串是不可變的,並且 Symbol 老是保證唯一的,因此仍然有可能生成字符串組合會產生衝突。從數學上講,這意味着 Symbol 確實提供了咱們沒法從字符串中獲得的好處。

在 Node.js 中,檢查對象時(例如使用 console.log() ),若是遇到名爲 inspect 的對象上的方法,將調用該函數,並將打印內容。能夠想象,這種行爲並非每一個人都指望的,一般命名爲 inspect 的方法常常與用戶建立的對象發生衝突。

如今 Symbol 可用來實現這個功能,而且能夠在 equire('util').inspect.custom 中使用。inspect 方法在 Node.js v10 中被廢棄,在 v1 1 中徹底被忽略, 如今沒有人會偶然改變檢查的行爲。

模擬私有屬性

這裏有一個有趣的方法,咱們能夠用來模擬對象上的私有屬性。這種方法將利用另外一個 JavaScript 特性: proxy(代理)。代理本質上封裝了一個對象,並容許咱們對與該對象的各類操做進行干預。

代理提供了許多方法來攔截在對象上執行的操做。咱們可使用代理來講明咱們的對象上可用的屬性,在這種狀況下,咱們將製做一個隱藏咱們兩個已知隱藏屬性的代理,一個是字符串 _favColor,另外一個是分配給 favBook 的 S ymbol :

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 中有一個功能會破壞代理的隱私。 JavaScript 語 言自己不存在此功能,而且不適用於其餘狀況,例 如 Web 瀏覽器。 它容許在給定代理時得到對底層對象的訪問權。 如下是使用此功能打破上述私有屬性示例的示例:

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。

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用

相關文章
相關標籤/搜索