Symbol 基本用法详解

289 阅读4分钟

前言

ES5的对象属性名都是字符串,这样容易造成属性名冲突。
ES6新增了一种新的原始数据类型Symbol

基本使用

创建Symbol实例使用Symbol函数
每一个 Symbol 值都是不相等的

如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

let s1 = Symbol('a') // Symbol(a)
let s2 = Symbol('a') // // Symbol(a)
console.log(s1 === s2) // false

let s3 = Symbol({ a: '1' }) // // Symbol([object Object])
let s4 = Symbol(undefined) // Symbol()
let s5 = Symbol(null) // Symbol(null)
const obj1 = {
  toString() {
    return 'abc'
  }
}
let s6 = Symbol(obj1) // Symbol(abc)

ES2019 新增description的属性

let s7 = Symbol('abc')
console.log(s7.toString(s7)) // Symbol(abc)
console.log(s7.description) // abc

Symbol 共享体系

上面使用 Symbol() 函数的语法,不会在你的整个代码库中创建一个可用的全局的 symbol 类型。要创建跨文件可用的 symbol,甚至跨域(每个都有它自己的全局作用域),使用 Symbol.for() 方法和 Symbol.keyFor() 方法从全局的 symbol 注册表设置和取得 symbol。

注意,Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。
Symbol.for()的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。

例子:

Symbol.for('abc') === Symbol.for('abc') // true
Symbol('abc') === Symbol('abc') //false

keyFor:

const s1 = Symbol.for('abc')
const k1 = Symbol.keyFor(s1) // abc
function foo() {
    // 在函数内部运行的,但是生成的 Symbol 值是登记在全局环境的
  return Symbol.for('abc');
}
const x = foo();
const y = Symbol.for('abc');
console.log(x === y); // true

属性名的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回 。
可以使用Object.getOwnPropertySymbols()方法,获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

const a =  Symbol('a')
const b =  Symbol('b')
const obj1 = {}
obj1[a] = 'aa'
obj1[b] = 'bb'
const objectSymbols = Object.getOwnPropertySymbols(obj1) // [ Symbol(a), Symbol(b) ]

应用场景:自定义私有属性

function Fn() {
  const test = Symbol('test')
  const obj = {}
  obj[test] = 'test'
  return obj
}

const obj1 = Fn()
console.log(obj1[Symbol('test')]) // undefined

内置通用(well-known)Symbol

除了自己创建的 symbol,JavaScript 还内建了一些在 ECMAScript 5 之前没有暴露给开发者的 symbol,它们代表了内部语言行为。它们可以使用以下属性访问:

  • Symbol.hasInstance 一个在执行instanceof时调用的内部方法,用于检测对象的继承信息。
  • Symbol.isConcatSpreadable 一个布尔值,用于表示当传递一个集合作为Array.prototype.concat方法参数时,是否应该将集合内的元素规整到同一层级
  • Symbol.iterator 一个返回迭代器的方法
  • Symbol.match 一个在调用String.prototype.match方法时调用的方法,用于比较字符串。
  • Symbol.replace 一个在调用String.prototype.replace方法时调用的方法,用于替换字符串的子串。
  • Symbol.search 一个在调用String.prototype.search方法时调用的方法,用于在字符串中定位子串。
  • Symbol.species 用于创建派生对象的构造函数。
  • Symbol.split 一个在调用String.prototype.split方法时调用的方法,用于分割字符串。
  • Symbol.toPrimitive 一个返回对象原始值的方法。
  • Symbol.toStringTag 一个在调用String.prototype.toString方法时调用的方法,用于创建对象描述。
  • Symbol.unscopables 一个定义了一些不可被with语句引用的对象属性名称的对象集合。

Symbol.hasInstance

每一个函数都有一个Symbol.hasInstance方法,用于确定对象是否为函数的实例。该方法在Function.prototype中定义,所有所有函数都继承了instanceof属性的默认行为。为了确保Symbol.hasInstance不会被意外重写,该方法被定义为不可写、不可配置且不可枚举。

obj instantce Array 等价于Array[Symbol.hasInstance](obj)

所以,ES6 只是将instanceof操作符重新定义为此方法的简写语法,现在我们可以随意地改变instanceof的运行方式了。比如:

function MyObject() {}
Object.defineProperty(MyObject, Symbol.hasInstance, {
    value: function(v) {
      return false
    }
})
const obj = new Object()
console.log(obj instanceof MyObject) // false

只有通过 Object.defineProperty()方法才能改写一个不可写属性。

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable是一个布尔值,如果该属性为true,则表示对象有length属性和数字键,故它的数值属性应该被独立添加到concat调用结果中。

const collection = {
  0: 'hello',
  1: 'world',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}
const result = ['hi'].concat(collection)
console.log(result.length) // 3
console.log(result)

也可以在派生类数组子类中将Symbol.isConcatSpreadable设置为false,从而防止元素在调用concat方法时被分解。

Symbol.toPrimitive

在JS中,当执行特定操作时,有时尝试将对象转换到相应的原始值。在ES6中,通过Symbol.toPrimitive方法就可以更改那个暴露出来的值。调用Symbol.toPrimitive会传入一个值作为参数(hint),有如下三个值:

  • number
  • string
  • default
let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 123;
      case 'string':
        return 'str';
      case 'default':
        return 'default';
      default:
        throw new Error();
     }
   }
};

console.log(2 * obj) // 246
console.log(3 + obj) // '3default'
console.log(obj == 'default') // true
console.log(String(obj)) // 'str'

Symbol.toStringTag

ES6 通过 Symbol.toStringTag改变了调用Object.prototype.toString()时返回的身份标识。

也就是说,这个属性可以用来定制[object Object][object Array]object后面的那个字符串。

function Person(name){
  this.name = name
}
Person.prototype[Symbol.toStringTag] = 'Person'

const p1 = new Person('Jack')
console.log(p1.toString()) // [object Person]
console.log(Object.prototype.toString.call(p1)) // [object Person]