本文从使用 forEach 对数组进行遍历开始说起,粗略对比使用 forEach , for...in , for...of 进行遍历的差异,并由此引入 ES6 中 *可迭代对象/迭代器 *的概念,并对其进行粗略介绍。

forEach

forEach 方法按升序为数组中的有效值的每一项执行一次callback 函数

特点

  1. 只会对“有效值”进行操作,也就是说遍历的时候会跳过已删除或者未初始化的项,即会跳过那些值为 empty 的内容。(undefined 不会被跳过哦)。
  1. forEach 的第二个参数 thisArg 传入后可以改变 callback 函数的 this 值,如果不传则为 undefined 。当然, callback 所拿到的 this 值也受普遍规律的支配:这意味着如果 callback 是个箭头函数,则 thisArg 会被忽略。(因为箭头函数已经在词法上绑定了this值,不能再改了)
  2. 不建议在循环中增/删数组内容:首先,forEach 遍历的范围在第一次调用 callback 前就会确定,这意味着调用forEach 后添加到数组中的项不会被 callback 访问到。同时,由于 forEach 的遍历是基于下标的(可以这么理解,并能从 Polyfill 中看到这一实现),那么在循环中删了数组几项内容则会有奇怪的事情发生:比如下图中,在下标为 1 的时候执行了 shift() ,那么原来的第 3 项变为了第 2 项,原来的第 2 项则被跳过了。

缺点

  1. 无法中断或跳出:如果想要中断/跳出,可以考虑使用如下两种方式:

    1. 使用 for / for..of
    2. 使用 Array.prototype.every() / Array.prototype.some() 并返回 false / true 来进行中断/跳出
  2. 无法链式调用(因为返回了 undefined

for...in

for...in 循环实际是为循环”enumerable“对象而设计的:

for...in 语句以任意顺序遍历一个对象的 可枚举属性 。对于每个不同的属性,语句都会被执行。

特点

  1. 遍历可枚举属性:for...in 循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)。
  2. 可以被中断

缺点

  1. 会访问到能访问到的所有的 __ 可枚举属性__ ,也就是说会包括那些原型链上的属性。如果想要仅迭代自身的属性,那么在使用 for...in 的同时还需要配合 getOwnPropertyNames()hasOwnProperty()
  2. 不能保证for ... in将以任何特定的顺序返回索引,比如在 IE 下可能会乱来。
  3. 不建议用于迭代 Array
    1. 不一定保证按照下标顺序
    2. 遍历得到的下标是字符串而不是数字

for...of

for...of 的本质是在 可迭代对象(iterable objects) 上调用其 迭代方法 创建一个迭代循环,并执行对应语句。可以迭代 数组/字符串/类型化数组(TypedArray)/Map/Set/generators/类数组对象(NodeList/arguments) 等。需要注意的是, Object 并不是一个可迭代对象。

for...of 是 ES6 中新出炉的,其弥补了 forEachfor...in 的诸多缺点:

  1. 可以使用breakthrow 或return 等终止
  2. 能正常迭代数组(依赖数组默认的迭代行为)

区别

那么我们简单的几句话说明一下 for...offor...in 的区别:

  • for...in 语句以原始插入顺序(还不一定保证)迭代对象的 可枚举属性
  • for...of 语句遍历 可迭代对象 定义要迭代的数据。
  • for...of  得到的是 值(value), 而 for...in 得到的是 键(key)。

那么扯到了 可迭代对象 ,就不得不说说 ES6 中新增的与 可迭代对象/迭代器 有关东西了。

可迭代对象/迭代器

*iteration *是 ES6 中新引入的遍历数据的机制,其核心概念是:iterable(可迭代对象)  和 iterator(迭代器):

  • *iterable(可迭代对象):*一种希望被外界访问的内部元素的数据结构,实现了 Symbol.iterator 方法
  • *iterator(迭代器):*用于遍历数据结构元素的指针

可迭代协议(iterable protocol)

首先,可迭代协议允许 JavaScript 对象去定义或定制它们的迭代行为。而如 Array 或 Map 等内置可迭代对象有默认的迭代行为,而如 Object 则没有。(所以为什么不能对 Objectfor...of )再具体一点,即对象或其原型上有 [Symbol.iterator] 的方法,用于返回一个对象的无参函数,被返回对象符合迭代器协议。然后在该对象被迭代的时候,调用其 [Symbol.iterator] 方法获得一个在迭代中使用的迭代器。

首先可以看见的是 Array 和 Map 在原型链上有该方法,而 Object 则没有。这印证了上面对于哪些可以用于 for...of  的说法。

如果我们非要想用 for...ofObject 所拥有的属性进行遍历,则可使用内置的 Object.keys() 方法:

for (const key of Object.keys(someObject)) {console.log(key + ": " + someObject[key]);
}
复制代码

