原来 Es6 中的 Symbol 这么重要

684 阅读1分钟

Symbol 是 Es6 引入一种新的基本类型,它的设计解决了对象私有化,这也是我们开发和封装库时经常会遇到的问题。实际上 Symbol 内置属性定义了许多 js 的默认行为,这也是 Symbol 强大的地方。

很多时候我们在查看库和框架源码时,代码里充满着各种 $?___ 这类前缀的属性,其实这只是我们之间的约定,代表这这些变量只作为内部使用,我们开发者不应该去或者这些变量或方法。

这本质上没有解决问题,如果这个变量暴露出去了,比如 vue data里的 __ob__,开发者还是可以看到 key,也容易会被覆盖或修改,这时 Symbol 的作用就体现出来了。

Symbol的特性

创建一个 Symbol 时,可以接受一个参数,这个参数代表 Symbol 的描述信息,当调用 SymboltoString() 方法时,描述信息会被输出。这会有利于更好的阅读代码和进行调试。

let myName = Symbol('my name')
console.log(typeof myName) // symbol
console.log(myName) // Symbol(my name)
console.log(myName.toString()) // Symbol(my name)

作为唯一的key

Symbol 可以作为对象的 key 来使用,这意味着我们可以分配多个 Symbol 属性到一个对象上,这些 key 保证了不会和其他 key 冲突。

let myName = Symbol('my name')
let obj = {
  [myName]: 'symbol myName',
  myName: '字符串 myName'
}
console.log(obj[myName]) // symbol myName
console.log(obj.myName) // 字符串 myName
console.log(Object.keys(obj)) // ["myName"]
console.log(Object.getOwnPropertyNames(obj)) // ["myName"]
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(my name)]

在对象里,Symbolkey 在调用 for inObject.getOwnPropertyNamesJSON.stringifyObject.keys 这些方法时,会被自动忽略。

在对象里想要获取所有的 Symbol,可以通过 Object.getOwnPropertySymbols 来获取。

共享 Symbol

本质上 Symbol 是独一无二的,但是有时候我们需要在不同的代码块里使用同一个 Symbol 的值时(虽然可以通过导出引入这种方式来解决,但是比较繁琐),要想得到 Symbol('my')Symbol('my') 相等这样的效果,我们通过 Symbol.for() 来实现。

Symbol.for() 方法接受一个字符串类型的参数,作为目标符号值的标识符和描述信息。

let a = Symbol.for('uid')
let b = Symbol.for('uid')

console.log(a === b) // true

Symbol.for() 方法会首先全局搜索 symbol 注册表,有没这个标识符存在,如果有则直接返回这个 symbol ,否则会创建一个新的 symbol ,并放到注册表里,也就是说只要描述符相同,那么它们就是同一个 symbol

Symbol.keyFor()方法接受一个 symbol 值来查询出对应的标识符。

let uid = Symbol.for('uid')
console.log(Symbol.keyFor(uid)) // 'uid'

let uid2 = Symbol('uid')
console.log(Symbol.keyFor(uid3)) // undefined

内置的 Symbol 属性

Symbol 的内置属性是指向 js 内部使用的方法,这也是 Symbol 比较重要的一部分。

Symbol.iterator

Symbol.iterator 定义了对象默认返回的迭代器对象。在 es6 中,数组、Set、Map、字符串,都是可迭代的对象,他们都会被默认指定迭代器,可以让 for of 循环调用。

对象默认没有Symbol.iterator,所有不能使用 for of 循环。

所有Symbol.iterator 对象都有 next() 方法,next() 方法会返回一个对象,对象里有两个属性,其中 done 代表是否结束,value 代表对应下一个的值。

下面创建一个迭代器:

function createIterator(items) {
  var i = 0
  
  return {
    next: function() {
    var done = (i >= items.length)
    var value = !done ? items[i++] : undefined
    return {
        done: done,
        value: value
      }
    }
  }
}

var iterator = createIterator([1, 2, 3])

console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

es6的 generator 是一个返回迭代器的函数,所以利用 generator 可以更简单的创建迭代器函数。

function *createIterator() {
  yield 1
  yield 2
  yield 3
}
let iterator = createIterator()
console.log(iterator.next().value) // 1
console.log(iterator.next().value) // 2
console.log(iterator.next().value) // 3

for of 本质上就是调用迭代器函数,对象默认是没有部署 Symbol.iterator 接口的,所有不能使用for of 遍历,不过我们可以在对象内添加一个自定义的迭代器。

var obj = {
  0: 1,
  1: 2,
  2: 3,
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let item of obj) {
  console.log(item)
}
// 1 2 3

利用 generator 更加方便:

var obj = {}
obj[Symbol.iterator] = function* () {
  yield 1
  yield 2
  yield 3
}
for (let item of obj) {
  console.log(item)
}
// 1 2 3

