源于一次面试,一块儿面试的同事问面试者的一个问题:数组遍历有哪些方式?想来数组操做是平时开发中的经常使用技能,面试者吞吞吐吐大概就说出了两种方式吧,最后就淘汰掉啦(面试者是个很认真的妹纸,面试都在简单作一些笔记,不过基础确实有些困难~)。node
对于"数组遍历"这个问题,其实答案很宽泛,关键在于你能不能列举出必定数量的方法以及描述它们之间的区别。本文即介绍一下数组的基本遍历操做和高阶函数。python
本部分介绍4种最经常使用的遍历方式。面试
for...in实际上是对象的遍历方式,并非数组专有,使用for...in将循环遍历对象自己的全部可枚举属性,以及对象从其构造函数原型中继承的属性,其遍历顺序与Object.keys()函数取到的列表一致。数组
该方法会遍历数组中非数字下标的元素,会忽略空元素:浏览器
let list = [7, 5, 2, 3] list[10] = 1 list['a'] = 1 console.log(JSON.stringify(Object.keys(list))) for (let key in list) { console.log(key, list[key]) }
输出:函数
> ["0","1","2","3","10","a"] > 0, 7 > 1, 5 > 2, 2 > 3, 3 > 10, 1 > a, 1
这个方法遍历数组是最坑的,它一般表现为有序,可是由于它是按照对象的枚举顺序来遍历的,也就是规范没有规定顺序的,因此具体实现是由着浏览器来的。MDN文档里也明确建议“不要依赖其遍历顺序”:post
这个方法用于可迭代对象的迭代,用来遍历数组是有序的,而且迭代的是数组的值。该方法不会遍历非数字下标的元素,同时不会忽略数组的空元素:网站
let list = [7, 5, 2, 3] list[5] = 4 list[4] = 5 list[10] = 1 // 此时下标六、七、八、9为空元素 list['a'] = 'a' for (let value of list) { console.log(value) }
输出:this
> 7 > 5 > 2 > 3 > 5 > 4 > // 遍历空元素 > // 遍历空元素 > // 遍历空元素 > // 遍历空元素 > 1
该方法和方法2比较像,是有序的,不会忽略空元素。spa
let list = ['a', 'b', 'c', 'd'] list[4] = 'e' list[10] = 'z' list['a'] = 0 for (let idx = 0; idx < list.length; idx++) { console.log(idx, list[idx]) }
输出:
> 0, a > 1, b > 2, c > 3, d > 4, e > 5, //空元素 > 6, > 7, > 8, > 9, > 10, z
forEach是数组的一个高阶函数,用法以下:
arr.forEach(callback[, thisArg])
参数说明:
callback
为数组中每一个元素执行的函数,该函数接收三个参数:
数组中正在处理的当前元素。
数组中正在处理的当前元素的索引。
forEach() 方法正在操做的数组。
thisArg可选
可选参数。当执行回调函数时用做 this 的值(参考对象)。
forEach遍历数组会按照数组下标升序遍历,而且会忽略空元素:
let list = ['a', 'b', 'c', 'd'] list[4] = 'e' list[10] = 'z' list['a'] = 0 list.forEach((value, key, list) => { console.log(key, value) })
输出:
> 0, a > 1, b > 2, c > 3, d > 4, e > 10, z
有一个很容易忽略的细节,咱们都应该尽量地避免在遍历中取增删数组的元素,不然会出现一些意外的状况,而且不一样的遍历方法还会有不一样的表现。
好比for...of遍历中删除元素:
let list = ['a', 'b', 'c', 'd'] for (let item of list) { if (item === 'a') { list.splice(0, 1) } console.log(item) }
输出:
> a > c > d
forEach遍历中删除元素:
let list = ['a', 'b', 'c', 'd'] list.forEach((item, idx) => { if (item === 'a') { list.splice(0, 1) } console.log(item) })
输出:
> a > c > d
能够看到,两者表现一致,遍历到a的时候,把a删除,则b会被跳过,增长元素则略为不一样。
for...of遍历中增长元素:
let list = ['a', 'b', 'c', 'd'] for (let item of list) { if (item === 'a') { list.splice(1, 0, 'e') } console.log(item) }
输出:
> a > e > b > c > d
forEach遍历中增长元素:
let list = ['a', 'b', 'c', 'd'] list.forEach((item, idx) => { if (item === 'a') { list.splice(1, 0, 'e') } console.log(item) })
输出:
> a > e > b > c
咦,少了个'd'! 能够看到,其实forEach遍历次数在一开始就已肯定,因此最后的'd'没有输出出来,这是forEach和for遍历数组的一个区别,另外一个重要区别是forEach不可用break, continue, return等中断循环,而for则能够。
总之,在遍历数组过程当中,对数组的操做要很是当心,这一点python、js很类似,由于两门语言中,对象/字典和数组都是引用,都为可变对象。
上面介绍的4种算是比较标准的遍历方式,不过JS中数组还有不少的高阶函数,这些函数其实均可以达到遍历数组的目的,只不过每一个函数的应用场景不一样,下面简单介绍一下。
map() 方法参数与forEach彻底相同,两者区别仅仅在于map会将回调函数的返回值收集起来产生一个新数组。
好比将数组中每一个元素的2倍输出为一个新数组:
let list = [1, 2, 3, 4] let result = list.map((value, idx) => value * 2) console.log(result) // 输出[2,4,6,8]
filter() 参数与forEach彻底一致,不过它的callback函数应该返回一个真值或假值。filter() 方法建立一个新数组, 新数组包含全部使得callback返回值为真值(Truthy,与true有区别)的元素。
好比过滤数组中的偶数:
let list = [1, 2, 3, 4] let result = list.filter((value, idx) => value % 2 === 0) console.log(result) // 输出[2,4]
find() 方法返回数组中使callback返回值为Truthy的第一个元素的值,没有则返回undefined。使用很是简单,好比找出数组中第一个偶数:
let list = ['1', '2', '3', '4'] let result = list.find(value => value % 2 === 0) console.log(result) // 输出 2
findIndex()方法与find方法很相似,只不过findIndex返回使callback返回值为Truthy的第一个元素的索引,没有符合元素则返回-1。好比找出数组中第一个偶数的下标:
let list = [1, 2, 3, 4] let result = list.findIndex(value => value % 2 === 0) console.log(result) // 输出 1
两个函数接收参数都与以上函数相同,返回都是布尔值。every用于判断是否数组中每一项都使得callback返回值为Truthy,some用于判断是否至少存在一项使得callback元素返回值为Truthy。
let list = [1, 2, 3, 4] // 判断数组中是否每一个元素小于10 let result = list.every(value => { return value < 10 }) console.log(result) // 输出true // 判断是否每一个元素大于2 result = list.every(value => { return value > 2 }) console.log(result) // 输出false // 判断是数组中否存在1 result = list.some(value => { return value === 1 }) console.log(result) // 输出true // 判断数组中是否存在大于10的数 result = list.some(value => { return value > 10 }) console.log(result) // 输出false
参数与其它函数有所不一样:
callback
执行数组中每一个值的函数,包含四个参数:
累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。
数组中正在处理的元素。
数组中正在处理的当前元素的索引。 若是提供了initialValue,则起始索引号为0,不然为1。
调用reduce()的数组
initialValue可选
做为第一次调用 callback函数时的第一个参数的值。 若是没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。
reduce() 方法对数组中的每一个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值,而reduceRight只是遍历顺序相反而已。
好比很常见的一个需求是,把一个以下结构的list变成一个树形结构,使用forEach和reduce能够轻松实现。
列表结构:
let list = [ { id: 1, parentId: '' }, { id: 2, parentId: '' }, { id: 3, parentId: 1 }, { id: 4, parentId: 2, }, { id: 5, parentId: 3 }, { id: 6, parentId: 3 } ]
树形结构:
[ { "id":1, "parentId":"", "children":[ { "id":3, "parentId":1, "children":[ { "id":5, "parentId":3 }, { "id":6, "parentId":3 } ] } ] }, { "id":2, "parentId":"", "children":[ { "id":4, "parentId":2 } ] } ]
利用reduce和forEach实现list转为树形结构:
function listToTree(srcList) { let result = [] // reduce收集全部节点信息存放在对象中,能够用forEach改写,不过代码会多几行 let nodeInfo = list.reduce((data, node) => (data[node.id] = node, data), {}) // forEach给全部元素找妈妈 srcList.forEach(node => { if (!node.parentId) { result.push(node) return } let parent = nodeInfo[node.parentId] parent.children = parent.children || [] parent.children.push(node) }) return result }
以上即为本文围绕数组遍历介绍的数组基本操做。这些高阶函数其实均可以用于数组遍历(若是想强行遍历的话,好比some的callback恒返回false),不过实际使用中应该根据不一样的需求选用不一样的方法。
至此,面试中遇到“数组遍历有多少种方法?”这种问题,你能够回答“10种以上”了,毕竟,本文介绍了12种...
最后,JS实际上是一门特别愚蠢的语言,有时候你交给它的事情,它不会办不说,居然还会骂人!不信?控制台输入下面的算式试试:
(![]+{})[-~!+[]^-~[]]+([]+{})[-~!![]]
Just for fun. 别太认真~,~
本文原创,首发于个人我的网站:http://wintc.top/site/article?postId=8,转载请注明出处。