Fork me on GitHub

JavaScript数组和对象的遍历

遍历数组和对象是我们经常遇到的编程场景,JS有多种不同的方式和方法来遍历数组和对象,但每种方法深究起来又有各种差异,再加上对象的继承属性,不可枚举属性,Symbol属性等等,如果不熟悉这些方法的话,可能会造成隐藏较深的 bug

因此,本篇博客就对有关数组和对象的各种遍历方法,做一个总结。

首先需要说明的是,在JS中,数组可以视为恰巧拥有一些整数属性的对象。遍历对象的一些方法,都可以用来遍历数组。但通常对于数组和对象这两种不同用途的结构,我们在遍历它们时目的是不一样的,因此虽然有的方法可以用来遍历数组,但我们几乎不会拿来遍历数组,在下文中会有提及。

数组的遍历

数组可以视为一种特殊形式的对象,特殊之处在于,它有一些从0开始的整数的属性,也就是我们通常所说的索引。同时这个对象有一个length属性,它会根据索引数量的变化来动态改变其值。

对于数组遍历,我们通常是遍历它每个索引属性对应的值,而不会去遍历作为对象的它的其他属性,例如toString方法等。

关于数组的遍历,有以下几种方法。我们统一使用下面这个数组来举例。

JavaScript
1
var arr = ['yewen', 'huangfeihong', 'fangshiyu', 'huoyuanjai']

for

这是遍历数组时使用频率最高,也最简单的方式。

下面是普通的for循环。

JavaScript
1
2
3
4
for(var i = 0; i < arr.length,; i++){
console.log(i)
console.log(arr[i])
}

也有一些对for循环进行优化的小方法,例如缓存数组长度,使用局部的循环index等。如下

JavaScript
1
2
3
4
for(let i = 0,len = a.length; i < len; i++){
console.log(i)
console.log(arr[i])
}

for循环除了写起来麻烦一点,其实效率是相当不错的。

forEach( )

forEach是数组的方法。你可能要问了,上面说了数组也是一个普通的对象,那么数组的这些不同于其他对象的方法是哪里来的呢?别忘了JSArray类型,数组的一些独有的方法,就是继承自Array.prototype

forEach方法的使用如下:

JavaScript
1
2
3
4
arr.forEach(function(val, index, arr){
console.log(index)
console.log(val)
})

forEach方法接收一个函数作为参数。对每个元素,forEach都会使用当前元素,当前元素的索引,数组本身作为三个参数,调用传入的函数。(后两个参数如不需要,可省略)

同时需要注意,forEach方法一旦开始,是无法类似for循环一样,使用break终止的。

map( )

map方法,同样接受一个函数作为参数。此函数与forEach方法接受同样的三个函数参数。不同点在于,map对数组的每个元素调用此函数,并使用此函数的返回值作为数组项,组成一个新的数组返回。

JavaScript
1
2
3
4
5
arr.map(function(val, index, arr){
console.log(val)
return val + 'map'
})
//=> ['yewenmap', 'huangfeihongmap', 'fangshiyumap', 'huoyuanjaimap']

for…of

for...ofES6提供的新特性,用于遍历部署了Iterator接口的数据类型。例如MapSet等。因为数组类型也具有原生的Iterator接口,所以也可以使用for...of遍历。

JavaScript
1
2
3
for(var i of arr){
console.log(i)
}

for...of重复的将数组的值赋值变量i来供我们使用。

需要注意,此特性不支持IE。另外,字符串类型String也部署了Iterator接口,也可以通过for...of来遍历。

以上就是关于数组遍历的几种方法。另外,数组也有一些特别的方法,例如filtersomeeveryfindreduce等,但这些方法主要的目的并不是完全为了单纯的遍历数组,因此在此不多做介绍。

keys()

在ES6中,加入了数组的keys()values(), entries()三个方法,它们都返回一个迭代器,可以使用for...of来迭代。

keys()返回由数组的索引组成的迭代器。

values()返回由数组的值组成的迭代器。(此方法在chromefirefox中均未实现)

entries()返回一个由包含数组索引和值的数组的迭代器。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(var i of arr.keys()){
console.log(i)
}
//0
//1
//2
//3

for(var item in arr.entries()){
console.log(item)
}
//[0,'yewen']
//[1,'huangfeihong‘]
//[2,'fangshiyu']
//[3,'huoyuanjia']

遍历对象

对象的属性,有继承属性和自有属性之分,又有可枚举属性和不可枚举属性之分。

所谓继承属性,就是指对象从原型对象继承而来的属性。例如我们上文提到的数组的map等方法继承自Array.prototypemap就是继承属性。

所谓可枚举属性,是指在ES5之后,对象的属性有了以下可以设置的四个特性:

  • value

    属性的值

  • 可读性 writeable

    决定属性是否可读取

  • 可配置性 configurable

    决定属性是否可删除,可修改

  • 可枚举性 enumerable

    决定属性是否可以通过for...in循环返回

