1

首先先熟悉一下Symbol类型的定义及其作用:

  • 可以用作对象属性键的非字符串值的集合。
  • 每个Symbol值都是惟一且不可变的。
  • 每个Symbol值都与一个[[Description]]的值关联,该值要么是undefined,要么是一个字符串。

内置的Symbol值主要是用于ECMAScript规范算法扩展的。本文主要是通过对规范的解读来了解Symbol内置的值是如何使用及其规范定义的。

我们先浏览一下规范里的Symbol内置的值:

规范名称Description值及其作用
@@asyncIterator"Symbol.asyncIterator"一个返回异步迭代器的方法,主要用于for await
@@hasInstance"Symbol.hasInstance"用于确认对象是否为该构造函数实例的方法,主要用于instanceof
@@isConcatSpreadable"Symbol.isConcatSpreadable"一个Boolean值,标识是否可以通过Array.prototype.concat进行扁平化处理
@@iterator"Symbol.iterator"一个返回异步迭代器的方法,主要用于for of
@@match"Symbol.match"用于String.prototype.match调用
@@replace"Symbol.replace"用于String.prototype.replace调用
@@search"Symbol.search"用于String.prototype.search调用
@@species"Symbol.species"一个用来返回创建派生对象的构造函数的方法
@@split"Symbol.split"用于String.prototype.split调用
@@toPrimitive"Symbol.toPrimitive"用于ToPrimitive抽象方法
@@toStringTag"Symbol.toStringTag"用于描述一个对象的字符串,主要用于Object.prototype.toString调用
@@unscopables"Symbol.unscopables"用于with环境绑定中排除的属性名称

上面有些描述比较抽象,不要急,我们将逐个来仔细了解其规范定义和作用

Symbol.hasInstance(@@hasInstance)

作用

和上面描述的一样,用于确认对象是否为该构造函数实例的方法,主要用于instanceof,当调用instanceof时,内部方法会调用对象上的Symbol.hasInstance方法。

我们来看一个例子

class MyArray {
  static [Symbol.hasInstance](val){
    return val instanceof Array;
  }
}

[1,2,3] instanceof MyArray; // true

规范解读

在执行instanceof (V instanceof target) 操作时,Es6规范规定以下步骤:

  1. 判断target是否为对象,如果不是抛出TypeError exception.
  2. let instOfHandler = GetMethod(target, @@hasInstance). // GetMethod为内部的抽象方法,获取对象的指定方法
  3. 如果instOfHandler不等于undefined,返回调用target的@@hasInstance方法,并将结果返回Boolean值,算法结束。

    <font color="red">注意:这里会将结果值进行隐式转换</font>

  4. 判断对象是否IsCallable(可以看着是否是Function的实例), 如果不是抛出TypeError exception.
  5. 这里进入Es5中对instanceof的规范,Es6中称之为OrdinaryHasInstance。

紧接着我们看一下OrdinaryHasInstance是怎么规定的:

  1. 判断target是否IsCallable,如果是上面算法进来的,肯定是可以Callable的。
  2. 判断是否有[[BoundTargetFunction]]内部属性,如果有, let BC = target.[[BoundTargetFunction]],返回 V instanceof BC, 算法结束。

    <font color="red">注意: 这里的[[BoundTargetFunction]]其实是调用bind方法之前的原始方法</font>
    看下面的例子说明:

    function F1(){}
    const F2 = F1.bind({});
    const obj = new F2();
    obj instanceof F1 // true
    1. 判断V是否为Object,如果不是 返回false。
    2. let P = target.prototype;
    3. 判断P是否为Object,如果不是抛出TypeError exception;
    4. 循环判断
    let V = V.__proto__;
    if (V === null) {
        return false;
    }
    if(P === V){
        return true;
    }

默认值

Function.prototype[@@hasInstance] = function(V) {
    return OrdinaryHasInstance(this, V);
}

我们可以看到在es6规范中,先尝试获取对象上的@@hasInstance方法,如果有,先调用对象上的@@hasInstance方法并返回。

