JavaScript為什么要有Symbol 類型

Symbols 是ES6引入了一個新的數(shù)據(jù)類型 ,它為JS帶來了一些好處,尤其是對象屬性時。 但是,它們能為我們做些字符串不能做的事情呢?

在深入探討Symbol之前,讓我們先看看一些 JavaScript 特性,許多開發(fā)人員可能不知道這些特性。

背景

js中的數(shù)據(jù)類型總體來說分為兩種,他們分別是:值類型和 引用類型

值類型(基本類型):數(shù)值型(Number),字符類型(String),布爾值型(Boolean),null 和 underfined

引用類型(類):函數(shù),對象,數(shù)組等

值類型理解:變量之間的互相賦值,是指開辟一塊新的內(nèi)存空間,將變量值賦給新變量保存到新開辟的內(nèi)存里面;之后兩個變量的值變動互不影響,例如:

var?a=10;?//開辟一塊內(nèi)存空間保存變量a的值“10”;

var?b=a;?//給變量?b?開辟一塊新的內(nèi)存空間,將?a?的值?“10”?賦值一份保存到新的內(nèi)存里;

//a?和?b?的值以后無論如何變化,都不會影響到對方的值;

一些語言,比如 C,有引用傳遞和值傳遞的概念。JavaScript 也有類似的概念,它是根據(jù)傳遞的數(shù)據(jù)類型推斷的。如果將值傳遞給函數(shù),則重新分配該值不會修改調(diào)用位置中的值。但是,如果你修改的是引用類型,那么修改后的值也將在調(diào)用它的地方被修改。

順便給大家推薦一個裙,它的前面是 537,中間是631,最后就是 707。想要學(xué)習(xí)前端的小伙伴可以加入我們一起學(xué)習(xí),互相幫助。群里每天晚上都有大神免費(fèi)直播上課,如果不是想學(xué)習(xí)的小伙伴就不要加啦。

引用類型理解:變量之間的互相賦值,只是指針的交換,而并非將對象(普通對象,函數(shù)對象,數(shù)組對象)復(fù)制一份給新的變量,對象依然還是只有一個,只是多了一個指引~~;例如:

var?a={x:1,y:2}?//需要開辟內(nèi)存空間保存對象,變量?a?的值是一個地址,這個地址指向保存對象的空間;

var?b=a;?//?將a?的指引地址賦值給?b,而并非復(fù)制一給對象且新開一塊內(nèi)存空間來保存;

//?這個時候通過?a?來修改對象的屬性,則通過?b?來查看屬性時對象屬性已經(jīng)發(fā)生改變;

值類型(神秘的 NaN 值除外)將始終與具有相同值的另一個值類型的完全相等,如下:

const?first?=?"abc"?+?"def";

const?second?=?"ab"?+?"cd"?+?"ef";

console.log(first?===?second);?//?true

但是完全相同結(jié)構(gòu)的引用類型是不相等的:

const?obj1?=?{?name:?"Intrinsic"?};

const?obj2?=?{?name:?"Intrinsic"?};

console.log(obj1?===?obj2);?//?false

//?但是,它們的?.name?屬性是基本類型:

console.log(obj1.name?===?obj2.name);?//?true

對象在 JavaScript 語言中扮演重要角色,它們的使用無處不在。對象通常用作鍵/值對的集合,然而,以這種方式使用它們有一個很大的限制: 在symbol出現(xiàn)之前,對象鍵只能是字符串,如果試圖使用非字符串值作為對象的鍵,那么該值將被強(qiáng)制轉(zhuǎn)換為字符串,如下:

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()函數(shù)會返回 symbol 類型的值,該類型具有靜態(tài)屬性和靜態(tài)方法。它的靜態(tài)屬性會暴露幾個內(nèi)建的成員對象;它的靜態(tài)方法會暴露全局的 symbol 注冊,且類似于內(nèi)建對象類,但作為構(gòu)造函數(shù)來說它并不完整,因為它不支持語法:"new Symbol()"。所以使用 Symbol 生成的值是不相等:

const?s1?=?Symbol();

const?s2?=?Symbol();

console.log(s1?===?s2);?//?false

實例化symbol時,有一個可選的第一個參數(shù),你可以選擇為其提供字符串。 此值旨在用于調(diào)試代碼,否則它不會真正影響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在對象上創(chuàng)建私有屬性,許多其他編程語言在其類中有自己的私有屬性,私有屬性遺漏一直被視為 JavaScript 的缺點。

不幸的是,與該對象交互的代碼仍然可以訪問其鍵為 symbol 的屬性。 在調(diào)用代碼尚不能訪問 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開始可用。

防止屬性名稱沖突

符號可能不會直接受益于JavaScript為對象提供私有屬性。然而,他們是有益的另一個原因。當(dāng)不同的庫希望向?qū)ο筇砑訉傩远淮嬖诿Q沖突的風(fēng)險時,它們非常有用。