或者如果你想要更简单得同时得到键和值,可以考虑使用 Object.entries()

for (const [key, value] of Object.entries(someObject)) {console.log(key + ": " + value);
}
复制代码

其次,有如下情况会使用可迭代对象的迭代行为:

  1. for...of
  2. 扩展运算符(Spread syntax)
  3. yield*
  4. 解构赋值(Destructuring assignment )
  5. 一些可接受可迭代对象的内置API(本质是这些接口在接收一个数组作为参数的时候会调用数组的迭代行为)
    1. Array.from()
    2. Map(), Set(), WeakMap(), WeakSet()
    3. Promise.all() /Promise.race() 等

迭代器协议(iterator protocol)

上文说到返回了一个迭代器用于迭代,那我们就来看看符合什么样规范的才算一个 迭代器 。 只需要实现一个符合如下要求的 next 方法的对象即可:

属性
next
返回一个对象的无参函数,被返回对象拥有两个属性:
  • done (boolean)
    • 如果迭代器已经经过了被迭代序列时为 true。这时 value 可能描述了该迭代器的返回值。
    • 如果迭代器可以产生序列中的下一个值,则为 false。这等效于连同 done 属性也不指定。
  • value - 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

本质上,在使用一个迭代器的时候,会不断调用其 next() 方法直到返回 done: true

自定义迭代行为

既然符合可迭代协议的均为可迭代对象,那接下来就简单自定义一下迭代行为:

// 让我们的数组倒序输出 value
const myArr = [1, 2, 3];
myArr[Symbol.iterator] = function () {const that = this;let index = that.length;return {next: function () {if (index > 0) {index--;return {value: that[index],done: false};} else {return {done: true};}},};
};
[...myArr]; // [3, 2, 1]
Array.from(myArr) // [3, 2, 1]
复制代码

一句说明可迭代对象和迭代器的关系

当一个__可迭代对象__需要被迭代的时候,它的 Symbol.iterator 方法被无参调用,然后返回一个用于在迭代中获得值的迭代器。 换句话说,一个对象(或其原型)上有符合标准的 Symbol.iterator 接口,那他就是 可迭代的(Iterator) ,调用这个接口返回的对象就是一个 迭代器

关闭迭代器

上文提到说 for...offorEach 好在其可以被“中断”,那么对于在 for...of 中中断迭代,其本质是中断了迭代器,迭代器在中断后会被关闭。说到这里,就继续说一下迭代器关闭的情况了。

首先,迭代器的关闭分为两种情况:

  1. Exhaustion:当被持续调用 next() 方法直到返回 done: true ,也就是迭代器正常执行完后关闭
  2. Closing:通过调用 return() 方法来告诉迭代器不打算再调用 next() 方法

那么什么时候会调用迭代器的 return 方法呢:

  1. 首先,return() 是个可选方法,只有具有该方法的迭代器才是 可关闭的(closable)
  2. 其次,只有当没有 Exhaustion 时才应该调用 return() ,如 breakthrow 或 return