Symbol.isConcatSpreadable

作用

@@isConcatSpreadable用于在执行Array.prototype.concat时判断对象是否可展开。
我们先看两个例子

class MyArray {
  constructor(){
    this.length = 0;
  }
  push(val){
    this[this.length++] = val;
  }
  [Symbol.isConcatSpreadable] = true;
}
const array = new MyArray();
array.push(1);
array.push(2);
Array.prototype.concat.call(array, []); //[1,2] 这里自动展开array
[].concat(array); // [1,2] 这里自动展开array

class MyArrayNotConcatSpreadable {
  constructor(){
    this.length = 0;
  }
  push(val){
    this[this.length++] = val;
  }
}

const array2 = new MyArrayNotConcatSpreadable();
array2.push(1);
array2.push(2);
[].concat(array2); // [MyArrayNotConcatSpreadable对象] 这里不会自动展开array2

规范解读

@@isConcatSpreadable用于IsConcatSpreadable抽象方法,先看一下IsConcatSpreadable(O)规范定义:

  1. 判读O是否为对象,如果不是返回false.
  2. let spreadable = O[@@isConcatSpreadable].
  3. 如果spreadable不是undefined,将其转换为Boolean值并返回。
  4. return IsArray(O).

IsConcatSpreadable是抽象方法,不会暴露给javascript api,仅供内部调用,其用于Array.prototype.concat方法。

IsConcatSpreadable在Array.prototype.concat中会产生如下作用:

  1. 根据当前调用对象类型生成新的数组,length为0,
  2. 循环当前调用对象和传入的arguments列表
  3. 调用IsConcatSpreadable,判断当前是否可展开,如果可展开进行以下操作
  4. 取出当前值的length,循环k = 0 to length,将每一项设置到第一步生成的新数组中。

伪代码如下

const O = ToObject(this.value);
const A = ArraySpeciesCreate(O, 0);
let n = 0;
for(item of [O, ...arguments]){
    if(IsConcatSpreadable(item)){
        const length = item.length;
        let k = 0;
        while(k < length) {
            if(item.HasProperty(ToString(k))){
                Object.defineProperty(A, ToString(k), {
                    value: item[ToString(k)]
                });
            }
        }
    }
}

注意:上述伪代码只是展示了IsConcatSpreadable的使用,并不是全部的concat算法逻辑

Symbol.match

作用

@@match主要用于两个地方

  • 用于正则判断,抽象方法为IsRegExp(argument)
  • 用于String.prototype.match,自定义match逻辑

我们还是结合例子看:

const helloWorldStartMatcher = {
    toString(){
        return 'Hello';
    }
}

'Hello World'.startsWith(helloWorldStartMatcher);// true  
// startsWith在这里会调用helloWorldStartMatcher的toString方法进行判断

helloWorldStartMatcher[Symbol.match] = function(){
    return true;
}
'Hello World'.startsWith(helloWorldStartMatcher);// throw TypeError
// startsWith调用时会调用IsRegExp对helloWorldStartMatcher进行判断,因为定义了Symbol.match,所有返回true,startsWith会对正则抛出TypeError
const helloWorldMatcher = {
    [Symbol.match](val){
        return 'Hello World'.indexOf(val);
    }
}

'Hello'.match(helloWorldMatcher); // 0

helloWorldMatcher[Symbol.match] = function(){
    return /Hello/[Symbol.match](val);
};
'Hello World'.match(helloWorldMatcher); // 执行正则的match逻辑 等同于  'Hello World'.match(/Hello/);

规范解读

IsRegExp(argument)规范定义如下:

  1. 判断argument不是Object,return false。
  2. let matcher = argument[@@match]
  3. 如果matcher不是undefined, 将matcher转换为Boolean并返回.
  4. 如果argument有内置的[[RegExpMatcher]]属性, return true
  5. return false.

