for in、for of以及generator

285 阅读6分钟

在写Symbol类型的时候碰到了[Symbol.iterator]的属性,在了解[Symbol.iterator]的过程中,发现对迭代器,for in,for of和generator的概念了解也不甚清晰,于是单开一篇学习这个问题。 提前思考的问题:

  1. for...in和for...of有什么不同?
  2. 什么是generator生成器?

Iterator

js中表示集合的数据类型:

  1. Array
  2. Object
  3. Set(es6新增)
  4. Map(es6新增)

what
Iterator是一种接口,为不同的数据结构提供统一的访问机制,任何数据结构只要有Iterator,就可以依次访问该数据结构内所有的成员。

why
将遍历方法抽象出来,避免暴露数据结构内部,有的需要for(i<index),有的如同链表需要while(e.next!==null)。降低遍历的方法和数据结构本身的耦合程度。 Iterator本身也是一种设计模式,称为迭代器模式:不关心被迭代对象的内部构造,仅提供一种方法顺序访问聚合对象中的各个元素。
可以引申为对于想实现相同目的不同的方法,都可以依据优先级放入迭代器中,迭代器从头开始访问每个方法,直到遇到return不为false的为止,即将传统迭代中作为指针的i变成具体的方法。

how
实现一个js的Iterator:

  1. 创建一个指针对象,指向起始位置,它具有next()方法
  2. 不断调用指针对象的next()方法,指针指向数据结构的第n个成员。
  3. 指针指向了数据结构的末尾,迭代结束 具体实现牢记三个对象:可迭代对象,迭代器对象,迭代结果对象
    (可迭代对象)通过Iterator(迭代器对象)返回一个对象(迭代结果对象),对象包含next()方法,next()方法返回一个包含value和done的值的对象。
function makeIterator(arr) {
    let index = 0;
    return {
      next: function() {
        return index < arr.length ? {
          value: arr[index ++],
        } : {
          done: true,
        }
      }
    };
}

for...of

既然已经有了默认的Iterator方法,提供迭代器对象,那自然也应该有一个方法去调用这个迭代器对象。
for...of是es6提供的一种迭代器消费方式,它会去自动寻找Iterator接口,也就是数据结构的[Symbol.iterator]属性。有些数据结构原型就具有[Symbol.iterator]属性,有些没有。比如Object没有[Symbol.Iterator],使用for..of会报错TypeError; for..of在不同数据类型下的运行结果:

// 数组类型,for of遍历访问数组的值
const arrayTest = [
    1, 2, 'abc', null, undefined, 5,
    { name: 'jack', age: 20 },
    { name: 'tom', age: 18 },
    [1, 2, 3],
];
for (let result of arrayTest) {
    console.log(result);
}

image.png

// 字符串类型,输出每一个字符
const strTest = 'afgeggweg';

for (let result of strTest) {
    console.log(result);
}

image.png

// Map类型
const mapTest = new Map();
mapTest.set("today", "tuesday");
mapTest.set("tomorrow", "friday");
for (let result of mapTest) {
    console.log(result);
}

image.png

但是,当我们想对对象使用for...of时,就报错了。

const obj = {
    a: '1',
    b: '2',
};
for (let result of obj) {
    console.log(result);
}

image.png

如果想要让对象可以使用for...of,需要给它手动定义一个[Symbol.iterator]

const obj = {
    m: 'm',
    1: 2,
    a: 'a',
    d: [1, 2],
    [Symbol.iterator]() {
        let index = 0;
        let self = this;
        return {
            next() {
                let keys = Object.keys(self);
                if (index >= keys.length) {
                    return {
                        value: undefined,
                        done: true,
                    }
                } else {
                    return {
                        value: self[keys[index ++]],
                        done: false,
                    }
                }
            }
        }
    }
};

for (let val of obj) {
    console.log(val);
}

除此之外,for...of还有一些特性:

  1. 惰性加载,等到实际需要下一个值时才发生计算
  2. 可以通过return()在done还不为true时,跳出循环

generator生成器

个人平时开发中并没有较熟练地使用生成器,所以这里也想好好梳理一下生成器以及yield关键字的理解和使用。

