你需要了解的 5 个 Symbol 的使用场景

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)

上面代码中,addminus 都是魔术字符串。他们在代码中多次出现,与代码形成"强耦合",比如当日后表示 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 攻击

相关推荐
深度混淆5 分钟前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China5 分钟前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q6 分钟前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge
滴水可藏海7 分钟前
Chrome离线安装包下载
前端·chrome
endingCode11 分钟前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript
m512718 分钟前
LinuxC语言
java·服务器·前端
Myli_ing1 小时前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维1 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
I_Am_Me_2 小时前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
雯0609~2 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存