聊聊JS中的遍历

遍历算法在我们日常编程过程中算是使用得最多的了。这里我们就简单的聊聊,在JS中关于各种基本数据结构的遍历方式以及其相关的坑。

字符串的遍历

  • for循环

在以前的JS中,并没有直接遍历字符串的函数,我们遍历一个字符串的话,需要这样做:

1
2
3
4
5
6
7

var str = 'Hello Sa';
for (i = 0; i < str.length; i++) {
console.log(str.charAt(i));
}

// -> Hello Sa
  • for in遍历

取到字符串的长度,然后通过for循环,用charAt函数来定位到具体的位置。同样的方式还有for in :

1
2
3
4
5
6
7

var str = 'Hello Sa';
for (var i in str) {
console.log(str.charAt(i));
}

// -> Hello Sa

但是这种方式虽然可行,可是又用了遍历索引,又用了字符串方法,感觉不够优雅。

既然说到了for in方法,那么就说说for in中得注意得地方: 在for(var i in str) 语句中,你所使用的i和for(var i = 0; i<str.length,i++)中的i是不一样的。前者是字符串类型,后者是number类型,这就需要我们引起重视,有时候我们会要做索引与变量的求和操作,这个时候如果索引是字符串类型就会引起一些很小很隐蔽的错误。

  • for of遍历

于是在ES6中,最最万金油的遍历方法诞生:

1
2
3
4
5
6
7

var str = 'Hello Sa';
for (var i of str) {
console.log(i);
}

// -> Hello Sa

看看关于MDN中对for of 方法的描述:

for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

for of可以遍历所有的基本数据类型,但是却无法遍历一般的对象。

1
2
3
4
5
6
var obj = {"name":"sa","age":25}
for (var i of obj) {
console.log(i);
}

// -> error

对于这样的对象,我们还是统一用for in吧。

数组的遍历

  • for 循环

看到遍历数组,大家最先想到的肯定是for循环:

1
2
3
4
5
6
7
8
9

var myArray = ["sa","25","changsha"];
for(var i = 0; i < myArray.length; i ++){
console.log(i);
console.log(myArray[i]);
}

// -> 0 1 2
// -> sa 25 changsha
  • while 循环

完全没有问题,这是最最common的遍历方式,但是也有人用while:

1
2
3
4
5
6
7
8
9

var myArray = ["sa","25","changsha"];
var i = 0;
while(i<myArray.length){
console.log(myArray[i]);
i++;
}

// -> sa 25 changsha

要是有人在我的代码里这样用while写遍历,我百分之一百会打死他,除非他是我头儿,哈哈。

while在js里来说还是需要非常注意以及小心使用的循环方法,如果稍有不慎,在跳出条件上没有做好控制,就能秒秒钟让你的web页面崩溃,卡到动不了,而且,更厉害的是,如果你用了很多这样的遍历方法,你甚至连错误都很难找到,控制台经常内存泄漏死掉连错误信息都没有,所以,上面的while遍历方法,在我看来,是很不安全的,尽量少用。

  • forEach 循环

forEach是ES5中发布的数组循环的方法,我们来看看:

1
2
3
4
5
6
7

var myArray = ["sa","25","changsha"];
myArray.forEach(function(value,index){
console.log(value,index);
});

// -> sa 0 25 1 changsha 2

看上去还是挺不错的,能成功的将值和索引都遍历出来,但是也有致命的缺陷。那就是forEach循环中无法使用跳出循环的语句,如:break。这样就导致forEach的使用场景被大大的限制了。

  • for in 循环

当然,遍历数组也是可以用到for in的:

1
2
3
4
5
6
7

var myArray = ["sa","25","changsha"];
for (var i in myArray){
console.log(myArray[i]);
}

// -> sa 25 changsha

完全没有问题,for in要小心的地方我已经在字符串遍历的时候说了,上述代码中的i都是字符串。

  • map 和 filter 遍历

这两个方法多是用在对数组元素的操作上,但是有很多同学可能都没有使用过。这里我们先看看MDN上关于两者的介绍:

map:

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

filter:

filter 为数组中的每个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 或 等价于 true 的值 的元素创建一个新数组。callback 只会在已经赋值的索引上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中。

其实这两个函数在使用的方法上还是挺相似的,都是通过遍历数组中的没个元素,然后对其执行相应的函数,然后将结果返回到一个新的数组中去。

我们来看看用这种方式的遍历:

map:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

var myArray = ["sa","25","changsha"];
var result = myArray.map(function(value){
console.log(value);
return "hello! " + value
});
console.log(myArray);
console.log(result);

// -> sa
// -> 25
// -> changsha
// -> [ 'sa', '25', 'changsha' ]
// -> [ 'hello! sa', 'hello! 25', 'hello! changsha' ]

我们在map函数中不仅可以遍历每个数组对象,还能对其操作返回新的数组,而且新数组与原来得数组相互不影响。这是其很棒得一个特性,解决了一个数组浅拷贝的问题。

filter方法的使用也是类似的,只是在返回新数组的时候filter有一个判别条件,用来返回符合要求的结果。也算是如其名吧。

filter:

1
2
3
4
5
6
7
8
9
10

var myArray = [1,2,3,4,5,6];

var result = myArray2.filter(function(value){
return value > 4;
});

console.log(me);

// -> [5,6]
  • for of

最后放上来的是ES6中的for of,除了对象类型,什么都可以遍历的万金油方法。

1
2
3
4
5
6
7

var myArray = ["sa","25","changsha"];
for(var item of myArray){
console.log(item);
}

// -> sa 25 changsha

JS对象的遍历

  • for in
1
2
3
4
5
6

var obj = {"name":"sa","age":"25","location":"changsha"};

for(var i in obj){
console.log(obj[i]);
}

首先列出来的是最最常用的for in循环。前面已经做过了介绍,这里就不再重复了。

  • Object.keys和 forEach的组合方式
1
2
3
4
5

var obj = {"name":"sa","age":"25","location":"changsha"};
Object.keys(obj).forEach(function(value,index){
console.log(value,index);
});

这是通过Object.keys方法来获取对象的key数组,然后使用forEach遍历该数组,最后遍历出对象所有value的方法。类似的组合方式还有一些,我就不举例说明了,原理都和上述方式一致。

关于for循环中的闭包所引起的索引问题

1
2
3
4
5
6
7
8
9
10
11

var list = [];
for (var i = 0; i < 3; i++) {
list.push(function () {
console.log(i)
});
}

console.log(list[0](), list[1](), list[2]());

// -> 3 3 3

很多人预期的结果是0 1 2 ,但是最后的结果是3 3 3。

造成这个结果的原因是因为i的变量升级,当我们在for循环的时候执行list.push时,此时的函数只是一个声明,并没有将i的值确定,而在我们执行list0这样的函数时,程序才会开始去查找这个i的值,而此时的for循环早已经结束了,i从for循环中块级变量提升到了全局变量(js中没有块级作用域),最后停到了3的位置,于是,不论是list[0],还是list[1],list[2]所能取到的i都只有最后一个3(不要问我为什么是3不是2)。当然,对这个问题,你只要这样理解就可以了,至于更底层的原因,如果你有兴趣就自己去网上搜一搜,我就不展开讲了。

解决的办法:

1
2
3
4
5
6
7
8
9
10
11
12
13

var list = [];
for (var i = 0; i < 3; i++) {

(function (i) {
return list.push(function () {
console.log(i)
});
})(i)

}

console.log(list[0](), list[1](), list[2]());

为什么加了一个闭包就可以了呢?

因为这里的闭包是一个立即执行的函数,每一次的运行,i作为参数传进去,函数中的i值立马就确定了,不需要等到最后调用的时候才去查找这个i的值(确定的 0 1 2)。

不过在ES6中有了更棒的解决办法。由于ES6引入了新的声明关键字let,而let又是属于块级作用域的,所以

1
2
3
4
5
6
7
8
9

var list = [];
for (let i = 0; i < 3; i++) {
return list.push(function () {
console.log(i)
});
}

console.log(list[0](), list[1](), list[2]());

当执行list[0]()时,i的值不会再从全局作用域里取到了,而是从存在的块级作用域中正确拿到。所以,在for循环中使用let时,不需要引入闭包也能解决变量升级的问题了。

结尾

ok,关于js的遍历就讲到这里,关于遍历中的坑我也简单的提了一下,希望能对不了解的同学有所帮助。