Symbol
是 ES6 引入的一种新的原始数据类型,表示独一无二的值,用于解决 ES5 中对象属性命名冲突的问题。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法,新方法的名字就有可能与现有方法产生冲突。
Symbol
类型的值可通过 Symbol()
、Symbol.for()
函数生成。
Symbol
类型的值主要有 5 个方面的应用,分别为:消除魔术字符串 、全局共享 Symbol 、解决属性名称冲突 、实现类的私有属性和私有方法 和服务端渲染时,防止 XSS 攻击
消除魔术字符串
魔术字符串指的是,在代码之中多次出现 、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
js
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
的类型要由其他字符串来表示,则需要手动一个个地修改。不利于将来的维护。
常用的消除魔术字符串的方法,就是把他写成一个变量。
js
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 值。
js
// 改用 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 值作为对象的属性名。这样可以保证属性名的唯一性,例如:
js
const id = Symbol('id');
const obj = {
[id]: 'id value',
};
console.log(obj[id]);
或者我们要给第三方库中提供的对象添加属性时,为了避免与对象中原有属性冲突,也可使用 Symbol 值做对象的属性名。
实现类的私有属性和私有方法
在早期的 ES6 中,没有提供原生的实现私有方法和私有属性的语法,只能通过变通方法模式实现。
一种做法是在命名上加以区分,比如约定,对于私有方法和私有属性,统一用下划线开头命名:
js
class MyClass {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// 私有属性
_age = 20
}
但是,这种方式实现私有方法和私有属性的方式是不保险的,在类的外部,还是可以调用到这个方法,没有私有性可言。
另一种方法是将私有方法移出类,因为类内部的方法都是对外可见的,然后使用 call
方法,将外部的私有方法与类连接起来,即使用 call
将外部私有方法的 this 绑定到相关类实例中:
js
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 定义的属性和方法,从而实现了私有属性和私有方法的效果:
js
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()
拿到私有属性、属性方法的名字,增加了暴露的风险:
js
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
:
js
// 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 攻击。