IsRegExp主要用于String.prototype.startsWith和String.prototype.endsWith,在这两个方法中会先通过IsRegExp对参数进行判断,如果是true,会抛出typeError异常。

@@match被String.prototype.match ( regexp )调用规则如下:

  1. 令O为当前对象的值。
  2. 如果regexp既不是undefined也不是null,let matcher = GetMethod(regexp, @@match)。
  3. 如果matcher不是undefined,返回regexp[@@match]](O)。

注意:上述描述只是展示了@@match在规范中的作用,并不是全部的String.prototype.match算法逻辑

Symbol.replace

作用

@@replace用于String.prototype.replace,自定义replace逻辑

例子

const upperCaseReplacer = {
    [Symbol.replace](target, replaceValue){
        return target.replace('hello', replaceValue.toUpperCase());
    }
}

'hello world'.replace(upperCaseReplacer, 'my');// MY world

规范解读

@@replace被String.prototype.replace ( searchValue, replaceValue )调用规则如下:

  1. 令O为当前对象的值。
  2. 如果searchValue既不是undefined也不是null,let replacer = GetMethod(searchValue, @@replace)。
  3. 如果replacer不是undefined,返回searchValue[@@replace]](O, replaceValue)。

注意:上述描述只是展示了@@replace在规范中的作用,并不是全部的String.prototype.replace算法逻辑

Symbol.search

作用

@@search用于String.prototype.search,自定义search逻辑

例子

const upperCaseSearcher = {
    value: '',
    [Symbol.search](target){
        return target.search(this.value.toUpperCase());
    }
}
upperCaseSearcher.value = 'world';
'hello WORLD'.search(upperCaseSearcher);// 6

规范解读

@@search被String.prototype.search (regexp)调用规则如下:

  1. 令O为当前对象的值。
  2. 如果regexp既不是undefined也不是null,let searcher = GetMethod(regexp, @@search)。
  3. 如果searcher不是undefined,返回regexp[@@search]](O)。

注意:上述描述只是展示了@@search在规范中的作用,并不是全部的String.prototype.search算法逻辑

Symbol.split

作用

@@split用于String.prototype.split,自定义split逻辑

例子

const upperCaseSplitter = {
    value: '',
    [Symbol.split](target, limit){
        return target.split(this.value.toUpperCase(), limit);
    }
}
upperCaseSplitter.value = 'world';
'hello WORLD !'.split(upperCaseSplitter);// ["hello ", " !"]
'hello WORLD !'.split(upperCaseSplitter, 1);// ["hello "]

规范解读

@@split被String.prototype.split ( separator, limit )调用规则如下:

  1. 令O为当前对象的值。
  2. 如果separator既不是undefined也不是null,let splitter = GetMethod(separator, @@split)。
  3. 如果splitter不是undefined,返回regexp[@@split]](O, limit)。

注意:上述描述只是展示了@@split在规范中的作用,并不是全部的String.prototype.split算法逻辑

Symbol.toStringTag

作用

@@toStringTag通过Object.prototype.toString来调用的,用于描述对象。

例子

const obj = {
    [Symbol.toStringTag]: 'Hello'
}

Object.prototype.toString.call(obj); // "[object Hello]"

class ValidatorClass {}

Object.prototype.toString.call(new ValidatorClass()); // "[object Object]" 默认值

class ValidatorClass {
  get [Symbol.toStringTag]() {
    return "Validator";
  }
}

Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"

class ValidatorClass {
  get [Symbol.toStringTag]() {
    return {};
  }
}

Object.prototype.toString.call(new ValidatorClass()); // "[object Object]"

规范解读

@@toStringTag被Object.prototype.toString调用规则如下:

  1. 令O为当前对象的值。
  2. 先判断null和undefined,满足条件返回[object Null]和[object Undefined]
  3. 依次判断Array, String, Arguments, Function, Error, Boolean, Number, Date, RegExp, Object,将对应的类型字段赋值给builtinTag变量
  4. let tag = O[@@toStringTag];
  5. 判断tag,如果不是字符串,将builtinTag赋值给tag
  6. 返回"[object ",tag,and"]".

