循环遍历可以说是最常用的操作了,但是对各种循环遍历的方法理解可能并没有那么深。最近被问到许多与循环有关的问题,发现并不能答得很好,于是决定写一篇文章,从头到尾梳理一下。
问题列表:
for...in 和for...of有什么区别?for...of了解吗?for...of和 forEach这两个怎么取舍?另外一个与循环有关的问题就是循环中的setTimeout,判断运行结果是什么。这个问题就不单单是循环了,会牵涉到闭包、事件循环等知识。
1 | let arr = [11,22,33,44,55,66,77] |
for...in不宜用来遍历数组。问题在于:
1 | for(let index in arr){ |
for...in会遍历所有可枚举属性,包括原型上的属性。1 | let arr = [1,2,3,4,5,6,7] |
for...in还有一个特点,用例子来说明:
1 | let arr = [1,2,3,2,2,4,5] // 想要用for in去掉所有的2 |
也就是说我们期望的在splice之后将i往前移一位的操作是不能实现的,这就导致了出现两个连续的2时,splice掉第一个2之后跳过了第二个2. 在for...in循环中,下标序列(string类型)在创建循环时就确定了,无法在循环过程中对其进行操作。而这个情况下如果用普通的for循环是可以满足要求的:
1 | let arr = [1,2,3,2,2,4,5] |
总的来说,for in适合遍历对象,而不适宜遍历数组。
for...of是es6中出现的,功能十分强大。只要是可以被迭代的数据结构,都可以使用for...of来遍历。
摘录阮一峰博客中的一段话:一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
用for of 遍历各种数据类型的示例,可以参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...of
而对于一个对象,for of有时会失灵。例如:
1 | let obj = { |
原因是obj对象不是可迭代的。而如果使用for in则可以遍历对象中key的值:
1 | let obj = { |
因此总结来说,for in 适合遍历对象,而for of适合遍历数组。
4.16更新内容:关于@@iterator
之前提到只要一个数据结构有iterator接口,就可以使用for...of对其进行遍历。这样的说法是正确的。for...of循环首先向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有的返回值。而数组对象有内置的@@iterator,这也就是for...of能直接应用在数组上的原因。
1 | let arr = [1,3,5] |
和数组对象不同,普通的对象并没有内置的@@iterator,所以不能进行for...of遍历。不过,如果想的话,也可以给对象定义@@iterator,就像下面这样:
1 | let obj = { |
从这个例子可以看出来,迭代器是可以自定义的。而这也就给了用户非常多的自由度,可以在各种自定义的数据结构上利用自定义迭代器和for...of进行各种操作。
forEach() 方法对数组的每个元素执行一次给定的函数,与map很类似。值得注意的是,与map不同,forEach并不会返回任何东西。
1 | let arr = [1,2,3] |
还有一点就是forEach无法被break、continue等语句打断,无法中止或者跳出循环。
这一块内容其实和循环关系不大,更多的是关于作用域以及闭包的问题,但是想到了就也放在这篇文章里。
1 | for(var i=0;i<5;i++){ |
运行的结果是在1000ms后输出了5个5.
setTimeout是一个异步函数,执行到setTimeout函数后会将其弹入宏任务队列中,然后继续执行后续的同步代码。执行完所有的代码后开始清空宏任务队列,这里由于循环了五次,宏任务队列中则会有5个console.log(i)的任务等待清空。这时由于要输出i,执行器会在全局作用域中寻找i。在循环条件中使用var关键字定义了全局变量i,循环结束后i等于5,因此输出了5个5.
1 | for(let i=0;i<5;i++){ |
运行的结果是在1000ms后输出0 1 2 3 4.
和上一题类似,但是使用了let关键词定义的变量i拥有块级作用域。在弹入宏任务队列时,块级作用域也会被一并弹入,因此i分别为0、1、2、3、4.
1 | for(let i=0;i<5;i++){ |
运行的结果是每隔一秒输出0、1、2、3、4.
和上一题类似,使用了let关键词,导致每一个宏任务都带有一个不同的i,这样第一个宏任务的i=0,没有时延直接输出0;第二个宏任务的i=1,延迟1000ms输出1;以此类推。如果在定义循环时使用var关键词,则会每隔一秒输出一个5,因为全局变量中的i此时等于5.
1 | for(let i=0;i<5;i++){ |
和上一题完全一致,只不过使用了立即执行函数,将当前的i作为参数传入了立即执行函数,那么在宏任务队列中的所有任务都有着不同的i。这里将let换成var也会得到一样的结果。
JavaScript — Mar 20, 2020