JS数组基本操作——数组遍历到底有多少种方式?

原文链接:http://wintc.top/site/article?postId=8

对于"数组遍历"这个问题,其实答案很宽泛,关键在于你能不能列举出一定数量的方法以及描述它们之间的区别。本文即介绍一下数组的基本遍历操作和高阶函数。

一、数组基本遍历

本部分介绍4种最常用的遍历方式。

1.for...in

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文档里也明确建议“不要依赖其遍历顺序”:


image.png

2.for...of

这个方法用于可迭代对象的迭代,用来遍历数组是有序的,并且迭代的是数组的值。该方法不会遍历非数字下标的元素,同时不会忽略数组的空元素:

let list = [7, 5, 2, 3]
list[5] = 4
list[4] = 5
list[10] = 1
// 此时下标6、7、8、9为空元素
list['a'] = 'a'

for (let value of list) {
  console.log(value)
}

输出:

> 7
> 5
> 2
> 3
> 5
> 4
>   // 遍历空元素
>  // 遍历空元素
>  // 遍历空元素
>  // 遍历空元素
> 1

3.取数组长度进行遍历

该方法和方法2比较像,是有序的,不会忽略空元素。

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

4.forEach遍历

forEach是数组的一个高阶函数,用法如下:

arr.forEach(callback[, thisArg])

参数说明:

callback
为数组中每个元素执行的函数,该函数接收三个参数:

  • currentValue

数组中正在处理的当前元素。

  • index 可选

数组中正在处理的当前元素的索引。

  • array 可选

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和forEach遍历中删除元素

比如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和forEach遍历中增加元素

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中数组还有很多的高阶函数,这些函数其实都可以达到遍历数组的目的,只不过每个函数的应用场景不同,下面简单介绍一下。

1. map

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]

2.filter

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]

3. find/findIndex

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

4.every/some

两个函数接收参数都与以上函数相同,返回都是布尔值。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

5.reduce/reduceRight 累加器

参数与其它函数有所不同:
callback
执行数组中每个值的函数,包含四个参数:

  • accumulator

累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。

  • currentValue

数组中正在处理的元素。

  • currentIndex 可选

数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则为1。

  • array 可选

调用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种...


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容