搞清楚ECMAScript 6新增的基本数据类型Symbol
前言
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
对象的Symbol.iterator
属性,指向该对象的默认遍历器方法。针对对象进行for...of
循环时,便是调用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
消费。
例如,给对象 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;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self[self.data[index++]],
done: false
};
}
return { value: undefined, done: true };
}
};
}
};
for (var i of obj){
console.log(i);
}
打印结果如下:
总结
- 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;
参考
转载自:https://juejin.cn/post/7259234035393888293