前言
ECMAScript 6(简称ES6),于2015年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2015(ES2015)。ES6之前,基本数据类型分五种:Boolean、Number、String、Null 和 Undefined,ES6为防止因对象合并发生属性名冲突而被覆盖,增加了一种 Symbol 基本数据类型,表示独一无二的值。
下面将从创建方式(Symbol() 函数 和 Symbol.for())、Symbol.for() 和 Symbol.keyFor()、使用场景(作对象的属性名和替代某些场景下的常量)和 遍历(Object.getOwnPrototypeSymbols()和Reflect.ownKeys())四个方面来学习。
Symbol 介绍
创建 Symbol 类型值
创建 Symbol 类型值,不是通过 new 关键字(因为 new 关键字是创建一个对象的实例),而是 Symbol() 函数,其 typeof 值为 "symbol"
。
const nameProp = Symbol()
console.log(nameProp, typeof nameProp) // Symbol() "symbol"
因为 Symbol 表示的是独一无二的值,即使连着创建两个 Symbol 类型的值,也是不同的,例如:
const nameProp = Symbol()
const ageProp = Symbol()
console.log(nameProp, ageProp, nameProp === ageProp) // Symbol() Symbol() false
后面还会介绍另一种方式(Symbol.for())创建 Symbol 类型值。
增加描述内容
Symbol.prototype.description:访问其”实例“的描述
为字面化,易理解,建议增加描述内容即在 Symbol() 函数传入参数,通过 .description 访问描述 (该描述也会被视作该 Symbol 类型值的 key):
const nameProp = Symbol("name")
const ageProp = Symbol("age")
console.log(nameProp, nameProp.description) // Symbol(name) "local"
Symbol.for() 和 Symbol.keyFor()
如果要使用前面已创建过的 Symbol 类型值,可使用 Symbol.for(key),该方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局(Symbol.for() 定义的 Symbol 类型的值具有全局登记特性)。
Symbol.for()
与 Symbol()
区别在于, 调用 Symbol("name")
30 次,返回 30 个不同的 Symbol 类型值,而 Symbol.for("name")
调用 30 次,返回的是 30 个相同的 Symbol 类型值。例如:
const ageProp = Symbol("age")
const childAgeProp = Symbol("age")
console.log(ageProp, childAgeProp, ageProp === childAgeProp) // Symbol(age) Symbol(age) false
const nameProp = Symbol.for("name")
const childNameProp = Symbol.for("name")
console.log(nameProp, childNameProp, nameProp === childNameProp) // Symbol(name) Symbol(name) true
Symbol.keyFor()
方法接收一个 Symbol 类型的值,返回一个已登记的 Symbol 类型值的key
,如果 Symbol 类型值是通过 Symbol() 创建的,因为没有被全局注册则返回 undefined;如果是通过 Symbol.keyFor() 创建的则返回其key(描述内容)。
console.log(Symbol.keyFor(ageProp)) // undefined
console.log(Symbol.keyFor(nameProp)) // name
Symbol 作用
因为 Symbol 类型为对象合并引起的属性名相同而被改写或覆盖,扩展对象的属性名类型,首先便是可做对象的属性名(ES6之前属性名只能是字符串类型,ES6后支持两种类型:字符串和Symbol类型)。又因为代表的是独一无二的值,可代替某些场景下的 const 定义的常量(为什么说是某些场景?举例说明:定义常量 storageType 表示本地存储方式(localStorage OR sessionStorage),而后通过 storageType.getItem() 获取指定值, 这种场景下,便不可将 storagteType 定义为 Symbol 类型,因为 storageType 不仅是一个常量而且还可通过该常量去访问其值的属性或方法)。
作对象的属性名
const nameProp = Symbol("name")
const ageProp = Symbol("age")
const person = {
[nameProp]: "露水晰123",
}
person[ageProp] = 18
console.log(person) // {Symbol(age): 18,Symbol(name): "露水晰123",__proto__: Object}
代替某些场景下的 const 常量
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
消除魔术字符串常用的方式是将其改为一个变量,更改上述代码如下:
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
但是,仔细分析,可以发现shapeType.triangle
等于哪个值并不重要,只要确保不会跟其他shapeType
属性的值冲突即可。因此,这里就很适合改用 Symbol 值。上面代码中,除了将shapeType.triangle
的值设为一个 Symbol,其他地方都不用修改。
const shapeType = {
triangle: Symbol()
};
Symbol 属性名的遍历
Object.getOwnPropertySymbols() 只获取一级 Symbol 类型的属性名
Symbol 类型的属性名不会出现在 for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。即以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法(外部不能访问)。
但是可以通过 Object.getOwnPropertySymbols(obj) 获取对象 obj 里定义的 Symbol 类型的属性名,以数组的形式返回。
const nameProp = Symbol("name")
const ageProp = Symbol("age")
const studentProp = Symbol("student")
const selfProp = Symbol("self")
const person = {
date: new Date(),
[nameProp]: "露水晰123",
}
person[ageProp] = 18
person[studentProp] = [
{ [nameProp]: "小晰1", [ageProp]: 19 },
{ [nameProp]: "小晰2", [ageProp]: 20 },
]
person[selfProp] = {
[selfProp]: 18,
}
console.log(person)
console.log(Object.getOwnPropertySymbols(person))
打印结果如下:
Obejct.getOwnPropertySymbols(person) 返回的是对象 person 的一级属性名称中 Symbol 类型值,不包含深层的。
Reflect.ownKeys() 获取对象的所有一级属性
console.log(Reflect.ownKeys(person))
内置的 Symbol 值
除了自定义的 Symbol 类型值,ES6提供了 11 个内置的 Symbol 值,指向语言内部的使用方法。
Symbol.iterator
ES6 新增了一个机制:遍历器(Iterator)机制,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历(Iterable)操作(即依次处理该数据结构的所有成员)。
基本的数据结构主要有:对象(Object)、数组(Array),还有 ES6 新增的 Set、WeakSet、Map、WeakMap 四种数据结构。实际项目中还有众多简单或复杂的数据结构,针对这部分的数据结构的遍历便需要手动设置遍历器。
遍历器(Iterator)机制 的作用有三个:
- 一是为各种数据结构,提供一个统一的、简便的访问接口(通过 next() 方法更改指针对象依序指向下一个成员,并返回一个包含 done(是否结束遍历) 和 value(当前项的值) 属性的对象);
- 二是使得数据结构的成员能够按某种次序排列(按顺序依次往下,可由用户手动指定该顺序;正是因为对象(Object)不确定哪个属性先遍历,哪个属性后遍历,故没有给默认的 Iterator(原生具备 Iterator 接口的数据结构:Array、 Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象),需要用户手动通过 Symbol.iterator 指定);
- 三是 ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费。
console.log([1,2,3])
// 该数组的原型上有属性Symbol(Symbol.iterator): ƒ values()
// 该属性便是定义数组的遍历器, 也是数组可以直接遍历的原因
Iterator 的遍历过程是这样的。
- (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- (2)第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员。 - (3)第二次调用指针对象的
next
方法,指针就指向数据结构的第二个成员。 - (4)不断调用指针对象的
next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
遍历器首先是一个函数,该函数返回一个对象且包含一个next方法,该next方法又返回一个包含value(是当前遍历得到的值)和done(布尔值,表示是否是结束遍历即done为true表示没有可遍历的元素了)的对象。
const it = makeArrayIterator([1,2])
console.log("it", it)
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {value: undefined, done: true} 结束了
/**
* 自定义遍历器
* @param {*} arr
* @returns
*/
function makeArrayIterator(arr) {
let index = 0 // 闭包变量
return {
next() {
// ==index++和++index的区别==
// ==index++是先使用index再+1==
// ==++index是先+1再使用index==
return (index < arr.length)
? {value:arr[index++], done:false} // 当前遍历的值
: {value:undefined, done:true} // 表示已遍历完最后一个元素,结束遍历
}
}
}
给对象添加遍历器
对象的Symbol.iterator
属性,指向该对象的默认遍历器方法。针对对象进行for...of
循环时,调用Symbol.iterator
方法(也可以在原型链上定义),返回该对象的默认遍历器。
例如,给对象 obj 定义一个遍历器,依序访问该对象的属性,代码如下:
let obj = {
data: [ 'parent', 'name', 'age', 'brother', 'children' ],
name: '露水晰123',
age: 18,
parent: [{
name: '露水晰12',
age: 36,
}],
brother: [{
name: '露水晰124',
age: 19,
}],
children: [{
name: '露水晰1231',
age: 1,
}],
[Symbol.iterator]() {
const $self = this;
const {data} = $self
let index = 0;
return {
next() {
if (index < data.length) {
return {
value: $self[data[index++]],
done: false
};
}
return { value: undefined, done: true };
}
};
}
};
for (var i of obj){
console.log(i);
}
打印结果如下:
对对象使用数组形式的解构赋值
解构赋值是ES6新增的针对对象和数组数据结构中提取值的一种模式,例如let [a, b, c] = [1, 2, 3]
,对等式右边的数组进行解构,分别赋值到变量a,b,c。
// ==正常情况下对对象或对数组解构赋值==
// let {c,d} = {c:1,d:2}
// let [c,d] = [c:1, d:2]
// ==可以对数组做对象形式的解构赋值==
// ==但由于对象的解构赋值是按键名来提取的,数组[1,2]对应的键名为0,1, 没有e和f对应键名, 所以e和f是undeined==
// let {e,f} = [1,2]
// console.log(e,f) // undefined,undefined
// ==但若是对对象做数组形式的解构赋值则不行,程序报错 Uncaught TypeError: {(intermediate value)(intermediate value)} is not iterable, 表示该对象不具备迭代器属性==
let [c,d] = {c:1, d:2} // 报错
那为什么不可以对对象做数组形式的解构赋值?
因为对数组解构赋值会默认调用Symbol.iterator
方法,而对象本身没有Symbol.iterator
方法。
事实上,只要某种数据结构具有Iterator接口即设置了遍历器, 都可以采用数组形式的解构赋值。
下面给对象原型添加Symbol.iterator
方法即迭代器:
Object.prototype[Symbol.iterator] = function () {
// ==方式一:获取对象自身定义的属性值(Object.values(this)),根据索引(index)一个一个取==
const $self = this
const curValues = Object.values($self) // 自身所有的键对应的值且不包含键类型为Symbol的值
let index = 0
return {
next() {
return index < curValues.length
? { value: curValues[index++] }
: { done: true }
}
}
// ==方式二:利用数组的遍历器(Object.values(this))==
// ==Object.values(this)获取当前对象的值组成的数组,而数组本身包含迭代器,所以可直接将该迭代器返回,由于迭代器本身是一个函数,所以需要执行该函数以在函数中返回==
// return Object.values(this)[Symbol.iterator]()
}
执行对对象解构赋值语句:
const targetObj = {c:1, d:2}
// ==对对象做解构赋值操作==
let [c,d] = targetObj
console.log(c, d) // 1,2
// ==遍历对象==
for (let i of targetObj) {
console.log(i) // 1/2
}
ok,完成对对象做数组形式的解构赋值~
总结
- Symbol 表示的是独一无二的值,通过 Symbol() 函数或者 Symbol.for() 函数创建,是基本数据类型;
- Symbol 可以做为对象的属性名(ES6之前只能是字符串);
- Symbol 可以代替某些场景下的 const 常量;
- Symbol 不会出现在
for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回,因为他们涉及的属性名是字符串; - 遍历对象的 Symbol 类型的属性,通过 Object.getOwnPropertySymbols(obj),但是只是一级的,不包含深层;Reflect.ownKeys() 可以获取对象的所有类型的键名(字符串和Symbol类型的);
- Symbol.for() 全局登记特性,与 Symbol() 的区别在于调用多次前者返回的是相同的而后者返回的是不同的;
- Symbol.keyFor() 返回一个已登记的 Symbol 类型值的 key,如果未登记则返回 undefined;
- Symbol.iterator是ES6提供的内置值,定义遍历器(遍历器首先是一个函数,返回一个对象,该对象包含next方法,而next方法又返回一个包含value和done的对象);
- 可以给非数组的数据结构定义遍历器,从而可以使用for...of循环遍历;
- 对对象做数组形式的解构,需要给对象添加
Symbol.iterator
方法即定义迭代器。