每次调用iterator的next()会返回按照迭代规则,下一个应该输出的内容。但是当被迭代的不是数据结构,而是某些复杂的运算,可以使用生成器。想使用生成器,就在function和()之间加一个*

  1. 当有一个生成器函数function* test() { ... },调用它并不会执行这个函数,而是会返回一个生成器对象,这个生成器对象是一个迭代器,拥有next()方法,调用next()方法,它会开始执行直到遇到一个yield或return
  2. 在使用next()时可以往里添加参数,它会作为上一个yield表达式的值,如果不传相当于传递了一个undefined
  3. next()返回的结果是一个迭代结果对象{ value: .., done: .. }
  4. 当试图在一个generator中再使用另一个generator时,需要用yield* 来定义它,相当于for...of,因为yield* 可以用来迭代任何可迭代对象
  5. yield和yield* 只能在生成器函数中使用
function* A() {
    yield 'a1';
    yield 'a2';
    yield 'a3';
}

function* B() {
    yield 'b1';
    yield 'b2';
    yield A();
    yield 'b3';
}

let a = B();

// 输出结果,当使用的是yield时,无法迭代A(),返回一个generator对象
console.log(a.next()); // { value: 'b1', done: false }
console.log(a.next()); // { value: 'b2', done: false }
console.log(a.next()); // { value: Object [Generator] {}, done: false }
console.log(a.next()); // { value: 'b3', done: false }

function* C() {
    yield 'c1';
    yield* A();
    yield 'c2';
}

let a = C();

// 输出结果,使用的是yield*,相当于迭代了A
console.log(a.next()); // { value: 'c1', done: false }
console.log(a.next()); // { value: 'a1', done: false }
console.log(a.next()); // { value: 'a2', done: false }
console.log(a.next()); // { value: 'a3', done: false }
console.log(a.next()); // { value: 'c2', done: false }

yield意为回传,当调用next时,生成器会运行到yield表达式,yield关键字后面的表达式会被求值,作为next()调用的返回值(yield将结果传给调用者),而调用者通过next()可以向生成器传值(next将入参传给生成器),可以形成交替控制

生成器的return()和throw()

在生成器上调用内置的return和throw方法,会改变生成器的控制流,就像生成器的下一条语句是return和throw一样
通过在生成器函数中定义try{...}finally{...},可以使用return来保证使用finally中的语句,且生成器再也不会被使用。
而使用throw(),也可以像next一样传给生成器一个错误信息,告诉它应该通过这个异常来改变控制流。
生成器是非常强大的控制结构,可以配合新增的async和await关键字使用(之后讨论),将虽然是单线程的javascript实现异步管理,代码看起来依旧像是同步的。

for...in

最后讨论一下开头的for..of(es6)和for...in(es5)问题,我们已经知道了for...of其实是使用被迭代对象的[Symbol.iterator]迭代器对象,那for...in是做什么的呢?如何使用呢?为什么会被使用呢?

for...in是用来遍历数字或者对象的属性,得到和了for...of的第一个区别:获取被遍历的属性,而非值,且可以对对象使用

// 简单粗暴的实验
const test = [
    1,2,3,4
];

for (let value of test) {
    console.log(value); // 1,2,3,4 得到了数组的值
}

for (let result in test) {
    console.log(result); // 0,1,2,3 输出的是对象的下标
}

// 如果被遍历对象是字符串,会输出每个字符在字符串中的位置

第二个区别:for...in可以获取被遍历对象中的自定义属性

const test = [1, 2, 3];

test.name = 'jack';

for (let value of test) {
    console.log(value);  // 1 2 3
}

for (let result in test) {
    console.log(result); // 只有for...in才会输出jack
}

第三个区别:for...in返回的索引都是字符串类型,而for...of输出的结果是原类型

const test = [1, 2, 'name', true];

test.name = 'jack';

for (let value of test) {
    console.log(value, typeof value);
}

for (let result in test) {
    console.log(result, typeof result);
}

image.png

PS: 所以不要用for...in遍历数组,得到的下标也不能直接计算
第四个区别:for...in会跳过空值的属性(空值不是undefined和null)

const test = [1, 2, 'name',, true]; // 有一个空值,如果为undefined和null就不是空值

for (let value of test) {
    console.log(value, typeof value);
}

for (let result in test) {
    console.log(result, typeof result);
}

image.png

建议使用for...in遍历对象,for...in和for...of都可以打断,但forEach不可以打断