Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
youngwind opened this issue Oct 23, 2016 · 0 comments
Open

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

youngwind opened this issue Oct 23, 2016 · 0 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Oct 23, 2016

顾名思义

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

  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]

===完===

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant