这一篇文章继续分享高程4第3章的内容,现在讲到了Symbol类型。
3. 使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty() / Object.definedProperties() 定义的属性。对象字面量只能在计算属性语法中使用符号作为属性(也就是,必须在中括号[ ]中才能使用符号)
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
// 在字面量中定义symbol属性
let o = {[s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val';
console.log(o);
// {Symbol(foo): foo val}
// 用Object.defineProperty来定义属性,参数是对象,属性名,属性的特性(可以描述值,可配置性,可写性,可枚举性4个特性)
Object.defineProperty(o, s2, {value: 'bar
val'});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): barval}
// 用Object.defineProperties来定义属性,参数是对象,要定义的属性及其特性
Object.defineProperties(o, {[s3]: {value: 'baz val'},[s4]: {value: 'qux val'}
});console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}
类似于 Object.getOwnPropertyNames() 返回对象实例的常规属性数组, Object.getOwnPropertySymbols() 返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。
Object.getOwnPropertyDescriptors() 会返回同时包含常规和符号属性描述符的对象。 Reflect.ownKeys() 会返回两种类型的键。
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键。
下面这一段代码的意思是:对象o里面有两个符号属性,这两个符号属性是直接把Symbol函数得到的值作为了属性名,而不是事先准备好Symbol值在变量里面,再用“[变量名]”作为属性名的。这样的话,如果想要取得某个属性的值,就必须要遍历它的所有符号属性来查找才能拿到它(把symbol类型转成字符串类型会得到“Symbol(描述)",所以可以使用match来查找)
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "barval"}
let barSymbol = Object.getOwnPropertySymbols(o).find((symbol) =>
symbol.toString().match(/bar/));console.log(barSymbol);
// Symbol(bar)
4. 常用内置符号
ECMAScript 6也引入了一批常用内置符号(well-knownsymbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在(也就是Symbol.xxx的形式)
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上
重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。
这些内置符号也没有什么特别之处,它们就是全局函数 Symbol的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
注意 在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为 @@ 。比如, @@iterator 指的就是Symbol.iterator 。
5. Symbol.asyncIterator
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator 。由 for-awaitof 语句使用”。换句话说,这个符号表示实现异步迭代器API的函数
for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator 为键的函数,并期望这个函数会返回一个实现迭代器API的对象。很多时候,返
回的对象是实现该API的 AsyncGenerator
class Foo {
async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
这个由 Symbol.asyncIterator 函数生成的对象应该通过其 next() 方法陆续返回 Promise 实例。可以通过显式地调用 next() 方法返回,也可以隐式地通过异步生成器函数返回
下面的这一段代码就是隐式地通过一部生成器函数返回了Promise实例,因为有await,所以得到的结果是promise的决议值,体现了异步函数和生成器的两块知识点。
class Emitter {constructor(max) {this.max = max;this.asyncIdx = 0;} async *[Symbol.asyncIterator]() {while(this.asyncIdx < this.max) {yield new Promise((resolve) =>resolve(this.asyncIdx++));}}
}async function asyncCount() {let emitter = new Emitter(5);for await(const x of emitter) {console.log(x);}
} asyncCount();
// 0
// 1
// 2
// 3
// 4
注意 Symbol.asyncIterator 是ES2018规范定义的,因此只有版本非常新的浏览器支持它。
6. Symbol.hasInstance
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof 操作符使用。 instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型。 instanceof 的典型使用场景如下:
function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // trueclass Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true
在ES6中, instanceof 操作符会使用Symbol.hasInstance 函数来确定关系。以Symbol.hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下(instanceof关键字的操作数是构造函数/类,这个函数的参数是实例)
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); //true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); //true
这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof 操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在
继承的类上通过静态方法重新定义这个函数(下面的代码就是重写了子类的这个方法,让子类的这个方法一律返回false。因为子类重写了,所以instanceof使用到子类的这个方法时,根据就近原则,没有到原型上查找,而是直接用了静态属性这里的方法,所以返回了false。而原型上默认的方法会返回true的,像父类那样。)
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
7. Symbol.isConcatSpreadable
根据ECMAScript规范,这个符号作为一个属性表示“一个布尔值,如果是 true ,则意味着对象应该用Array.prototype.concat() 打平其数组元素”。ES6中的Array.prototype.concat() 方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable 的值可以修改这个行为。
数组对象默认情况下会被打平到已有的数组(就是把数组元素一个个放到另一个数组后面), false 或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾(就是整个对象作为另一个数组的最后一个元素), true 或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象的对象在Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略。(也就是只有数组对象或者类数组对象可以打平到已有数组,数组对象的这个属性默认表现为这个值为true的样子,类数组对象的默认表现为这个值为false的样子。其他的对象默认表现是这个值为false的样子,改为true后,直接被忽略,不会被加到数组里去。这个属性的默认值一律是undefined)
下面的代码展示了这三种类型的对象的效果:
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']