【扒一扒】这一次,完全理解 ES6 Iterator

写在前面

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

Iterator能够说是ES6内至关重大的一个特性,也是不少其余特性运行的基石。html

为何Iterator地位如此之高呢?前端

从一个变量提及

var arr = ['红','绿','蓝'];
复制代码

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

这个还不简单,直接来个 for循环,若是你以为循环 low,那就来个 forEach 呗。es6

ok,马上撸串代码面试

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

//forEacth
arr.forEach(item=>{
    console.log(item);
});

// 结果 略
复制代码

ok,没毛病。后端

那我们继续,请看下面代码。给定一个字符串,若是我想输出每个字符怎么作?api

var str='1234567890';
复制代码

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

能够用 for in,也能够用 for 循环,按照类数组方式来处理。bash

马上撸串代码

//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]();//获得迭代器方法,返回迭代器对象

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]();//获得迭代器

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]();
   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);
}
复制代码

对象为何没有默认部署

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

因此遍历的时候根本不知道如何肯定他们的前后顺序,因此须要咱们根据状况手动实现。

扩展

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]() {
    yield 'hello';
    yield 'world';
  }
};

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

能够看出它很是简洁,无需过多代码就能够生成一个迭代器。

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

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

下次搞 Generator

练习

若是以为本文有收获的话,能够试着作作下面的练习题,加深下理解,而后在评论内写上你的答案。

  1. 写一个迭代器(Iterator)对象 。
  2. 自定义一个可迭代对象。
  3. 说说你对Iterator的理解,总结性输出下。

参考

es6.ruanyifeng.com/#docs/itera…

developer.mozilla.org/zh-CN/docs/…

www.cnblogs.com/xiaohuochai…

最后一件事

我正在打造一个纯技术交流群,面向初中级前端开发者,以学习、交流、思考、提高能力为目标,由于一我的学不如你们一块儿学,有了更多的交流才会进步的更快。

  • 我理想的模式是,每期让一我的深刻学习一个技术,而后本身再转述给你们听,相似一个分享课堂,这样能够成倍的提高学习效率。

  • 或者能够按照题库的顺序依次进行,每人天天对对一个问题进行思考和总结性的输出,锻炼技术的同时提高表达能力。

  • 在这个群里不用担忧本身的能力不足,不用担忧问题是否过小白而不敢说,大胆的说出问题, 让更多的人一块儿来分析,说错了也不要紧。

有想加入请的关注公众号《前端技术江湖》,回复‘进群’,我拉你进群。

推荐你们关注个人公众号《前端技术江湖》,得到更多原创精选文章,另外在业余时间我作了一个面试题库网站,用来收集前端面试题,目的是节省时间,提升效率,碎片时间刷刷题。推荐你们多多关注。

为了方便找到题库入口不迷路,入口绑定了到了公众号的独立菜单。

网站入口 - 大前端面试刷题- http://bigerfe.com/


最后,但愿本文能够给你带了一些帮助,文中若有错误,欢迎在评论区指。

若是这篇文章能给你一丝启发,欢迎点赞和关注。

老铁们,江湖见 《前端技术江湖》

相关文章
相关标签/搜索