自定义的遍历对象:

var obj = {
  name: 'feng',
  age: 12,
  color: 'blue',
  [Symbol.iterator]: () => {
    var current = 0
    var len = Object.keys(obj)
    return {
      next() {
        if (current < len.length) {
          return {
            done: false,
            value: len[current++]
          }
        }
        return {
          done: true,
          value: undefined
        }
      }
    }
  }
}
for (let item of obj) {
  console.log(obj[item])
}
// 'feng' 12 'blue'

Symbol.hasInstance

Symbol.hasInstance 是表达式中的 instanceof 的运算行为,当我们调用 instanceof 运算符时,实际上就是调用 Symbol.hasInstance, 这个方法定义在 Function.prototype[Symbol.hasInstance]上。

function Person () {}
var a = new Person

a instanceof Person
// 相当于
Person[Symbol.hasInstance](a)

我们可以重写这个方法

function Person () {}
Object.defineProperty(Person, Symbol.hasInstance, {
   value: (v) => {
     console.log(v)
     return true
   }
})

var a = new Number(12)
var b = new Number(16)
a instanceof Person // Number {12}  true
b instanceof Person // Number {16}  true

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 属性是一个布尔值,是用来标记该对象在作为 concat() 参数时如何工作,只出现在特定类型的对象上。

  • 当值为 true 时:它表示目标对象拥有长度属性与数值类型的键、并且数值类型键所对应的属性值在参与 concat() 调用时需要被分离为个体。
  • 当值为 false 时:则无需被分离。
// 修改了默认行为,避免项目被分离。
let nums1 = [ '1', "2" ]
nums1[Symbol.isConcatSpreadable] = false
let nums2 = [].concat(nums1, ['3', '4'])
console.log(nums2) // [ ['1', '2'], '3', '4' ]

也可以用在对象上,让该对象参与 concat() 调用时能够和数组表现一致。

let color = {
  0: 'red',
  1: 'yellow',
  length: 2,
  [Symbol.isConcatSpreadable]: true
}

let arr = ['blue'].concat(color)
console.log(arr) // ['blue', 'red', 'yellow']

Symbol.match 、 Symbol.replace 、 Symbol.search 与 Symbol.split

这些方法都是对标正则表达式的方法,会被定义在 RegExp.prototype 上作为默认使用。所以这样可以自定义正则的规则。

let reg = {
  [Symbol.match]: function(value) {
    return 'match'
  },
  [Symbol.replace]: function(oldStr, replacement) {
    return 'replace'
  },
  [Symbol.search]: function(value) {
    return 'search'
  },
  [Symbol.split]: function(value) {
    return 'split'
  }
}
let msg = '自定义的正则'
console.log(msg.match(reg)) // match
console.log(msg.replace(reg)) // replace
console.log(msg.search(reg)) // search
console.log(msg.split(reg)) // split

自定义字符串的正则表达式

var str = new String('自定义的正则')
str[Symbol.split] = function (value) {
  var arr = ['!', '@', '#', '$', '^']
  var res = ''
  for (let i = 0; i < value.length; i++) {
    let ran = arr[Math.floor(Math.random() * arr.length)]
    res += value[i] + ran
  }
  return res
}
console.log('自定义的正则'.split(str))

Symbol.toPrimitive

当我们使用 == 运算符来将对象转换为原始值时,就会调用 Symbol.toPrimitive 方法。该方法被定义在所有常规类型的原型上。执行该方法时系统会自动传入字符串参数 number、 string、default,根据参数执行不同的分支。

function Person (age) {
  this.age = age
}

Person.prototype[Symbol.toPrimitive] = function(hint) {
  if (hint === 'string') {
    return this.age + ' 岁'
  } else if (hint === 'number') {
    return this.age
  } else if (hint === 'default'){
    return this.age + ' age'
  }
}

var my = new Person(12)

// 触发了 'default' 模式
console.log(my + '!') // '12 age!'
// 触发了 'number' 模式
console.log(my / 1) // 12
// 触发了 'string' 模式
console.log(String(my)) // '12 岁'

Symbol.toStringTag

Symbol.toStringTag 方法定义了 Object.prototype.toString.call() 被调用时应当返回的值。在 es6 之前,我们调用 Object.prototype.toString.call() 方法会返回 [object Object] 或者 [object Array]的内部定义名称,用来判断变量的类型。在 es6 之后,我们可以通过 Symbol.toStringTag 方法来定义它的行为。

function Person(name) {
    this.name = name
}

Person.prototype[Symbol.toStringTag] = 'Person'

let my = new Person('feng')

console.log(my.toString()) // [object Person]
console.log(Object.prototype.toString.call(my)) // [object Person]

参考

ES6 入门教程

《深入理解ES6》