最后,return() 方法中也不是想怎么写就怎么写的,也有自己的要求, return()方法需要符合以下规范:

  1. return(x) 通常应该返回如 { done: true, value: x } 的结果,如果返回的不是个对象则会报错
  2. 调用return()后, next()返回的对象也应该是 done:true (这就是为什么有一些迭代器在 for...of 循环中中断后无法再次使用的原因,比如 Generator

同时,需要额外注意的是,及时在收到迭代器最后一个值后调用 break 等,也会触发 return()

function createIterable() {let done = false;const iterable = {[Symbol.iterator]() {return this;},next() {if (!done) {done = true;return {done: false,value: 'a'};} else {return {done: true,value: undefined};}},return () {console.log('return() was called!');return {done: true,value: undefined};},};return iterable;
}
for (const value of createIterable()) {console.log(value);break;
}
复制代码

生成器(Generator)

既是迭代器也是可迭代对象

上文 迭代器协议 中提到的返回的拥有 next() 方法的对象和我们在 Generator 中使用的 next() 方法似乎一模一样。确实, Generator 符合可迭代协议和迭代器协议的。

因为 Generator 既有符合规范的 next() (迭代器协议)方法,也有 Symbol.iterator (可迭代协议)方法,因此它 既是迭代器也是可迭代对象

可关闭的(closable

默认情况下,Generator对象是可关闭的。因此在用 for...of 时中断迭代后,无法再次对原有 Generator对象进行迭代。(因为调用return()后, next()返回的对象也应该是 done:true

当然,既然是默认情况,我们就可以想办法让其无法被关闭: 可以通过包装一下迭代器,将迭代器本身/原型上的 return() 方法被重写掉

class PreventReturn {constructor(iterator) {this.iterator = iterator;}[Symbol.iterator]() {return this;}next() {return this.iterator.next();}// 重写掉 return 方法return (value = undefined) {return {done: false,value};}
}
复制代码

更多关于 Generator 的内容就不在本篇进行阐述,有机会将单独作为一篇慢慢讲。

参考

  • MDN-forEach
  • MDN-for...in
  • MDN-for...of
  • MDN-delete(跨浏览器提示)
  • MDN-迭代协议
  • MDN-Generator
  • Iterator 和 for...of 循环
  • Iterables and iterators

从forEach到迭代器相关推荐

  1. Java 方法使用总结(重载、数组输出、enum和switch、foreach和迭代器、可变长度参数、重载中使用可变长度参数)

    方法重载 方法名相同 方法的参数类型,参数个不一样 方法的返回类型可以不相同 方法的修饰符可以不相同 main 方法也可以被重载 class MyClass {int height;MyClass() ...

  2. java里的foreach迭代器_java 中 for 、foreach 和 迭代器 的学习笔记

    for  ,foreach 的区别如下: 1. 形式区别 对于for循环,我们采用: for(int i=0;i 对于foreach: for(int i:arr){...} 对与迭代器: Itera ...

  3. 面试官:有了 for 循环 为什么还要 forEach ?

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:juejin.cn/post/7018097650687803422 本质区别 for循环和forEach的语法区别 for循 ...

  4. java foreach 删除_为什么java不要在foreach循环里进行元素的remove/add操作

    首先,这涉及多线程操作,Iterator是不支持多线程操作的,List类会在内部维护一个modCount的变量,用来记录修改次数 举例:ArrayList源码 protected transient ...

  5. Java基础23-集合类2(Set接口,Iterator迭代器)

    一.Set接口简介 根据API,Set接口是一个不包含重复元素的 collection.更确切地讲,set 不包含满足 e1.equals(e2) 的元素对 e1 和 e2,并且最多包含一个 null ...

  6. C# in depth (第六章 实现迭代器的捷径)

    迭代器模式是行为模式的一种范例,行为模式是一种简化对象之间通信的设计模式.这是一种非常易于理解和使用的模式.实际上,它允许你访问一个数据项序列中的所有元素,而无须关心序列是什么类型----数组,列表, ...

  7. 大话设计模式——迭代器模式

    迭代器模式 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示. 当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,你就应该考虑用迭代器模式. 你需要对 ...

  8. 面试官:有了for循环 为什么还要forEach?

    面试官:有了for循环 为什么还要forEach? js中那么多循环,for for-in for-of forEach,有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异.我们从 ...

  9. 有了for循环 为什么还要forEach?

    js中那么多循环,for for-in for-of forEach,有些循环感觉上是大同小异今天我们讨论下for循环和forEach的差异. 我们从几个维度展开讨论: for循环和forEach的本 ...

  10. 通过foreach遍历ArrayList时同时修改报错分析

    遍历ArrayList可以有for循环.foreach.迭代器iterator.listIterator,其中通过foreach来遍历同时修改 ArrayList时会抛出 ConcurrentModi ...

最新文章

  1. JSP中两种include的区别
  2. Kotlin的Reified类型:怎样在函数内使用这一类型(KAD 14)
  3. 运营商与SP再次博弈手机广告
  4. android 自定义view实现拖动放大缩小_自定义itemClickView
  5. linux开发windows程序设计,X-win编程求助
  6. java 线程 函数_java – 从后台线程调用主线程上的函数
  7. 书籍推荐:Machine Learning Yearning
  8. TCP/IP网络编程(3)
  9. java 网络编程 总结篇
  10. php 公众号推送图片尺寸,微信公众号推送文图片什么尺寸最佳?
  11. 恶意代码分析实战_实验练习
  12. 常见计算机蓝屏代码,史上最全蓝屏代码分析,快速解决电脑常见故障,值得收藏!...
  13. [历年IT笔试题]美团2015校园招聘笔试题
  14. Clouda聊天室实践
  15. 【原创】使用高德 API
  16. 六年级计算机学习,小学六年级计算机学习教案(24页)-原创力文档
  17. 如何用电脑控制手机屏幕,写工作日志
  18. qq邮箱服务器发信怎么配置,WordPress网站实现使用QQ邮箱作为SMTP发信服务器配置教程...
  19. T-SQL 基础学习 01
  20. 使用NetBackup进行oracle备份和恢复

热门文章

  1. python3 annotations
  2. Request.getparameternames有什么用
  3. sap快捷搜索菜单栏
  4. [杂谈]逗比的语录?
  5. 主数据文件损坏(或丢失)情况下,如何备份尾部事务日志.
  6. 将输入中的制表符替换成适当数目的空格,使空格充满到下一个制表符终止位的地方...
  7. Ubuntu使用技巧集锦(持续追加中……)
  8. 在vue的项目中引入swiper插件
  9. 《移动平台开发实践》第1周作业
  10. java-读取xml