ES3中,对象属性默认就是可写,可配置,可枚举且无法修改的。ES5之后,我们可以通过Object.defineProperty()方法来定义对象的这些特性。

另外,在ES6中,引入了Symbol数据类型。因此对象存在特殊的一种情况,即使用Symbol值作为属性名。

对于上面提到的几种属性的区分和特殊情况,各个遍历属性的方法会有不同表现,具体的我们在每个方法中详细介绍。

下面来看遍历对象属性的常用方法。

for…in

for...in会遍历对象的所有可枚举属性,因此,只要对象的属性是可枚举的,它就会被遍历到,无论此属性是继承自原型还是对象自有。

JavaScript
1
2
3
4
5
6
7
8
9
10
var obj = {a: 1, b: 2, c: 3}

for(var prop in obj){
console.log(prop)
}

//=>
//a
//b
//c

我们来通过下面这个例子来演示for...in遍历的特点。

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//使childObj继承自obj
var childObj = Object.create(obj)

//为childObj定义一个不可迭代属性d(使用defineProperty定义的属性默认enumerable其实就是false)
Object.defineProperty(childObj, 'd', {
value: 4,
enumerable: false
})

childObj.e = 5

for(var i in childObj){
console.log(i)
}

//=>
//a
//b
//c
//e

可以看出,for...in遍历了childObj的继承属性a,b,c和自有属性e,但并没有遍历不可迭代属性d

for...in不被建议用来遍历数组,使用它来遍历数组,主要有以下两点问题:

  • for...in不光会遍历到数组的索引值,如果数组有其他定义或继承的非整数可迭代属性,它一样会遍历到。
  • for...in的遍历顺序是取决于当前的执行环境的,它并不一定会按照索引顺序来遍历数组的索引。

Object.getOwnPropertypeNames()

Object.getOwnPropertypeNames()方法,返回一个由对象的所有自有属性的属性名组成的数组。

我们使用上面定义的childObj来看一下Object.getOwnPropertypeNames()的表现。

JavaScript
1
2
3
4
5
6
var propArr = Object.getOwnPropertypeNames(childObj)

console.log(propArr)

//=>
//["d", "e"]

我们可以看到,Object.getOwnPropertypeNames()并没有遍历childObj的继承属性a,b,c,而只是遍历了它自有的不可枚举属性d和可枚举属性e**

Object.keys()

Object.keys()方法,返回一个由对象的所有可枚举的自有属性的属性名组成的数组。

JavaScript
1
2
3
4
5
6
var propArr = Object.keys(childObj)

console.log(propArr)

//=>
//["e"]

从上面的代码可以看出,Object.keys()只会遍历到对象自有的且可枚举的属性,不会遍历到继承的属性和不可枚举的属性。

另外,ES6中添加了两个对象的新静态方法,Object.values()Object.entries(),用于对应Object.keys()的自有可枚举,分别返回对象的相应所有键值组成的数组和对象的相应键值键名数组组成的数组。

Object.getOwnPropertypeSymbols

此方法用于遍历对象所有属性名为Symbol值的属性,返回一个由所有Symbol值属性组成的数组。

我们上文提到的三个遍历对象的方法,for...inObject.getOwnPropertypeNames()Object.keys()都是不会遍历到对象的symbol值属性的。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
var foo = Symbol('foo')
var bar = Symbol('bar')

childObj[foo] = 666
childObj[bar] = 888

var symbolArr = Object.getOwnPropertypeSymbols(childObj)
console.log(symbolArr)

//=>
//[symbol(foo),symbol(bar)]

Reflect.ownKeys()

Reflect,是ES6中新加入的一个内置对象,因为ES6中提供了proxy来代理对象的默认行为,所以提供了Reflect来保证在代理时也可以使用对象的原始默认行为。

Reflect.ownKeys()Object.getOwnPropertypeNames()行为基本相同,都返回一个由对象的所有自有属性的属性名组成的数组。唯一不同的是,它还会包括对象的Symbol值属性。

JavaScript
1
2
3
4
5
6
var reflectKeys = Reflect.ownKeys(childObj)

console.log(reflectKeys)

//=>
//["d", "e", symbol(foo), symbol(bar)]

可以认为,Reflect.ownKeys(target) 等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。

关于遍历对象的方法的介绍就到这里,通过下面这个表格,对这些方法进行一个总结。

自有属性继承属性可枚举属性不可枚举属性Symbol属性
for...in
Object.getOwnPropertypeNames()
Object.keys()
Object.getOwnPropertypeSymbols()
Reflect.ownKeys()

✔ 表示只会遍历此种类型属性

✘ 表示不会遍历此种类型属性

----本文结束感谢阅读----