ES6中Symbol你都弄明白了吗?

173 阅读5分钟

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

Symbol还可以用来做什么事情?

1. Symbol.iterator改变for of的输出结果

我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。对于迭代器和生成器可以看看我之前的一篇文章.

在类里面定义*[Symbol.iterator]() {} 方法可以让类生成的实例具有可以迭代的属性.比如

class Emitter {
 constructor(max) {
 this.max = max;
 this.idx = 0;
 }
 *[Symbol.iterator]() {
 while(this.idx < this.max) {
 yield this.idx++;
 }
 }
}
function count() {
 let emitter = new Emitter(5);
 for (const x of emitter) {
 console.log(x);
 }
}
count(); 
// 0 
// 1
// 2
// 3
// 4 

这种方式是隐式地调用了函数内部的next方法,我们可以通过实例化上面键名为Symbol.iterator的方法去获取迭代器,在通过next获取值和状态,比如

class Emitter {
 constructor(max) {
 this.max = max;
 this.idx = 0;
 }
 *[Symbol.iterator]() {
 while(this.idx < this.max) {
 yield this.idx++;
 }
 }
}
let emitter = new Emitter(5);
let iterator = emitter[Symbol.iterator]() //生成迭代器对象
console.log(iterator.next()) //{value: 0, done: false}

回到正题,我们要怎么才能改变for of的输出结果? 只要我们在Symbol.iterator函数里面将yield 的东西改成自己想要的东西比如yield '一叶很好看'这样的话,for of输出的结果就会变成 yield后面的值.上代码:

class yiYe {
 *[Symbol.iterator]() {
 let i=10;
 while(i--) {
 yield '一叶很好看'
 }
 }
}
let ye = new yiYe()
for(const a of ye){
    console.log(a)
}

2.Symbol.asyncIterator 可以迭代操作异步的数据(可以用来控制爬虫)

根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator。 由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。 for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator 为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。

它可以让for-of 增加一个await的功能,这里的await和async/await的await类似,可以接受promise对象return的结果

class Emitter {
 constructor(max) {
 this.max = max;
 this.asyncIdx = 0;
 }
 async *[Symbol.asyncIterator]() {
 while(this.asyncIdx < this.max) {
 yield new Promise((resolve) => 
    setTimeout(()=>{
        resolve(this.asyncIdx++)
},1000)

);
 }
 }
}
async function asyncCount() {
 let emitter = new Emitter(5);
 for await(const x of emitter) {
 console.log(x);
 }
}
asyncCount(); 
// 0
// 1
// 2
// 3
// 4 每隔一秒输出一次

你们可能想到了可以用它来控制爬虫,一秒钟发送若干次,妈妈再也不怕我的下载萌妹子图片的请求太频繁把人服务器搞奔溃了.

3.Symbol.hasInstance 可以更改instanceof的输出结果

instanceof 操作符可以用来确定一个对象 实例的原型链上是否有原型。先简单介绍一下instanceof的使用场景

function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true 

在ES6中 instanceof的功能和函数方法为Symbol.hasInstance的函数一样.语言表达不了我内心的想法 上代码:

function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true 
// Foo[Symbol.hasInstance](f) <===> f instanceof Foo 

实际上,instanceof操作符会在原型链上寻找这个属性定义,我们可以更改这个原型链上面的静态方法来更改instanceof的返回结果

class Bar {}
class Baz extends Bar {
 static [Symbol.hasInstance]() {
 return false;
 }
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false 

4. Symbol.isConcatSpreadable可以决定Array.prototype.concat方法是否打散连接数组

数组实例对象上面的属性Symbol.isConcatSpreadable默认是Undefined,但通过改变它的值可以控制类数组对象和数组对象是否要被打平(flat)再连接到数组上面.

let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)] 
let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo'] 

5.字符串的replace,match,search,split与Symbol的联系

Symbol.match, Symbol.replace,Symbol.search, Symbol.split

根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式 去匹配字符串。由 String.prototype.match()方法使用”。String.prototype.match()方法会使 用以 Symbol.xxxx为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义, 因此所有正则表达式实例默认是这个 String 方法的有效参数.

也就是说Symbol.xxxx接收一个字符串或者正则表达式,如果是字符串的话就先转化成正则表达式再进行匹配.

那我不想用正则我非要用字符串怎么办,通过修改对象的Symbol.xxxx方法就可以实现想要的效果了

class FooMatcher {
 static [Symbol.match](target) {
 return target.includes('foo');
 }
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
 constructor(str) {
 this.str = str;
 }
 [Symbol.match](target) {
 return target.includes(this.str);
 }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false 

6.Symbol.species

我没想到怎么用,定义如下:

根据 ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构 造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方 法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义:

class Bar extends Array {}
class Baz extends Array {
 static get [Symbol.species]() {
 return Array;
 }
}
let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true
let baz = new Baz();
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true
baz = baz.concat('baz');
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // false 

它也能影响instanceof的结果

7.Symbol.toPrimitive

这个方法可以控制对象的原始值,原始值在类型转化的时候经常用到,比如1=='1'就是把字符串转化成数字的原始值再进行比较,如果没有设置这个方法,对象的原始值默认是[object Object],前一个object是对象,第二个Object是对象的字符串描述,通常为类名,可以通过Symbol.toStringTag修改

根据提供给这个函数的参数(string、number 或 default),可以控制返回的原始值:

class Foo {}
let foo = new Foo();
console.log(3 + foo); // "3[object Object]"
console.log(3 - foo); // NaN
console.log(String(foo)); // "[object Object]"
class Bar {
 constructor() {
 this[Symbol.toPrimitive] = function(hint) {
 switch (hint) {
 case 'number':
 return 3;
 case 'string':
 return 'string bar';
 case 'default':
 default:
 return 'default bar';
 }
 }
 }
} 
let bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3 - bar); // 0
console.log(String(bar)); // "string bar" 

8.Symbol.toStringTag

根据 ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认 字符串描述。由内置方法 Object.prototype.toString()使用”。 通过 toString()方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默 认为"Object"。内置类型已经指定了这个值,但自定义类实例还需要明确定义:

let s = new Set();
console.log(s); // Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set
class Foo {}
let foo = new Foo();
console.log(foo); // Foo {}
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined
class Bar {
 constructor() {
 this[Symbol.toStringTag] = 'Bar';
 }
}
let bar = new Bar();
console.log(bar); // Bar {}
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar 

9.Symbol.unscopables

更改with环境下的环境,不常用也不推荐用

let o = { foo: 'bar' };
with (o) {
 console.log(foo); // bar
}
o[Symbol.unscopables] = {
 foo: true
};
with (o) {
 console.log(foo); // ReferenceError
}