Symbol
是 ES6 引入的一種新的原始資料型別,表示獨一無二的值,用於解決 ES5 中物件屬性命名衝突的問題。比如,你使用了一個他人提供的物件,但又想為這個物件新增新的方法,新方法的名字就有可能與現有方法產生衝突。
Symbol
型別的值可透過 Symbol()
、Symbol.for()
函式生成。
Symbol
型別的值主要有 5 個方面的應用,分別為:消除魔術字串、全域性共享 Symbol 、解決屬性名稱衝突、實現類的私有屬性和私有方法和服務端渲染時,防止 XSS 攻擊
消除魔術字串
魔術字串指的是,在程式碼之中多次出現、與程式碼形成強耦合的某一個具體的字串或者數值。風格良好的程式碼,應該儘量消除魔術字串,改由含義清晰的變數代替。
function calculator(type, a, b) { if (type === 'add') { return a + b } if (type === 'minus') { return a - b } return a * b } calculator('add', 1, 2) calculator('minus', 1, 2)
上面程式碼中,add
、minus
都是魔術字串。他們在程式碼中多次出現,與程式碼形成“強耦合”,比如當日後表示 add
的型別要由其他字串來表示,則需要手動一個個地修改。不利於將來的維護。
常用的消除魔術字串的方法,就是把他寫成一個變數。
const calcType = { add: 'add', minus: 'minus' } function calculator(type, a, b) { if (type === calcType.add) { return a + b } if (type === calcType.minus) { return a - b } return a * b } calculator(calcType.add, 1, 2) calculator(calcType.minus, 1, 2)
上面程式碼中,我們把具有相同功能的並在多個地方使用的字串聚合到了一個物件的屬性中,這樣就消除了強耦合,方便後續修改。
其實,calcType
的各個屬性中的值等於什麼並不重要,只要確保這些屬性值在該物件中是唯一的即可。因此,這裏就很適合改用 Symbol 值。
// 改用 Symbol 值 const calcType = { add: Symbol(), minus: Symbol() } function calculator(type, a, b) { if (type === calcType.add) { return a + b } if (type === calcType.minus) { return a - b } return a * b } calculator(calcType.add, 1, 2) calculator(calcType.minus, 1, 2)
全域性共享 Symbol
所謂全域性共享的 Symbol ,指的是使用 Symbol.for()
方法建立的 Symbol 值。
Symbol.for(key)
方法會根據給定的鍵 key
,來從執行時的 symbol 登錄檔中找到對應的 symbol,如果找到了,則返回它,否則,新建一個與該鍵關聯的 symbol,並放入全域性 symbol 登錄檔中。
讀者可能會有疑問,JavaScript 分為全域性作用域和區域性作用域,如果要建立一個全域性共享的 Symbol 值,只需在全域性作用域下建立 Symbol 就可以了,為啥要用 Symbol.for()
方法?
其實 Symbol.for()
方法高階的地方在於,使用該方法建立的 Symbol 值可以做到跨檔案、跨域共享。
如果說,在不使用 Symbol.for()
的情況下,對於跨檔案的共享,可以使用模組匯入和匯出的方式進行共享。但是在跨域的情況下,比如在不同的 iframe 中,是無法使用模組匯入和匯出的,如果不使用 postMessage
的話,要共享 Symbol 值,只能使用 Symbol.for()
方法了。
可以說,Symbol.for()
方法是 Symbol()
方法的補充,使使用者在保證唯一性的情況下,能夠方便的重用 Symbol 型別的值。
解決屬性名稱衝突
當你開發一個庫或框架時,爲了避免屬性名衝突,可以使用 Symbol 值作為物件的屬性名。這樣可以保證屬性名的唯一性,例如:
const id = Symbol('id'); const obj = { [id]: 'id value', }; console.log(obj[id]);
或者我們要給第三方庫中提供的物件新增屬性時,爲了避免與物件中原有屬性衝突,也可使用 Symbol 值做物件的屬性名。
實現類的私有屬性和私有方法
在早期的 ES6 中,沒有提供原生的實現私有方法和私有屬性的語法,只能透過變通方法模式實現。
一種做法是在命名上加以區分,比如約定,對於私有方法和私有屬性,統一用下劃線開頭命名:
class MyClass { // 公有方法 foo (baz) { this._bar(baz); } // 私有方法 _bar(baz) { return this.snaf = baz; } // 私有屬性 _age = 20 }
但是,這種方式實現私有方法和私有屬性的方式是不保險的,在類的外部,還是可以呼叫到這個方法,沒有私有性可言。
另一種方法是將私有方法移出類,因為類內部的方法都是對外可見的,然後使用 call
方法,將外部的私有方法與類連線起來,即使用 call
將外部私有方法的 this 繫結到相關類例項中:
class MyClass { updateAge(age) { changeAge.call(this, age) } } function changeAge(age) { return this.age = age }
上面程式碼中,updateAge
是公開方法,內部呼叫 changeAge.call(this, age)
。這使得 changeAge()
實際上成爲了當前類的私有方法。但是這種方式並不能實現私有屬性。
接下來就要說到本文的主角 Symbol 啦,利用 Symbol 的唯一性,可以實現類的私有方法和私有屬性,由於 Symbol 的唯一性,在類的外部無法直接訪問利用 Symbol 定義的屬性和方法,從而實現了私有屬性和私有方法的效果:
const changeHobby = Symbol('changeHobby') const hobby = Symbol('hobby') class MyClass { [hobby] = 'coding'; updateHobby(hobby) { this[changeHobby](hobby) } [changeHobby](value) { return this[hobby] = value } }
當然,使用 Symbol 實現私有屬性和私有方法也不完美,可以使用 Reflect.ownKeys()
拿到私有屬性、屬性方法的名字,增加了暴露的風險:
const instance = new MyClass() // 私有屬性被暴露! Reflect.ownKeys(instance) // [Symbol(hobby)] // 私有方法被暴露! Reflect.ownKeys(MyClass.prototype) // ['constructor', 'updateHobby', Symbol(changeHobby)]
後來,在 ES2022 中推出了類的私有方法和私有屬性的原生語法,標誌着在 JS 中,類的私有方法和私有屬性有了正式寫法。由於本文重點是 Symbol ,因此不在此贅述。
服務端渲染時,防止 XSS 攻擊
當服務端渲染時,由於 Symbol 不能被轉換為 JSON ,所以即使伺服器存在用 JSON 作為文字返回安全漏洞,JSON 裡也不會包含該 Symbol 值。開發者可以透過判斷是否存在 Symbol 值,來判斷該 JSON 是否為使用者故意注入的,從而避免 XSS 攻擊。
例如在 React 中,Babel 會把 JSX 編譯為 React.createElement()
的函式呼叫,最終返回一個 ReatElement
:
// JSX const element = ( <h1 className="greeting"> Hello, world! </h1> ); // 透過 babel 編譯後的程式碼 const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' ); // React.createElement() 方法返回的 ReactElement const element = { $$typeof: Symbol.for('react.element'), type: 'h1', key: null, props: { children: 'Hello, world!', className: 'greeting' } }
合法的 ReactElement
物件會有個 Symbol 型別的值,React 透過 ReactElement
物件上是否有該 Symbol 型別的值來判斷是否為合法的 ReactElement
物件,從而決定是否渲染該 ReactElement
物件,從而避免了 XSS 攻擊。
總結
Symbol
是 ES6 引入的一種新的原始資料型別,表示獨一無二的值。
Symbol
型別的值主要有 5 個方面的應用,分別為:消除魔術字串、全域性共享 Symbol 、解決屬性名稱衝突、實現類的私有屬性和私有方法和服務端渲染時,防止 XSS 攻擊。