Symbol 為 JavaScrit 對象提供私有屬性還有點困難,但 Symbol 還有別外一個好處,就是避免當(dāng)不同的庫向?qū)ο筇砑訉傩源嬖诿麤_突的風(fēng)險。

考慮這樣一種情況:兩個不同的庫想要向一個對象添加基本數(shù)據(jù),可能它們都想在對象上設(shè)置某種標(biāo)識符。通過簡單地使用id作為鍵,這樣存在一個巨大的風(fēng)險,就是多個庫將使用相同的鍵。

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。

但是,你可能會問,為什么每個庫在實例化時不能簡單地生成隨機(jī)字符串或使用命名空間?

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 的方法非常相似,除非兩個庫選擇使用相同的屬性名,否則不會有沖突的風(fēng)險。

在這一點上,聰明的讀者會指出,這兩種方法并不完全相同。我們使用唯一名稱的屬性名仍然有一個缺點:它們的鍵非常容易找到,特別是當(dāng)運(yùn)行代碼來迭代鍵或序列化對象時??紤]下面的例子:

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}'

如果我們?yōu)閷ο蟮膶傩悦褂昧?Symbol,那么 JSON 輸出將不包含它的值。這是為什么呢? 雖然 JavaScript 獲得了對 Symbol 的支持,但這并不意味著 JSON 規(guī)范已經(jīng)改變! JSON 只允許字符串作為鍵,JavaScript 不會嘗試在最終 JSON 有效負(fù)載中表示 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屬性設(shè)置為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));?//?{}

在這點上,我們幾乎重新創(chuàng)建了 Symbol。隱藏的字符串屬性和 Symbol 都對序列化器隱藏。這兩個屬性都可以使用Reflect.ownKeys()方法讀取,因此它們實際上不是私有的。假設(shè)我們?yōu)閷傩悦淖址姹臼褂媚撤N名稱空間/隨機(jī)值,那么我們就消除了多個庫意外發(fā)生名稱沖突的風(fēng)險。

但是,仍然有一個微小的區(qū)別。由于字符串是不可變的,而且 Symbol 總是保證惟一的,所以仍然有可能生成字符串組合會產(chǎn)生沖突。從數(shù)學(xué)上講,這意味著 Symbol 確實提供了我們無法從字符串中得到的好處。

在 Node.js 中,檢查對象時(例如使用console.log()),如果遇到名為inspect的對象上的方法,將調(diào)用該函數(shù),并將打印內(nèi)容。可以想象,這種行為并不是每個人都期望的,通常命名為inspect的方法經(jīng)常與用戶創(chuàng)建的對象發(fā)生沖突。

現(xiàn)在 Symbol 可用來實現(xiàn)這個功能,并且可以在equire('util').inspect.custom中使用。inspect方法在Node.js v10 中被廢棄,在 v1 1中完全被忽略, 現(xiàn)在沒有人會偶然改變檢查的行為。

模擬私有屬性

這里有一個有趣的方法,我們可以用來模擬對象上的私有屬性。這種方法將利用另一個 JavaScript 特性: proxy(代理)。代理本質(zhì)上封裝了一個對象,并允許我們對與該對象的各種操作進(jìn)行干預(yù)。

代理提供了許多方法來攔截在對象上執(zhí)行的操作。我們可以使用代理來說明我們的對象上可用的屬性,在這種情況下,我們將制作一個隱藏我們兩個已知隱藏屬性的代理,一個是字符串_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字符串很簡單:只需閱讀庫的源代碼即可。 另外,通過蠻力找到動態(tài)鍵(例如前面的uuid示例)。但是,如果沒有對 Symbol 的直接引用,任何人都不能 從proxy對象訪問'Metro 2033'值。

Node.js警告:Node.js中有一個功能會破壞代理的隱私。 JavaScript語 言本身不存在此功能,并且不適用于其他情況,例 如Web 瀏覽器。 它允許在給定代理時獲得對底層對象的訪問權(quán)。 以下是使用此功能打破上述私有屬性示例的示例:

const?[originalObject]?=?process

??.binding('util')

??.getProxyDetails(proxy);

const?allKeys?=?Reflect.ownKeys(originalObject);

console.log(allKeys[3]);?//?Symbol(fav?book)

現(xiàn)在,我們需要修改全局Reflect對象,或者修改util流程綁定,以防止它們在特定的 Node.js 實例中使用。但這是一個可怕的兔子洞。如果你對掉進(jìn)這樣一個兔子洞感興趣,請查看我們的其他博客文章:?Protecting your JavaScript APIs。

原文:JavaScript 為什么要有 Symbol 類型

作者:前端小智

鏈接:https://segmentfault.com/a/1190000018522663

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容