默认值

Es6新增的@@toStringTag如下:

对象
AtomicsAtomics
MathMath
JSONJSON
Symbol.prototypeSymbol
Map.prototypeMap
Set.prototypeSet
WeakMap.prototypeWeakMap
WeakSet.prototypeWeakSet
Promise.prototypePromise
ArrayBuffer.prototypeArrayBuffer
Module Namespace ObjectsModule
SharedArrayBuffer.prototypeSharedArrayBuffer
DataView.prototypeDataView
GeneratorFunction.prototypeGeneratorFunction
AsyncGeneratorFunction.prototypeAsyncGeneratorFunction
Generator.prototypeGenerator
AsyncGenerator.prototypeAsyncGenerator
AsyncFunction.prototypeAsyncFunction
%StringIteratorPrototype%String Iterator
%ArrayIteratorPrototype%Array Iterator
%MapIteratorPrototype%Map Iterator (new Map()[Symbol.iterator]())
%SetIteratorPrototype%Set Iterator
%AsyncFromSyncIteratorPrototype%Async-from-Sync Iterator

Symbol.toPrimitive

作用

@@toPrimitive被ToPrimitive抽象方法调用,主要作用于类型转换。
我们还是结合例子来看:

const obj = {
    [Symbol.toPrimitive](hint){
        if(hint === 'number') {
            return 2;
        }
        return '1';
    }
}
const keyObj = {
    '1': 1
};
console.log(1 - obj);// -1  调用ToNumber类型转换
console.log(1 == obj); // true 抽象相等算法时调用
console.log(obj + 1); // 11 +号操作符时调用
console.log(keyObj[obj]); // 调用ToPropertyKey进行转换
console.log(0 < obj); // 抽象比较算法时调用

obj[Symbol.toPrimitive] = function(){return '2017-05-31'};
console.log(new Date(obj)); // Date构造时调用

obj[Symbol.toPrimitive] = function(){return {}};
console.log(obj + 1);// throw type error

规范解读

由于ToPrimitive抽象方法是Es6底层最主要的抽象方法之一,调用点比较多,我们先注重看一下它的实现。

ToPrimitive ( input [ , PreferredType ] )被定义为如下:

  1. 判断当前input是否为obj,如果不是,直接返回input
  2. 根据PreferredType设置类型转换标识并赋值为hint变量,默认为default
  3. 如果PreferredType是Number,hint赋值为number,PreferredType是String,hint赋值为string。
  4. let exoticToPrim = GetMethod(input, @@toPrimitive),如果exoticToPrim不是undefined进行如下操作

    1. 调用input[@@toPrimitive](hint)并赋值给result
    2. 如果result不是Object直接返回result,否则抛出type Error异常
  5. 如果hint为default,则赋值为number
  6. 调用OrdinaryToPrimitive( input, hint )

OrdinaryToPrimitive为Es5规范定义的ToPrimitive方法,这里顺带介绍一下:

  1. 先判断hint是否为string或number,如果都不是则抛出TypeError异常
  2. 如果hint是string,则尝试先调用toString,然后调用valueOf
  3. 否则先尝试调用valueOf,然后调用toString。
  4. 以上两个方法如果都没有,或者调用返回结果都为Object,则抛出TypeError异常

其次我们看一下ToPrimitive调用点:

  • ToNumber(input) 如果input是Object时,尝试调用ToPrimitive(input, 'number')
  • ToString(input) 如果input是Object时,尝试调用ToPrimitive(input, 'string')
  • ToPropertyKey(input) 尝试调用ToPrimitive(input, 'string')
  • 抽象比较时(例如:a < b),先尝试调用ToPrimitive(input, 'number')
  • 抽象相等操作是(==),如果两边分别是Number和String类型或者其中一方为Boolean类型就会引起ToNumber调用,否则如果一方是String, Number,或者 Symbol类型而另一方是Object类型,就会引起ToPrimitive(Object类型一方的值)
  • 二元+号操作符会触发ToPrimitive, ToString,ToNumber动作
  • Date构造时,对于非DateValue类型的参数会触发ToPrimitive
  • Date.prototype.toJSON 会触发ToPrimitive(thisValue, 'number')
  • 其他但不限于调用ToNumber的操作,例如:++,--,+,-等数字操作符,设置数组的length,排序,Math.max(min), Number(value), isNaN等。
  • 调用ToString的操作设计es规范的方方面面,这里不一一赘述。

