摘要:然而這樣使用有一個很大的限制在誕生之前,對象的鍵只能是字符串。如果只是簡單的使用字符串作為,這將會有很大的風險,因為它們的完全有可能相同。只允許字符串作為,并沒有試圖讓輸出。
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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103266.html
摘要:和命令命令在聲明所在的塊級作用域內有效。解構賦值從數組和對象中提取值,對變量進行賦值,這被稱為解構。數值和布爾值的解構解構賦值時,如果等號右邊是數值和布爾值,則會先轉為對象。默認值解構賦值允許指定默認值。 let和const命令 let命令 在聲明所在的塊級作用域內有效。 只要塊級作用域內存在let命令,它所聲明的變量就綁定(binding)這個區域,不再受外部的影響。 在同一個作用...
摘要:指針指針指針重要的時期說三遍由于對象類型為指針引用在變量復制方面,基本類型和引用類型也有所不同。在瀏覽器中,全局執行環境被認為是對象。 javascript---Symbol類型, 引用類型, 作用域 javascript的引用類型, 即對象類型是我們最常用的的類型, 其中有許多讓我們需要注意的地方, 最新的 , ES6 的推出, 使得對象類型的屬性名不僅僅可以是字符串類型,還可是Si...
摘要:返回布爾值標簽模板可以緊跟一個函數名后邊,該函數將被調用來處理這個模板字符串。其它情況下返回值為在內部,整數和浮點數使用同樣的存儲方法,所以和被視為同一個值。 簡介 ES6目標,讓JavaScript變成一個企業級的開發語言,不僅僅限制與前端頁面的腳本語言。 標準(Standard): 用于定義與其他事物區別的一套規則 實現(Implementation): 某個標準的具體實施/真實實...
摘要:一簡介與的關系是的規格,是的一種實現另外的方言還有和轉碼器命令行環境安裝直接運行代碼命令將轉換成命令瀏覽器環境加入,代碼用環境安裝,,根目錄建立文件加載為的一個鉤子設置完文件后,在應用入口加入若有使用,等全局對象及上方法安裝 一、ECMAScript6 簡介 (1) 與JavaScript的關系 ES是JS的規格,JS是ES的一種實現(另外的ECMAScript方言還有Jscript和...
摘要:筆記和和是塊作用域的,是聲明常量用的。一個對象如果要有可被循環調用的接口,就必須在的屬性上部署遍歷器生成方法原型鏈上的對象具有該方法也可。這種方式會訪問注冊表,其中存儲了已經存在的一系列。這種方式與通過定義的獨立不同,注冊表中的是共享的。 ECMAScript6 筆記 let 和 const let和const是塊作用域的 ,const是聲明常量用的。 {let a = 10;} a ...
閱讀 1164·2021-11-22 15:24
閱讀 4440·2021-09-23 11:51
閱讀 2302·2021-09-08 09:36
閱讀 3514·2019-08-30 15:43
閱讀 1295·2019-08-30 13:01
閱讀 1116·2019-08-30 12:48
閱讀 530·2019-08-29 12:52
閱讀 3366·2019-08-29 12:41