Symbol 是 Es6 引入一种新的基本类型,它的设计解决了对象私有化,这也是我们开发和封装库时经常会遇到的问题。实际上 Symbol 内置属性定义了许多 js 的默认行为,这也是 Symbol 强大的地方。
很多时候我们在查看库和框架源码时,代码里充满着各种 $ 、? 、 _ 、 __ 这类前缀的属性,其实这只是我们之间的约定,代表这这些变量只作为内部使用,我们开发者不应该去或者这些变量或方法。
这本质上没有解决问题,如果这个变量暴露出去了,比如 vue data里的 __ob__,开发者还是可以看到 key,也容易会被覆盖或修改,这时 Symbol 的作用就体现出来了。
Symbol的特性
创建一个 Symbol 时,可以接受一个参数,这个参数代表 Symbol 的描述信息,当调用 Symbol 的 toString() 方法时,描述信息会被输出。这会有利于更好的阅读代码和进行调试。
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)]
在对象里,
Symbol的key在调用for in、Object.getOwnPropertyNames、JSON.stringify、Object.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》