十万个为什么?--JS中的迭代器(JavaScript高级程序设计第四版)

168 阅读9分钟

注:这些问题和答案都是一些自己的思考和从书上的总结,可能并不完全是正确的,欢迎大家指正!

问题1:在什么时候会用迭代呢?

  • 在一个有序集合上会用到迭代器。有序是指:集合中所有项都可以按照既定的顺序被遍历,比如数组。 问题2:为什么需要迭代器呢?用for、while循环不行吗?
  • 如果使用循环来迭代,迭代之前需要知道如何使用数据结构,如何获取到下一项,不同的数据结构,可能获取下一项的方式不同,比如:数组是通过索引来获取下一项的,而链表是通过指针来获取下一项的。因此,面对不同的数据结构,用户就需要重写一个遍历方法,不符合开闭原则(对扩展开放,对修改封闭)。但如果使用的迭代器,使用同一种逻辑来遍历,用户不需要根据数据结构的不同来修改获取下一项的代码。比如:迭代器中都是统一使用next方法来获取下一项的,无论什么数据结构,只需要调用next方法就行。
  • 使用迭代器可以在不了解内部数据结构的情况下直接遍历,这样使得集合内部的数据不暴露。 问题3:为什么Array.prototype.forEach()方法不能实现通用迭代需求?
  • 该方法虽然解决了单独记录索引和通过数组对象获取值的问题,但是,没有办法标识迭代何时终止。因此这个方法只适用于数组,不适用于通用迭代。 问题4:什么是迭代器模式?
  • 实现Iterable接口的数据结构(被称为可迭代对象),都可以被实现Iterator接口的迭代器消费。迭代器(Iterator)是按需创建的一次性对象。每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代器关联可迭代对象的API。(下面还会详细地介绍) 问题5:为什么要将Iterable和Iterator分离呢?
  • Iterable指的是可迭代对象,而Iterator指的是迭代器。每个迭代器都会关联一个可迭代对象,而迭代器无须了解其关联的可迭代对象的结构,只需要知道如何取得连续的值。这也就是可迭代对象与迭代器分离的好处。 问题6:如何实现Iterable接口?(例子可见下面的自定义迭代器)
  • 暴露一个属性作为默认迭代器,而且这个属性必须使用特殊的Symbol.iterator作为键。
  • 这个迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新的迭代器对象。 问题7:JavaScript中哪些内置类型实现了Iterable接口?
  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments对象(这是个类数组对象,非箭头函数才有,箭头函数中没有,箭头函数也没有this、super、new target,其中new target属性用于检测函数是不是通过new来调用的)
  • NodeList等DOM集合类型 问题8:哪些原生语言特性可以接收可迭代对象?
  • for-of循环
  • 数组解构:let[a,b,c] = arr
  • 扩展操作符: (最后对扩展操作符有个总结)
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收由期约组成的可迭代对象
  • Promise.race()接收由期约组成的可迭代对象
  • yield* 操作符,在生成器中使用 这些语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器。注意⚠️:如果对象原型链上的父类实现了Iterable接口,那么这个对象也就实现了这个接口。 问题9:什么是迭代器?
  • 迭代器是一种创建一个后,这个只能一次性使用的对象,他可以用于迭代与其关联的可迭代对象。迭代器对象中有next()方法,可以用于遍历数据。每次调用next()方法,都会返回一个IteratorResult对象。
  • IteratorResult对象包含两个属性值,:done和value。done是一个boolean值,表示是否还可以再次调用next()取得下一个值(done为true,表示遍历完了,不能再调用next()了);value包含可迭代对象的下一个值(done为false时才有值,done为true时value为undefined)
let arr = [1,2,3];
//获取迭代器对象
let iter = arr[Symbol.iterator]();
console.log(iter.next());//{ "value": 1, "done": false }
console.log(iter.next());//{ "value": 2, "done": false }
console.log(iter.next());//{ "value": 3, "done": false }
console.log(iter.next());//{ "done": true }
  • 注意,每个迭代器都表示可迭代对象的一次有序遍历。不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象。
  • 并且,迭代器并不是与可迭代对象某个时刻的快照绑定,而仅仅是用游标来记录遍历可迭代对象的过程。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。(注意:尽量不要再for-of等用迭代器迭代时,再对数组进行一些添加或删除的操作,不然会产生一些不可预料的问题)比如:
            let arr = [1]
            let iter = arr[Symbol.iterator]()
            console.log(iter.next())//{value: 1, done: false},游标在index=0
            arr.push(2)
            arr.push(3)//此时数组为:[1,2,3]
            console.log(iter.next())//{value: 2, done: false},游标在index=1
            arr.shift()//删除第一个元素后,数组为[2,3]
            //此时再获取下一个元素,就没有了。因此获取不到3这个元素
            console.log(iter.next())//{value: undefined ,done: true}