Symbol.species

作用

在es规范中,很多的方法都需要获取当前调用者的构造函数,然后根据此构造函数构造对象,可能这样说比较抽象,我们还是先看例子吧。

class MyArray extends Array{

}

const array = new MyArray();
array.push(1);
array.push(2);
console.log(array instanceof Array); // true
console.log(array instanceof MyArray); // true

const mapArray = array.map(item => item);
console.log(mapArray instanceof Array); // true
console.log(mapArray instanceof MyArray); // true

从上面的例子中我们看到,map后的数组还是通过MyArray构造的,有时我们希望创建衍生对象时使用我们指定的构造器。

class MyArray extends Array{
    static [Symbol.species] = Array;
    // 等同于上面效果
    //static get [Symbol.species](){
    //    return Array;
    //}
}

const array = new MyArray();
array.push(1);
array.push(2);
console.log(array instanceof Array); // true
console.log(array instanceof MyArray); // true

const mapArray = array.map(item => item);
console.log(mapArray instanceof Array); // true
console.log(mapArray instanceof MyArray); // false

规范解读

在es6规范中,Symbol.species扩展属性主要作用于两个抽象动作中,分别是SpeciesConstructor,ArraySpeciesCreate,我们先来看看这两个抽象动作具体是如何执行的。

SpeciesConstructor ( O, defaultConstructor )定义如下:
其中O是当前的调用者,如果O中不存在@@species属性就以defaultConstructor为默认构造器

  1. let C = O.constructor。
  2. 如果C是undefined,返回defaultConstructor。
  3. 如果C不是对象,抛出TypeError
  4. let S = O[@@species]
  5. 如果S为null或者undefined,返回defaultConstructor。
  6. 调用IsConstructor(S),判断S是否为构造器,如果是返回S.
  7. 抛出TypeError

ArraySpeciesCreate ( originalArray, length )定义如下:
其中originalArray是当前的调用数组

  1. let isArray = IsArray(originalArray)。
  2. 如果isArray为false, return new Array(length)。
  3. let C = originalArray.constructor
  4. 如果C是构造器
    判断C和当前的全局环境对应Array构造器是否相同,如果不相同将C置为 undefined (防止跨window创建对象)
  5. 如果C是Object

    1. C = C[@@species]
    2. 如果C为null,重置为undefined
  6. 如果C是undefined,return new Array(length)。
  7. 如果C不是构造器, 抛出TypeError.
  8. 基于C创建数组,长度为length。

注:上述是规范的简化过程,去除了一些断言和判断

我们看一下SpeciesConstructor调用点:

  • 调用正则原型上的[Symbol.split]方法(调用字符串的split方法时会调用传入正则的[Symbol.split]方法)
  • 创建TypedArray时触发(其中还包括TypedArray的slice,subarray,map方法)
  • [Shared]ArrayBuffer.prototype.slice 被调用时触发
  • Promise.prototype.then或finally时被触发

例如

class MyPromise extends Promise {
}

const thenMyPromise = MyPromise.resolve().then();

console.log(thenMyPromise instanceof MyPromise); // true
console.log(thenMyPromise instanceof Promise); // true

class MyPromise2 extends Promise {
  static get [Symbol.species]() {
    return Promise;
  }
}

const thenMyPromise2 = MyPromise2.resolve().then();

console.log(thenMyPromise2 instanceof MyPromise); // false
console.log(thenMyPromise2 instanceof Promise); // true

