深入理解 Es6 Iterator

avatar
@腾讯科技(深圳)有限公司

作者:zz_jesse
转自 juejin.cn/post/684490…

本文主要来说下ES6Iterator,目的在于理解它的概念、作用、以及现有的应用,最后学以致用。

Iterator可以说是 ES6 内相当重大的一个特性,也是很多其他特性运行的基石。

为什么Iterator地位如此之高呢?

从一个变量说起

var arr = ['红','绿','蓝'];

上面是一个普通的数组,如果我要获取他的每一项数据,应该怎么做?

这个还不简单,直接来个 for 循环,如果你觉得循环 low,那就来个 forEach 呗。

ok,立刻撸串代码

//for 循环
for (var i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

//forEach
arr.forEach(item => {
    console.log(item);
});
// 结果 略

ok,没毛病。

那咱们继续,请看下面代码。给定一个字符串,如果我想输出每一个字符怎么做?

var str = '1234567890';

有么有搞错,这简单的让人发笑。

可以用 for in,也可以用 for 循环,按照类数组方式来处理。

立刻撸串代码

//for in
for (var s in str) {
    console.log(str[s]);
    //s 是 属性名称【key】
}

//转为数组
for (var i = 0; i < str.length; i++) {
    console.log(str[i]);
}

//或者转换为数组
Array.prototype.slice.call(str);

不过 for in 不是用来获取数据的,他会遍历对象上所有可枚举的属性,包括自身的和原型链上的,所以这个不保险。

emmm....没啥问题,那咱们继续。

请看下面代码,给定一个map对象,然后输出它每一项数据。

var map = new Map();
map.set('zhang', '1000w');
map.set('liu', '200w');
map.set('sun', '100w');

forEach 就妥妥的了。

map.forEach((val,key) => {
    console.log(val,key);
})

到这里看了这么多如此简单到令人发指的提问,估计都有些坐不住了,要掀桌子走人了。请稍后,慢慢往下看。

发现问题

好了,在上一步几个简单的问题中,我们的操作都是获得他们的每一项数据。

当然方法有很多种,实现方式也有很多,for 循环forEachfor in 啦。

但是有没有发现一个问题,或者我们站在一个更高的维度去看待,其实这些方法都不能通用,也就是说上面的这几种集合数据不能使用统一的遍历方法来进行数据获取。

谁说的,能统一呀,都可以用 forEach来遍历,数组和 map 本身就支持,字符串我直接转为数组后可以了。

ok,这没什么毛病。

但是每次都要转换,还要封装,还有可能要侵入原型。

那有没有一种更好的,通用方法,让开发者用的更舒服,更爽呢?

答案是肯定的,es5的时候还没出现,升级到 es6就有了。

NB 的 for of,扒一扒

这个可以对不同数据结构进行统一遍历的方式就是 es6for of 循环。

for of 循环 和 古老的for 循环很像呀。不就是个新增语法么。

引用阮大佬书中一句话,相信一看便知。

ES6 借鉴 C++、Java、C# 和 Python 语言,引入了 for...of 循环。作为遍历所有数据结构的统一的方法。 关键在于统一,另一个就是直接取值,简化操作,不需要在声明和维护什么变量、对数据做转换。

原来for of 这么牛,for 循环搞不定的你可以搞定。

为什么 for of 能具备这个能力,可以为不同的数据结构提供一种统一的数据获取方式。

for of 真的这么强大吗,他背后的机制是什么?

其实for of 并不是真的强大,他只是一种ES6新的语法而已。

并不是所有的对象都能使用 for of,只有实现了 Iterator接口的对象才能够使用 for of 来进行遍历取值。

所以说 for of 只是语法糖,真正的主角是Iterator

What ? Iterator.....

主角登场 - Iterator 迭代器

Iterator 是一种接口,目的是为不同的数据结构提供统一的数据访问机制。也可以理解为 Iterator 接口主要为 for of 服务的,供for...of进行消费。

其实在很多后端语言多年前早已存在 Iterator 这个特性,如 java、C++、C#等。

既然他是一种接口,那我们应该怎样实现这个接口呢?实现规则是什么样的呢?

因为 javascript 语言里没有接口的概念,这里我们可以理解成它是一种特殊的对象 - 迭代器对象,返回此对象的方法叫做迭代器方法。

首先他作为一个对象,此对象具有一个next方法,每次调用 next 方法都会返回一个结果值。

这个结果值是一个 object,包含两个属性, valuedone

value表示具体的返回值, done 是布尔类型的,表示集合是否遍历完成或者是否后续还有可用数据,没有可用数据则返回 true,否则返回 false

另外内部会维护一个指针,用来指向当前集合的位置,每调用一次 next 方法,指针都会向后移动一个位置(可以想象成数组的索引)。

看下代码实现

function getIterator(list) {
    var i = 0;
    return {
        next: function() {
            var done = (i >= list.length);
            var value = !done ? list[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}
var it = getIterator(['a', 'b', 'c']);
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 }"
console.log(it.next());// "{ value: undefined, done: true }"
console.log(it.next());// "{ value: undefined, done: true }"

上面代码便是根据迭代器的基本概念衍生出来的一个模拟实现。

  • getIterator方法返回一个对象 - 可迭代对象

  • 对象具有一个next 方法,next 方法内部通过闭包来保存指针 i 的值,每次调用 next 方法 i 的值都会+1.

  • 然后根据 i 的值从数组内取出数据作为 value,然后通过索引判断得到 done的值。

  • i=3的时候,超过数组的最大索引,无可用数据返回,此时 done 为true,遍历完成。

可迭代对象

到这里我们已经大概了解了 Iterator, 以及如何创建一个迭代器对象。但是他和 for of 有什么关系呢?

for of 运行机制

for of执行的时候,循环过程中引擎就会自动调用这个 对象上的迭代器方法, 依次执行迭代器对象的 next 方法,将 next 返回值赋值给 for of 内的变量,从而得到具体的值。

我觉得上面一句话包含了一个重要的信息- “对象上的迭代器方法”。

实现可迭代对象

对象上怎么会有迭代器方法呢?

ES6里规定,只要在对象的属性上部署了 Iterator接口,具体形式为给对象添加Symbol.iterator属性,此属性指向一个迭代器方法,这个迭代器会返回一个特殊的对象 - 迭代器对象。

而部署这个属性并且实现了迭代器方法后的对象叫做可迭代对象

此时,这个对象就是可迭代的,也就是可以被 for of 遍历。

Symbol.iterator,它是一个表达式,返回 Symbol 对象的 iterator 属性,这是一个预定义好的、类型为 Symbol 的特殊值。

举个栗子

普通的对象是不能被 for of 遍历的,直接食用会报错。

var obj = {};
for (var k of obj) {
}

obj 不是可迭代的对象。

那么我们来,让一个对象变成可迭代对象,按照协议也就是规定来实现即可。

iterableObj 对象上部署 Symbol.iterator属性,然后为其创建一个迭代器方法,迭代器的规则上面我们已经说过啦。

var iterableObj = {
    items: [100,200,300],
    [Symbol.iterator]: function() {
        var self = this;
        var i = 0;
        return {
            next: function () {
                var done = (i >= self.items.length);
                var value = !done ? self.items[i++] : undefined;
                return {
                    done: done,
                    value: value
                };
            }
        };
    }}
        
    //遍历它
    for (var item of iterableObj) {
        console.log(item);//100,200,300
    }

就这么简单,上面这个对象就是可迭代对象了,可以被 for of 消费了。

Iterator 原生应用场景

我们再回到最开始使用 for of 来进行遍历字符串、数组、map,我们并没有为他们部署Iterator接口,仍然可以使用 for of 遍历。

这是因为在 ES6中有些对象已经默认部署了此接口,不需要做任何处理,就可以使用 for of 来进行遍历取值。

不信?咿,你好难搞,我不要你说 - 信,我要我说 - 信。

看看能不能拿到它们的迭代器。

数组

//数组
var arr = [100,200,300];

var iteratorObj = arr[Symbol.iterator]("Symbol.iterator");

//得到迭代器方法,返回迭代器对象
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());
console.log(iteratorObj.next());

字符串

因为字符串本身的值是有序的,并且具有类数组的特性,支持通过索引访问,所以也默认部署了iterator接口。

var str = 'abc';

var strIteratorObj = str[Symbol.iterator]("Symbol.iterator");//得到迭代器

console.log(strIteratorObj.next());
console.log(strIteratorObj.next());
console.log(strIteratorObj.next());
console.log(strIteratorObj.next());

或者直接看原型上是否部署了这个属性即可。

arguments 类数组

函数内的arguments 是一个类数组,他也支持 for of,因为他内部也部署了 Iterator 接口。

我们都知道对象是默认没有部署这个接口的,所以arguments这个属性没有在原型上,而在在对象自身的属性上。

function test() {
    var obj = arguments[Symbol.iterator]("Symbol.iterator");
    console.log(arguments.hasOwnProperty(Symbol.iterator));
    console.log(arguments);
    console.log(obj.next());
}

test(1,2,3);

总结来说,已默认部署 Iterator 接口的对象主要包括数组、字符串、Set、Map 、类似数组的对象(比如 arguments 对象、DOM NodeList 对象。

代码验证略,都是一个套路,不多说

Iterator 另外一个作用

Iterator除了可以为不同的数据结构提供一种统一的数据访问方式,还有没有发现其他的作用?

那就是数据可定制性,因为我们可以随意的控制迭代器的 value 值。

比如:数组本身就是一个可迭代的,我们可以覆盖他的默认迭代器。

var arr = [100,200,300];

for(var o of arr) {
    console.log(o);
}

for of 数组默认输出如下

经过我们的改造

var arr = [100,200,300];
arr[Symbol.iterator] = function () {
    var self = this;
    var i = 0;
    return {
        next: function () {
            var done = (i >= self.length);
            var value = !done ? self[i++] : undefined;
            return {
               done: done,
               value: value
            };
        }
    };
}

for (var o of arr) {
    console.log(o);
}

对象为什么没有默认部署

对象可能有各种属性,不像数组的值是有序的。

所以遍历的时候根本不知道如何确定他们的先后顺序,所以需要我们根据情况手动实现。


关注【IVWEB社区】公众号查看最新技术周刊,今天的你比昨天更优秀!


扩展

for of 中断

我们都知道普通的 for 循环是可以随时中断的,那 for of 是否可以呢?

答案是肯定的,for of机制兼顾了 forforEach

迭代器除了必须next 方法外,还有两个可选的方法 returnthrow方法。

如果 for of 循环提前退出,则会自动调用 return 方法,需要注意的是 return 方法必须有返回值,且返回值必须是 一个object

ps:以抛出异常的方式退出,会先执行 return 方法再抛出异常。

var iterableObj = {
    items: [100,200,300],
    [Symbol.iterator]: function () {
        var self = this;
        var i = 0;
        return {
            next: function () {
                var done = (i >= self.items.length);
                var value = !done ? self.items[i++] : undefined;
                return {
                    done: done,
                    value: value
                };
            },
            return() {
                console.log('提前退出');
                return {
                   //必须返回一个对象
                    done: true
                }
            }
        };
    }}
    
    for (var item of iterableObj) {
        console.log(item);
        if(item === 200) {
            break;
    }}
    
    for (var item of iterableObj) {
        console.log(item);
        throw new Error();
    }

ps:throw 方法这里先不说,这里不是他的用武之地,后续文章见。

不止 for of

除了 for of 执行的时候会自动调用对象的Iterator方法,那么 ES6里还有没有其他的语法形式?

解构赋值

对可迭代对象进行解构赋值的时候,会默认调用Symbol.iterator方法。

//字符串
var str = '12345';
var [a,b] = str;
console.log(a,b);// 1 2

//map
var map = new Map();
map.set('我','前端');
map.set('是','技术');
map.set('谁','江湖');
map.set('作者','zz_jesse');

var [d,e] = map;
console.log(d,e);
//["我", "前端"] ["是", "技术"]....

同样如果对一个普通对象进行解构,则会报错。

因为普通对象不是可迭代对象。

var [d, e] = {name: 'zhang'};

从一个自定义的可迭代对象进行解构赋值。

var iterableObj = {
    items: ['红','绿','蓝'],
    [Symbol.iterator]: function () {
        var self = this;
        var i = 0;
        return {
            next: function () {
                var done = (i >= self.items.length);
                var value = !done ? self.items[i++] : undefined;
                return {
                    done: done,
                    value: value
                };
            }
        };
    }}
    var [d,e] = iterableObj;
    console.log(d,e);//红 绿

解构赋值的变量的值就是迭代器对象的 next 方法的返回值,且是按顺序返回。

扩展运算符

扩展运算符的执行(...)也会默认调用它的Symbol.iterator方法,可以将当前迭代对象转换为数组。

//字符串
var str = '1234';
console.log([...str]);//[1, 2, 3, 4]  转换为数组

//map 对象
var map = new Map([[1, 2], [3, 4]]);
[...map] //[[1, 2], [3, 4]

//set 对象
var set = new Set([1, 2, 3]);
[...set]//[1, 2, 3]

通用普通对象是不可以转为数组的。

var obj = {name: 'zhang'};
[...obj]//报错

作为数据源

作为一些数据的数据源,比如某些 api 方法的参数是接收一个数组,都会默认的调用自身迭代器。

例如:Set、Map、Array.from 等

//为了证明,先把一个数组的默认迭代器给覆盖掉

var arr = [100, 200, 300];

arr[Symbol.iterator] = function () {
    var self = this;
    var i = 0;
    return {
        next: function () { 
            var done = (i >= self.length); 
            var value = !done ? self[i++] : undefined;  
            return {        
                done: done,        
                value: value+'前端技术江湖' 
                //注意这里   
            };       
        }
    };
}

for (var o of arr) { 
    console.log(o);
}

// 100前端技术江湖
// 200前端技术江湖
// 300前端技术江湖

已覆盖完成

//生成 set  对象
var set = new Set(arr);

//调用 Array.from方法
Array.from(arr);

yield* 关键字

yield*后面跟的是一个可遍历的结构,执行时也会调用迭代器函数。

let foo = function* () {
    yield 1;
    yield* [2,3,4]; 
    yield 5;
};

yield 需要着重说明, 下一节再详细介绍。

判断对象是否可迭代

既然可迭代对象的规则必须在对象上部署Symbol.iterator属性,那么我们基本上就可以通过此属来判断对象是否为可迭代对象,然后就可以知道是否能使用 for of 取值了。

function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable('abcdefg')); // true
console.log(isIterable([1, 2, 3]));// true
console.log(isIterable("Hello"));// true
console.log(isIterable(new Map())); // true
console.log(isIterable(new Set()));// true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet()));// false

总结

ES6的出现带来了很多新的数据结构,比如 Map ,Set ,所以为了数据获取的方便,增加了一种统一获取数据的方式 for of 。而 for of 执行的时候引擎会自动调用对象的迭代器来取值。

不是所有的对象都支持这种方式,必须是实现了Iterator接口的才可以,这样的对象我们称他们为可迭代对象。

迭代器实现方式根据可迭代协议,迭代器协议实现即可。

除了统一数据访问方式,还可以自定义得到的数据内容,随便怎样,只要是你需要的。

迭代器是一个方法, 用来返回迭代器对象。

可迭代对象是部署了 Iterator 接口的对象,同时拥有正确的迭代器方法。

ES6 内很多地方都应用了Iterator,平时可以多留意观察,多想一步。

是结束也是开始

到这里我们已经可以根据迭代器的规则自定义迭代器了,但实现的过程有些复杂,毕竟需要自己来维护内部指针,有不少的逻辑处理,难免会出错。

那有没有更优雅的实现方式呢?

有,那就是- Generator -生成器 。

let obj = {
    *[Symbol.iterator]( "Symbol.iterator") {
        yield 'hello';
        yield 'world';
    }
};

for (let x of obj) {
    console.log(x);
}
// "hello"
// "world"

可以看出它非常简洁,无需过多代码就可以生成一个迭代器。

它除了可以作为生成迭代器的语法糖,他还有更多神奇的能力。

这么好玩的能力势必要扒一扒,不过今天天色已晚,熬夜伤身,这次就先搞定Iterator

下次搞 Generator

练习

如果觉得本文有收获的话,可以试着做做下面的练习题,加深下理解。

  1. 写一个迭代器(Iterator)对象 。

  2. 自定义一个可迭代对象。

  3. 说说你对Iterator的理解,总结性输出下。

参考资料

[1] es6.ruanyifeng.com/#docs/itera… es6.ruanyifeng.com/#docs/itera…

[2] developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…

[3] www.cnblogs.com/xiaohuochai… www.cnblogs.com/xiaohuochai…