数组遍历、for...of、Interator接口与迭代器模式

@youngwind 2016-10-23 03:48:56发表于 youngwind/blog JS

顾名思义

近日,我又在思考这个问题:**在编程的世界中,如何高效地学习理论知识,应用理论知识来解决实际生产中的问题。**前人的研究已经硕果累累,列举几点如下:

  1. 理论知识往往是抽象的,要多用形象化的思维辅佐思考。
  2. 不要光看书,要多敲几遍demo。
  3. 当碰到实际难题的时候,回过头来看看相应的理论,刻意补足。这样下次就不会再掉到这个坑里了。
  4. 多联想,多与已学到的知识建立有效关联。因为有效关联越多,碰到实际难题的时候,想起对应理论知识的概率才会越大。

所以,今天我们的目标是:将数组遍历、for...of、Interator接口与迭代器模式这几个概念串联起来,以加强记忆和理解。

数组遍历

在ES5中,我们有3种方法可以遍历数组。而ES6又给我们多提供了一种for...of。那么,他们之间各有什么优缺点呢?

以数组为例,JavaScript提供多种遍历语法。最原始的写法就是for循环。

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

这种写法比较麻烦,因此数组提供内置的forEach方法。

myArray.forEach(function (value) {
  console.log(value);
});

这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。

for...in循环可以遍历数组的键名。

for (var index in myArray) {
  console.log(myArray[index]);
}

for...in循环有几个缺点。

  1. 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  2. for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  3. 某些情况下,for...in循环会以任意顺序遍历键名。
    总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

    for...of循环相比上面几种做法,有一些显著的优点。
for (let value of myArray) {
  console.log(value);
}
  1. 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  2. 不同于forEach方法,它可以与break、continue和return配合使用。
  3. 提供了遍历所有数据结构的统一操作接口。

来源:阮一峰老师的es6教程

注:for...of不同于forfor...in的地方还有一处:for...of直接遍历的是数组的值,而非数组的下标,而forfor...in仅仅是遍历数组的下标(当然你可以通过下标进一步获取值。遍历下标还是遍历值,仅仅在于语言层面的区别才有意义)。所以ES6的for...of可以说是实现了直接遍历数组的值的命令语句

Interator接口

for...of的内部实现原理又是什么呢? → 是 iterator接口
比如下面这样的代码:

var myArray = ['a','b','c'];
var it = myArray[Symbol.iterator]();
console.log(it.next());   // {value: 'a', done: false}
console.log(it.next());   // {value: 'b', done: false}
console.log(it.next());   // {value: 'c', done: false}
console.log(it.next());   // {value: undefined, done: true}

**在ES6中,数组内置了iterator接口,但是普通的对象不是。也就是说,for...of无法直接用于普通对象的遍历。**如下图所示。
2016-10-23 4 28 32

那有什么解决方案吗?
从图中我们可以看出,其实for...of语句本质上是调用了iterator接口。所以对于任意的数据结构而言,只要部署了interator接口,就可以使用for...of创建迭代器,然后通过迭代器遍历其中的值。
所以,下面我们来看看,如何给一个普通的对象部署iterator接口

var myObject = {
    a: 2,
    b: 3
}

Object.defineProperty(myObject, Symbol.iterator, {
    enumerable: false,
    writable: false,
    configurable: true,
    value: function () {
        var o = this;
        var idx = 0;
        var ks = Object.keys(o);
        return {
            next: function () {
                return {
                    value: o[ks[idx++]],
                    done: (idx > ks.length)
                }
            }
        }
    }
});

for(let v of myObject) {
    console.log(v);
}
// 输出:
// 2
// 3

以上参考自《你不知道的JavaScript》上卷中的第二部分第3章第4小节。

Interator接口本质上是一个生成迭代器的函数,执行它,就可以生成一个迭代器,不断执行迭代器的next方法,就可以遍历该数据结构。

迭代器模式

迭代器生成函数(Interator)让我想起了一种设计模式迭代器模式,曾探所著的《JavaScript设计模式与开发实践》中第7章讲的就是这种模式。

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
书中演示了一个文件上传的例子,情景是:在不同的浏览器环境下,选择的上传方式是不一样的。所以我们会优先使用控件上传。如果浏览器没有安装上传控件,则使用Flash上传,如果连Flash也没安装,那就只好使用浏览器原生的表单上传了。

看,这种场景其实就是遍历。特殊点在于,当遍历到合适的情况的时候,就停止遍历了。这不正是for...of所擅长的吗?
我们先来看看不使用for...of的bad code

var getUploadObj = function(){
    try {
        return new ActiveXObject('TXFTNActiveX.FTNUpload');   // IE上传控件
    } catch (e) {
        try {
            new ActiveXObject('ShockwaveFlash.ShockwaveFlash');   // Flash上传控件
            let str = '<object type="application/x-shockwave-flash"></object>';
            return $(str).appendTo($('body'));
        } catch (e) {
            let str = '<input name="file" type="file" calss="ui-file"/>';   // 表单上传
            return $(str).appendTo($('body'));
        }
    }
}

var uploadObj = getUploadObj();
console.log(uploadObj);  // [input, prevObject: Z.fn.init[1], context: undefined]

**如果以后要有更多上传的方式,那么代码中将嵌套更多的try...catchif...else,可维护性非常差。**作者在书中重构了一版代码,写得非常精彩,具体的请直接看书。下面我给出我重构的版本,是基于for...of的。

let getUploadObj = {
    getActiveUploadObj (){
        try {
            return new ActiveXObject('TXFTNActiveX.FTNUpload');   // IE上传控件
        } catch (e) {
            return false;
        }
    },
    getFalshUploadObj () {  
        try {
            new ActiveXObject('ShockwaveFlash.ShockwaveFlash');   // Flash上传控件
            let str = '<object type="application/x-shockwave-flash"></object>';
            return $(str).appendTo($('body'));
        } catch (e) {
            return false;
        }
    },
    getFormUploadObj () {
        let str = '<input name="file" type="file" calss="ui-file"/>';   // 表单上传
        return $(str).appendTo($('body'));
    }
}

// 给对象getUploadObj定义iterator接口,上面演示过这段代码
// 这里可以通过工厂模式,抽象成一个专门给对象安装iterator接口的函数,这样就可以省却很多重复代码了。
Object.defineProperty(getUploadObj, Symbol.iterator, {
    enumerable: false,
    writable: false,
    configurable: true,
    value: function(){
        var o = this;
        var idx = 0;
        var ks = Object.keys(o);
        return {
            next: function(){
                return {
                    value: o[ks[idx++]],
                    done: (idx > ks.length)
                }
            }
        }
    }
});


function iteratorUploadObj (uploadObj){
    // 直接使用`for...of`遍历uploadObj对象
    for(let getUpload of uploadObj){
        let uploadObj = getUpload();
        if(uploadObj) return uploadObj;
    }
}

let uploadObj = iteratorUploadObj(getUploadObj);
console.log(uploadObj);  // [input, prevObject: Z.fn.init[1], context: undefined]

===完===