ArraySpeciesCreate调用点:
主要用于Array原型上的方法时调用触发,包括concat, filter, flat,map,slice,splice方法

默认值

es6规范中定义的javascript原始类型的@@species默认值为 Return the this value.

Symbol.iterator

作用

这个可能是自定义时使用的最多的,它可以帮助我们自定义迭代器,而且ECMAScript规范中的Set,Map等迭代过程都是基于它实现的。

在Typescript的Es6签名库,我们可以看到迭代器的签名如下:

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface Iterator<T, TReturn = any, TNext = undefined> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

通过签名我们可以看到实现自定义迭代器需要扩展[Symbol.iterator]方法,而该方法要返回一个Iterator,Iterator中的next方法接受一个值,返回IteratorResult。其中的return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。
throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获,主要是配合Generator使用。

我们先看两个例子感受一下。

function *iterable () {
  yield 1;
  yield 2;
  yield 3;
};
// iterable()返回一个迭代器
for(const val of iterable()){
    console.log(val);
    // 输出1,2,3
}

class EvenArray extends Array {
    [Symbol.iterator](){
        const _this = this;
        let index = 0;
        return {
            next(){
                if(index < _this.length){
                    const value = _this[index];
                    index += 2;
                    return {
                        done: false,
                        value,
                    }
                }
                return {
                    done: true
                };
            },
            return() {
                this._index = 0;
                console.log('return iterator');
                return {
                    done: true
                }
            }
        }
    }
}

const array = new EvenArray();
for(let i = 0; i <= 100; i++){
    array.push(i);
}

for(const val of array){
    console.log(val); // 0, 2, 4, 6, ... , 98, 100
}

for(const val of array){
    console.log(val); // 0
    // return iterator    调用了return 方法
    break;
}

for(const val of array){
    console.log(val); // 0
    // return iterator    调用了return 方法
    throw new Error();
}

// //等同于上面代码
// class EvenArray extends Array {
//     constructor(){
//         super();
//         this.index = 0;
//     }
//     [Symbol.iterator](){
//         this.index = 0;
//         return this;
//     }

//     next(){
//         if(this.index < this.length){
//             const value = this[this.index];
//             this.index += 2;
//             return {
//                 done: false,
//                 value,
//             }
//         }
//         return {
//             done: true
//         };
//     }
// }

const myIterable = {}
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};

// 扩展默认调用迭代器
console.log([...myIterable]); // [1, 2, 3]

function *iterable2 () {
  yield* myIterable; //悬停myIterable迭代器
};

for(const val of iterable2()){
    console.log(val); // 1,2,3
}

function consoleArgs(...args){
    console.log(args);
}

consoleArgs(...myIterable);// 剩余参数调用默认调用迭代器

规范解读

先梳理一下@@iterator的调用点:

  • 调用抽象方法GetIterator ( obj [ , hint [ , method ] ] )时,其中hint取值为async或sync,默认为sync。method为指定的返回迭代器的方法
  • 调用抽象方法CreateUnmappedArgumentsObject和CreateMappedArgumentsObject时(这里主要是处理arguments时调用)
  • 调用Array.from时
  • 调用%TypedArray%.from 时

我们一个个来剖析里面的具体实现及其作用

  1. GetIterator ( obj [ , hint [ , method ] ] )定义如下

    1. 如果hint为undefined,则重置为sync
    2. 如果method没有提供,进行如下操作

      1. 如果hint为async

        1. method = GetMethod(obj, @@asyncIterator).
        2. 如果method为undefined,则

          1. let syncMethod = GetMethod(obj, @@iterator)
          2. let syncIteratorRecord = GetIterator(obj, sync, syncMethod)
          3. return CreateAsyncFromSyncIterator(syncIteratorRecord)。//CreateAsyncFromSyncIterator为抽象方法,用于通过Iterator创建异步迭代器。
      2. method = GetMethod(obj, @@iterator)
    3. let iterator = obj.method();
    4. 判断如果iterator不是Object,抛出TypeError
    5. let nextMethod = iterator.next;
    6. let iteratorRecord = { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }
    7. return iteratorRecord;

