什么是Symbol
Symbol是ES6中新增的一种基本数据类型,它是一个函数,会返回一个Symbol类型的值,每一个Symbol函数返回的值都是唯一的,它们可以被作为对象属性的标识符。
Symbol也具有静态属性和静态方法,它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局Symbol注册。
注意Symbol不算是一个完整的构造函数,因为它不能使用new关键字进行调用
语法
Symbol([description])
typeof
typeof Symbol(); // Symbol
description是Symbol的标识符,是可选的(该特性是在ES10新增的)
上手Symbol基本用法
👇 使用Symbol值作为对象的key 👇
let s1 = Symbol()
// Symbol()
let obj = {
[s1]:'s1'
}
// obj => {Symbol(): 's1'}
obj.s1
// undefined
obj[s1]
// 's1'
上面例子我们使用Symbol函数创建了s1,之后使用了s1作为obj1中key,这里有个需要注意的点,当我们使用变量去定义一个对象的key时需要使用[]包裹着,否则会被自动转化成string类型。这也就是为什么obj.s1时获取到的是undefined而不是对应的's1'。
对面上面的例子你可能还不太明白Symbol的用途,我们在看下面这个例子。
let info = {}
info.name='zhangshan'
info.age = 18
obj.s1
// zhangshan
obj.age
// 18
let age = Symbol('age')
info[age] = 20
info[age]
// 20
// 注意,请不要这样子定义属性,因为这样会无法获取到对应的Symbol实例
info[Symbol('age')] = 20
从这个例子中,我们在info对象中设置了两个age属性,一个是Symbol符号定义,另外一个是通过字面量的方式定义。按ES6之前的写法在没有Symbol的情况下我们是没法在一个对象在设定同名的属性,而在这个例子中我们利用Symbol的唯一性给info对象再定义一个age属性。当你要获取对应的值时只需要通过对应的Symbol获取即可。
Symbol('age') :Symbol函数接收一个参数description,它是可选的,用来对Symbol的描述,可用于调试但不是访问 Symbol 本身。
获取对象中的Symbol
在ES6中,新增了Object.getOwnPropertySymbols()方法,用于获取一个对象中的Symbol属性的数组。
let age = Symbol()
let info = {
name:'zhangshan',
[age]:18
}
console.log(Object.getOwnPropertySymbols(info))
// ['Symbol()']
它和Object.getOwnPropertyNames()方法类似,但是它不能获取到对象中的Symbol属性。
console.log(Object.getOwnPropertyNames(info))
// ['name']
在Object.keys和for...in循环语句中也是无法获取到Symbol属性的
for(const key in info){
console.log(key)
}
// name
console.log(Object.keys(info))
// ['name']
全局Symbol
在前面的例子中我们定义的s1是一个本地的Symbol,如果你的项目中在运行时需要共享和复用Symbol实例,这就需要使用到Symbol.for和Symbol.keyFor方法了
Symbol.for
Symbol.for方法,它接收一个key值,用于从Symbol注册表中获取对应的Symbol并返回,如果没有找到就创建一个新的Symbol与这个key进行关联,并放入全局Symbol注册表中。
// 创建全局Symbol key为foo
const fooGlob = Symbol.for('foo')
// 从全局注册表中获取foo Symbol
const fooGlob2 = Symbol.for('foo')
// 获取全局Symbol符号,需要传入标识符foo
const getFooGlob=Symbol.keyFor('foo')
Symbol.for()和Symbol()不同之处是前者创建的Symbol都会存入到全局Symbol注册表中,在获取时如果有该Symbol时会返回该Symbol,否则创建。后者则是每次都会创建一个不同的Symbol实例。
Symbol.keyFor
Symbol.keyFor方法用于获取全局Symbol注册表中与某个Symbol关联的键,它接收一个参数sym用于需要查找键值的某个Symbol,该方法会返回一个查找到Symbol的key值,否则返回undefined
// 创建全局Symbol key为foo
const fooGlob = Symbol.for('foo')
console.log(Symbol.keyFor(fooGlob))
// foo
// 创建本地Symbol 描述符为foo
const localFoo = Symbol('foo')
console.log(Symbol.keyFor(localFoo))
// undefined
Symbol.length
Symbol也有length属性,值为0。
使用场景
在实际项目开发中,有哪些情景会使用到Symbol呢?
模拟private
利用Symbol模拟private属性,让其外部无法访问到。
const _Phone = Symbol()
export default class Foo{
constructor(phone){
this[_Phone] = phone
}
}
在ES12中新增
Private Class Fields and Methods,可以使得属性或方法无法被外界访问
单例模式
// Person.js
class Person{
constructor(){
this.name = '_island',
this.age = 18
}
}
const key = Symbol.for('Person')
if(!window[key]){
window[key] = new Person()
}
export default window[key]
代替魔法字符串
例如你的项目中有一个角色选择功能,你可能会根据它们的标识来判断做一些对应事件。
if(type === 'DOCTOR'){
// 一些要处理的事情
}
if(type === 'PATIENT'){
// 一些要处理的事情
}
但是上面这种代码不是最好的解决方案,我们可以通过Symbol的方式对上面的代码进行修改。
const UserType = {
DOCTOR: Symbol(),
PATIENT: Symbol()
}
if(type === UserType.DOCTOR){
// 一些要处理的事情
}
if(type === UserType.PATIENT){
// 一些要处理的事情
}
如果你的项目是使用typescript的,推荐使用enum来管理
常用内置符号
在ES6中也引入了一些常用的内置符号,也就是well-known Symbol。
它们用于暴露语言内部行为,开发者可以访问、重写、模拟这些行为。改变原生结构的行为,比如下面所说的for-of循环会在遍历对象上使用[Symbol.iterator],如果我们重写下[Symbol.iterator]的值,将改变迭代对象时的行为。
所有的内置符号属性都是不可写,不可枚举,不可配置的,它们就是全局函数Symbol的普通字符串属性,指向一个符号的实例
注意 在提到ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如@@iterator 指的就是Symbol.iterator
Symbol.iterator
iterator是一个迭代器,当对象拥有了这一个迭代器之后可以使用for...of语句进行遍历。
在Array实例对象中存在[Symbol.iterator]属性,因此它可以使用for...of进行遍历。
[][Symbol.iterator]
// f values() { [native code] }
如果你使用for...of语句去遍历一个对象时,控制台将抛出一个类型错误,告诉你遍历的对象不是一个iterable,因为object实例对象上不存在[Symbol.iterator]属性。
let obj={a:'12'}
for(const item of obj){
// Uncaught TypeError: obj is not iterable
console.log(item)
}
在ES6之后出现了数组扩展运算符,其实际也是利用了iterator实现的,如果被解构的目标不存在[Symbol.iterator]是无法被正常解构的
const numbers = [1, 2, 3]
sum(...numbers)
// 6
如果你还不熟悉什么是扩展运算符可以点击这里
引出关于对象的扩展问题
但日常开发中我们也经常会在对象中使用...扩展运算符,按上面的说法JavaScript中的对象是不存在[Symbol.iterator]属性的,那么为什么在对象也是使用...扩展运算符?
在这里我们需要分清一下扩展运算符了,扩展运算符分为对象扩展运算符,数组扩展运算符。当在数组中使用扩展运算符时是数组扩展运算符,当在对象中使用扩展运算符时是对象扩展运算符,(好吧,有点废话了)
对象扩展运算符是在ES9之后新增的规范,也即是在ES9之后我们才可以在对象中使用对象扩展运算符。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
(Object Rest/Spread Properties)[github.com/tc39/propos…]
Symbol.asyncIterator
asyncIterator是一个异步迭代器,配合着for-await-of语句使用。
当使用for-await-of语句循环对象时,内部会调用asyncIterator这个函数,遍历异步可迭代对象以及同步可迭代对象。
class Demo {
constructor(num) {
this.num = num
}
async *[Symbol.asyncIterator]() {
let i = 0
while (i < this.num) {
yield new Promise(resolve => resolve(i++))
}
}
}
const d1 = new Demo(5)
async function asyncCount() {
// 实现遍历
for await (const item of d1) {
console.log(item);
}
}
asyncCount()
Symbol.hasInstance
Symbol.hasInstance用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。
当我们用instanceof操作符,会调用Symbol.hasInstance函数来确定关系。
function Foo(){}
let f = new Foo()
console.log(f instanceof Foo) // true
console.log(Foo[Symbol.hasInstance](f)) // true
也可以修改它的默认行为,在类中重新定义这个静态方法。
class Bar {
static [Symbol.hasInstance](instance) {
return false;
}
}
let b = new Bar()
console.log(b instanceof Bar) // false
console.log(Bar[Symbol.hasInstance](b)) // false
Symbol.species
Symbol.species用于当使用Array.prototype.Map()时生成派生对象的构造方法,取代原有的对象。除了Map方法,在filter、slice等方法也部署了Symbol.species。
class Foo extends Array {}
f.map(i=>i) instanceof Foo; // true
f.map(i=>i) instanceof Array; // true
// 改变返回时
class Bar extends Array{
static get [Symbol.species](){
return Array
}
}
let b = new Bar(1,2,3)
b.map(i=>i) instanceof Bar; // false
b.map(i=>i) instanceof Array; // true
Symbol.match
Symbol.match用于匹配正则表达式而不是字符串,当调用String.prototype.match()时,会先去调用该函数。
const fooReg = /foo/;
console.log('/foo/'.startsWith(fooReg));
// TypeError: First argument to String.prototype.startsWith must not be a regular expression
fooReg[Symbol.match] = false;
console.log('/foo/'.startsWith(regexp1)); // true
Symbol.match还用于标识对象是否具有正则表达式的行为,例如String中的startsWith、endsWith方法都会去检测第一个参数是否为正则表达式,如果是就抛出TypeError,你可以使用Symbol.match修改它的行为。
Symbol.isConcatSpreadable
Symbol.isConcatSpreadable用于配置某些对象作为Array.prototype.concat()方法时是否展开其数组元素。
//默认情况下,被拼接的元素是展开的
let arr1 = [1,2,3];
let arr2 = [4,5,6];
console.log(arr1.concat(arr2)) // [1, 2, 3, 4, 5, 6]
// 将arr2的isConcatSpreadable设置为false
arr2[Symbol.isConcatSpreadable]=false
console.log(arr1.concat(arr2)) // [1, 2, 3, Array(3)]
Symbol.toStringTag
Symbol.toStringTag由内置方法Object.prototype.toString()使用,当通过toString()方法获取时,会检索由Symbol.toString指定的实例标识符,默认情况下为Object,在内置类型已经指定了这个值,但自定义实例默认是undefined,可以在自定义类添加Symbol.toStringTag属性即可添加上你的实例标识符。
// 内置类型
let m = new Map()
console.log(m.toString()) // [object Map]
console.log(m[Symbol.toStringTag]) // Map
// 自定义类
class Foo{}
let f = new Foo()
console.log(f.toString())
console.log(f[Symbol.toStringTag]) // undefined
// 自定义实例标识符
class Bar {
get [Symbol.toStringTag]() {
return 'bar';
}
}
let b = new Bar()
console.log(b.toString()) // [object bar]
console.log(b[Symbol.toStringTag]) // bar
Symbol.toPrimitive
Symbol.toPrimitive用于当一个对象被转换成数据类型时会调用该函数。
const foo = {
[Symbol.toPrimitive](hint) {
console.log(hint)
if (hint === 'string') {
return 'bar';
}
return null;
}
};
console.log(String(foo)) // bar
Symbol.unscopables
Symbol.unscopables用于解除对象属性在with语句中的绑定。
const foo = {
name:'_island',
age:18
}
foo[Symbol.unscopables] = {
name: true
};
with(foo){
console.log(name) // 打印空白,因为name在with环境中已经被排除了
console.log(age) // 18
}
不推荐使用with语句,所以也就不推荐使用Symbol.unscopables
Symbol.replace
当该对象被String.prototype.replace方法调用时会调用Symbol.replace,会返回该方法的返回值。
class Foo{
[Symbol.replace](string){
return 'foo' + string
}
}
console.log('bar'.replace(new Foo()))
// foobar
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。