问题10:迭代器会阻止对可迭代对象的垃圾回收嘛?

  • 会,迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。比如从侧面验证一下,迭代器维护着一个指向可迭代对象的引用:
            let arr = [1,2,3]
            let iter = arr[Symbol.iterator]()
            arr = null
            /**
             * 即使我们将arr设置为了null,明面上没有人引用[1,2,3]这个数组。
             * 但实际上,iter迭代器引用了这个数组,如,我们在调用next方法时依然可以获取数组的数据
            */
            console.log(iter.next())//{value: 1, done: false}

问题11:如何写一个自定义的迭代器?

    1. 对象实现Iterable接口,也就是设置一个方法,方法名为[Symbol.iterator]
    1. [Symbol.iterator]方法是构造器的工厂函数,调用该方法返回的是一个构造器对象。而构造器对象实现了Iterator接口,也就是这个构造器对象实现next()方法(当然也可以实现return方法,来实现提前终止迭代器的功能)。所以,自定义迭代器的框架如下:
            //定义一个可迭代对象
            class MyIterable{
                constructor(){}
                //实现Iterable接口,作为迭代器对象的工厂函数,返回一个迭代器对象
                [Symbol.iterator](){
                    return new MyIterator();
                }
            }
            //定义一个迭代器对象
            class MyIterator{
                constructor(){}
                //实现Iterator接口
                next(){
                    //next函数的具体实现
                }
            }

问题12:详细版本的自定义迭代器,以及一些小的注意点

    1. 可迭代对象可以创建多个迭代器,而且每个迭代器互不影响。因此,在每个迭代器对象中都维护着一个自己的游标
    1. 如何设置终止迭代器:在迭代器对象中除了next方法以外,再写一个return(){}方法,该方法返回的也是IteratorResult对象,只不过done为true,value为undefined.这个return方法在关闭迭代器的时候会被调用,但是注意:正常迭代完时,不会调用return方法
    1. 提前终止迭代器
    • for-of循环中,通过break、continue、return或throw提前退出
    • 解构操作并为消费所有的值,比如:let [a,b] = counter;(counter是有5个值的可迭代对象,但是这里解构操作只消费了两个值)
    • 如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代,比如数组的迭代器就是不能关闭的。
    • 并非所有的迭代器都是可关闭的,如果想要看迭代器是否可以关闭,可以看看这个迭代器实例的return属性是不是函数对象。但是,仅仅给不可关闭的迭代器增加这个return方法,不能把它变为可关闭的。这是因为调用return()方法不会强制迭代器进入关闭状态。即便如此,return()方法还是会被调用。
let a = [1,2,3];
let iter = a[Symbol.iterator]();
iter.return = function(){
    console.log("Exiting early");
    return {done: true};
}
//测试
for(let item of iter){
    console.log(item)
    break;
}
for(let item of iter){
    console.log(item)
}
//输出情况为: 1,Exiting early, 2,3;
//因此可以看到,执行了return方法后,再次对迭代器对象迭代时,还可以输出2,3。所以证明了并没有关闭迭代器对象。
//完整版自定义迭代器
 //定义一个可迭代对象
            class MyIterable{
                constructor(limit){
                    this.limit = limit;
                }
                //实现Iterable接口,作为迭代器对象的工厂函数,返回一个迭代器对象
                [Symbol.iterator](){
                    return new MyIterator(this.limit);
                }
            }
            //定义一个迭代器对象
            class MyIterator{
                constructor(limit){
                    this.limit = limit
                    this.count = 1;
                }
                //实现Iterator接口
                next(){
                    //next函数的具体实现
                    if(this.count<=this.limit){
                        return {done:false, value: this.count++}
                    }else{
                        return {done:true, value: undefined}
                    }
                }
                //提前终止的方法
                return(){
                    console.log("提前终止")
                    return{done:true, value:undefined}
                }
            }

问题14:对扩展操作符的总结

  • 作用1:将String展开 (let str = "abcd")

    • let arr = [...str] => arr=["a","b","c","d"]
    • let obj = {...str} => obj={0:"a", 1:"b", 2:"c", 3:"d"}
  • 作用2: 在数组构造时,将数组表达式展开(let a1 = [1,2]; let a2 = [3,4])(只能用于可迭代对象)

    • let arr = [...a1, ...a2] => arr=[1,2,3,4]
    • let obj = {...a1} => obj={0:"1", 1:"2"}
  • 作用3: 在构造字面量对象时,将对象表达式按key-value的方式展开(let obj1 = {name: "liu"}; let obj2 = {age: 15})

    • let obj = {...obj1, ...obj2} => obj={name:"liu", age:15}
    • let arr = [...obj1] => 报错!obj不是可迭代的
    • 如果obj里面有setter属性,并且obj1和obj2里面也有相同的属性名(不管是不是setter),都不会触发setter方法。但是Object.assign(target, ...sources)将所有可枚举属性和自有属性从一个或多个源对象复制到目标对象,返回修改后的对象。这个会触发target身上的setter方法。
  • 作用4: 在函数调用时,将参数收集为一个数组

    • function setName(...names){}; 调用函数时setName("liu", "bing", "tao");传入的三个参数,都在names数组里;等价于apply的方式