通过上述算法我们可以看到,GetIterator最终返回一个包装好的迭代器对象。那么都有那些地方调用GetIterator抽象方法呢?

  • 扩展数组时,let array = [1, 2, ...array2];
  • 解构数组时,let [one] = array;
  • rest参数处理时,function gen(...args){}; gen(...array);
  • 参数解构绑定时,function gen([one]){}; gen(array);
  • yield 调用时, function gen() { yield* array };
  • Array.from调用时, Array.from(array)。
  • new Set,new Map调用时(其中包括WeakSet和WeakMap),new Set(array)。
  • Promise.all|race调用时,Promise.all(array)。
  • for of调用时。

由于迭代器涉及的调用点比较多,可能需要单独的一篇文档介绍,这里注重看一下for of的规范:

for of执行主要包含两个部分:

  1. 调用ForIn/OfHeadEvaluation抽象方法,返回迭代器
  2. 调用ForIn/OfBodyEvaluation执行迭代器

接下来看一下ForIn/OfHeadEvaluation(TDZnames, expr, iterationKind )的规范定义:
说明:该抽象方法有三个参数,分别表示:绑定的环境变量名称、of后面的语句、迭代的类型(包括enumerate、async-iterate、iterate)。具体含义及其作用我们接着往下看。

  1. 设置oldEnv为当前执行环境
  2. 如果TDZnames不为空,执行如下操作

    1. TDZ 为使用oldEnv创建的新的声明式环境
    2. TDZEnvRec 设置为TDZ的环境记录项
    3. 将TDZnames绑定到TDZEnvRec上
    4. 将当前执行上下文的词法环境设置为TDZ
  3. 设置exprValue为expr执行后的值
  4. 判断iterationKind是否为enumerate,如果是(这里主要用于for in)

    1. 如果exprValue为null或者undefined,return Completion{ [[Type]]: break, [[Value]]: empty, [[Target]]: empty } (这是es规范中的一种类型,用来控制break, continue, return 和 throw, 在这里可以看作跳出循环)
    2. let obj = ToObject(exprValue)
    3. return EnumerateObjectProperties(obj) // EnumerateObjectProperties用于循环对象,返回对象迭代器,这里不展开讨论
  5. 否则

    1. 判断iterationKind是否为async-iterate,如果是设置变量iteratorHint为async
    2. 否则 iteratorHint 为sync
    3. 调用GetIterator(exprValue, iteratorHint)获取迭代器并返回

上述方法返回的结果会传入到ForIn/OfBodyEvaluation进行变量执行
ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind, labelSet [ , iteratorKind ])规范定义如下:

参数比较多,我们一个一个解释:

  • lhs:of前面的声明语句
  • stmt:for of循环体
  • iteratorRecord:上文中返回的迭代器
  • iterationKind:迭代的类型(同上文)
  • lhsKind:变量绑定类型(assignment, varBinding 或者 lexicalBinding)
  • labelSet:控制语句(例如return, break, continue)
  • iteratorKind: 迭代器类型(用于标识异步迭代器async)

算法执行逻辑如下:

  1. 如果iteratorKind为空,设置为sync
  2. 用oldEnv变量表示当前执行上下文的词法环境
  3. 声明一个V变量,设为undefined
  4. 如果lhs是解构语句,对解构语句进行处理
  5. 开始进入循环

    1. let nextResult = iteratorRecord.[[Iterator]][iteratorRecord.[[NextMethod]]]();
    2. 如果iteratorKind为async,nextResult = Await(nextResult)(异步迭代器,使用await悬停)
    3. 通过IteratorComplete(nextResult)判断是否迭代完成(这里其实就是判断的done是否为true)
    4. 如果done为true,return NormalCompletion(V) (这里和上文中的Completion作用相似,可以看作是跳出循环)
    5. let nextValue = IteratorValue(nextResult) (获取迭代器执行返回的value)
    6. 这里主要是根据lhsKind解析lhs获取对应的变量绑定引用(规范描述的太详细,我们这里先了解其作用)
    7. 上面绑定变量时会返回status用于描述执行后的状态,如果status不是NormalCompletion(例如出现异常),则判断iterationKind,如果iterationKind是enumerate直接返回status,否则返回iteratorRecord.[[Iterator]][iteratorRecord.[[ReturnMethod]]]()
    8. 设置result为执行stmt的结果(result也是一个Completion)
    9. 判断result结果是否可继续循环(例如break, return等语句会跳出循环),如果不可以,则判断iterationKind,如果iterationKind是enumerate直接返回status,否则返回iteratorRecord.[[Iterator]][[iteratorRecord[[ReturnMethod]]]()
    10. 如果result.[[Value]]不为空,则 V = result.[[Value]]

上述算法去除了规范里的一些繁琐的步骤,尤其是lhs解析绑定的部分,如果想要深入了解,建议查看ECMAScript规范文档。

默认值

Es6内置的多数对象都实现来迭代器,具体如下:

  • String.prototype [ @@iterator ]
  • Array.prototype [ @@iterator ]
  • %TypedArray%.prototype [ @@iterator ]
  • Map.prototype [ @@iterator ]
  • Set.prototype [ @@iterator ]
  • %IteratorPrototype% [ @@iterator ]

Symbol.asyncIterator(@@asyncIterator)

作用

Symbol.asyncIterator指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。

接下来我们看几个例子:

  • 例子1:
const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
    yield 1;
    yield 2;
    yield 3;
};

(async () => {
    for await (const x of myAsyncIterable) {
        console.log(x);
        // 输出:
        //    1
        //    2
        //    3
    }
})();

当然也可以通过它遍历promise

  • 例子2:
const myAsyncIterable = new Object();
const promise1 = new Promise(resolve=>setTimeout(() => resolve(1), 500));
const promise2 = Promise.resolve(2);
myAsyncIterable[Symbol.asyncIterator] = async function*() {
    yield await promise1;
    yield await promise2;
};

(async () => {
    for await (const x of myAsyncIterable) {
        console.log(x);
        // 输出:
        //    1
        //    2
    }
})();

也可以自定义异步迭代器

  • 例子3:
const myAsyncIterable = {
    promiseList:[
        new Promise(resolve=>setTimeout(() => resolve(1), 500)),
        Promise.resolve(2)
    ],
    [Symbol.asyncIterator](){
        const _this = this;
        let index = 0;
        return {
            next(){
                if(index === _this.promiseList.length){
                    return Promise.resolve({done: true});
                }
                return _this.promiseList[index++].then(value => ({done: false, value}))
            }
        }
    }
};

(async () => {
    for await (const x of myAsyncIterable) {
        console.log(x);
        // 输出:
        //    1
        //    2
    }
})();

规范解读

@@asyncIterator作用和@@iterator,在规范定义中也是统一处理的,只是在执行ForIn/OfBodyEvaluation时iteratorKind参数设置为了async,执行函数时通过Await动作处理@@asyncIterator。

Symbol.unscopables(@@unscopables)

作用

对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除

const object1 = {
  property1: 42
};

object1[Symbol.unscopables] = {
  property1: true
};

with (object1) {
  console.log(property1);
  // expected output: Error: property1 is not defined
}

规范解读

@@unscopables用于HasBinding调用

HasBinding查看对象是否绑定到当前的环境记录项中,规范中的HasBinding最后会通过@@unscopables进行过滤。

默认值

规范中只有Array.prototype指定了@@unscopables
具体如下:

{
    "copyWithin":true,
    "entries":true,
    "fill":true,
    "find":true,
    "findIndex":true,
    "flat":true,
    "flatMap":true,
    "includes":true,
    "keys":true,
    "values":true
}

望峰
4 声望0 粉丝