阅读 1161

JavaScript遍历方法总结与对比

引言

首先需要知道对于数组和可迭代对象的遍历方法,我们需要从不同的维度进行对比,方法的功能性,方法的应用场景,方法的兼容性,方法的效率,方法的返回值以及是否改变原始数组。深层次的我们可以思考如何实现这些方法,并且考虑到低版本浏览器的兼容性。如果要分group的话,可以这么分:forEach()与map()every()与some()filter()与find()findIndex(),keys()、values()与entries(),reduce()与reduceRight()。当然对于的常规的遍历方法,比如forwhiledo-while,$.each,$(selector).each这里就不赘述了,原生的方法当然是主角,但也要看应用场景,项目中使用新增的遍历方法确实能提高代码效率,jquery的方法当然也有借鉴的地方,但是在现在的前端项目中用到的较少。

forEach ()

指定数组的每一项元素都执行了一次传入的函数,返回值为undefined,thisArg可选,用来当做fn函数内的this对象;forEach不能在低版本IE(6~8)中使用,兼容写法请参考 Polyfill。得益于鸭式辨型,虽然forEach不能直接遍历对象,但它可以通过call方式遍历类数组对象

var o = {0:1, 1:3, 2:5, length:3};
Array.prototype.forEach.call(o,function(value, index, obj){
  console.log(value,index,obj);
  obj[index] = value * value;
},o);
// 1 0 Object {0: 1, 1: 3, 2: 5, length: 3}
// 3 1 Object {0: 1, 1: 3, 2: 5, length: 3}
// 5 2 Object {0: 1, 1: 9, 2: 5, length: 3}
console.log(o); // Object {0: 1, 1: 9, 2: 25, length: 3}
复制代码

可以说这么多遍历函数中,forEach()要注意的地方最多,有以下要注意的:

  • this都是指向调用方法的数组;

    var array = [1, 3, 5];
    var obj = {name:'cc'};
    var sReturn = array.forEach(function(value, index, array){
      array[index] = value * value;
      console.log(this.name); // cc被打印了三次
    },obj);
    console.log(array); // [1, 9, 25], 可见原数组改变了
    console.log(sReturn); // undefined, 可见返回值为undefined
    复制代码
  • 没有返回值,它总是返回undefined值,即使你return了一个值

    var solt = arr1.forEach((v,i,t) => {
      console.log(v)
    })
    
    console.log(solt)	// undefined
    复制代码
  • 不能 中途退出循环,不能用break,会报错的,只能用return退出本次回调,进行下一次回调

    var arr1 = [1, 2, 3, 4, 5]
    
    // 使用break会报错
    arr1.forEach((v,i,arr) => {
      console.log(v)
      if(v === 3) {
        break
      }
    })
    //SyntaxError: Illegal break statement
    
    
    
    // return false 也无效
    arr1.forEach((v,i,arr) => {
      console.log(v)
      if(v === 3) {
        return false
        console.log('-----')//不会执行
      }
    })
    /*
    1
    2
    3
    4
    5
    */
    复制代码
  • 使用箭头函数,thisArg 参数会被忽略

    var arr1 = [1, 2, 3]
    var arr2 = [7, 8, 9]
    
    arr1.forEach((v, i, arr) => {
      console.log(this)
    })
    // window
    // window
    // window
    
    arr1.forEach((v, i, arr) => {
      console.log(this)
    }, arr2)
    // window
    // window
    // window
    复制代码

map()

map() 方法遍历数组,使用传入函数处理每个元素,并返回函数的返回值组成的新数组。其在低版本IE(6~8)的兼容写法请参考 Polyfill

every()与some()

every() 方法使用传入的函数测试所有元素,只要其中有一个函数返回值为 false,那么该方法的结果为 false;如果全部返回 true,那么该方法的结果才为 true;every 一样不能在低版本IE(6~8)中使用,兼容写法请参考 Polyfill

以下是鸭式辨型的写法:

var o = {0:10, 1:8, 2:25, length:3};
var bool = Array.prototype.every.call(o,function(value, index, obj){
  return value >= 8;
},o);
console.log(bool); // true
复制代码

some()方法 测试数组元素时,只要有一个函数返回值为 true,则该方法返回 true,若全部返回 false,则该方法返回 false;some 的鸭式辨型写法可以参照every,同样它也不能在低版本IE(6~8)中使用,兼容写法请参考 Polyfill

filter()

filter() 方法使用传入的函数测试所有元素,并返回所有通过测试的元素组成的新数组。它就好比一个过滤器,筛掉不符合条件的元素。filter一样支持鸭式辨型,具体请参考every方法鸭式辨型写法。其在低版本IE(6~8)的兼容写法请参考 Polyfill

var array = [18, 9, 10, 35, 80];
var array2 = array.filter(function(value, index, array){
  return value > 20;
});
console.log(array2); // [35, 80]
复制代码

for-in 语句

一般会使用for-in来遍历对象的属性的,不过属性需要 enumerable,才能被读取到. for-in 循环只遍历可枚举属性。一般常用来遍历对象,包括非整数类型的名称和继承的那些原型链上面的属性也能被遍历。像 Array和 Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性就不能遍历了

var obj = {
    name: 'test',
    color: 'red',
    day: 'sunday',
    number: 5
}
for (var key in obj) {
    console.log(obj[key])
}
复制代码

for-of 语句

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

var arr = [{name:'bb'},5,'test']
for (item of arr) {
    console.log(item)
}
复制代码

find()与findIndex()

find() 方法基于ECMAScript 2015(ES6)规范,返回数组中第一个满足条件的元素(如果有的话), 如果没有,则返回undefined。

findIndex() 方法也基于ECMAScript 2015(ES6)规范,它返回数组中第一个满足条件的元素的索引(如果有的话)否则返回-1。

var array = [1, 3, 5, 7, 8, 9, 10];
function f(value, index, array){
  return value%2==0; // 返回偶数
}
function f2(value, index, array){
  return value > 20; // 返回大于20的数
}
console.log(array.find(f)); // 8
console.log(array.find(f2)); // undefined
console.log(array.findIndex(f)); // 4
console.log(array.findIndex(f2)); // -1
复制代码

includes()

判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回false。

var array1 = [1, 2, 3];

console.log(array1.includes(2));
// expected output: true

var pets = ['cat', 'dog', 'bat'];

console.log(pets.includes('cat'));
// expected output: true

console.log(pets.includes('at'));
// expected output: false
复制代码

reduce()与reduceRight()

reduce() 方法接收一个方法作为累加器,数组中的每个值(从左至右) 开始合并,最终为一个值;initialValue 指定第一次调用 fn 的第一个参数。;当 fn 第一次执行时:

  • 如果 initialValue 在调用 reduce 时被提供,那么第一个 previousValue 将等于 initialValue,此时 item 等于数组中的第一个值;
  • 如果 initialValue 未被提供,那么 previousVaule 等于数组中的第一个值,item 等于数组中的第二个值。此时如果数组为空,那么将抛出 TypeError。
  • 如果数组仅有一个元素,并且没有提供 initialValue,或提供了 initialValue 但数组为空,那么fn不会被执行,数组的唯一值将被返回。
var array = [1, 2, 3, 4];
var s = array.reduce(function(previousValue, value, index, array){
  return previousValue * value;
},1);
console.log(s); // 24
// ES6写法更加简洁
array.reduce((p, v) => p * v); // 24
复制代码

reduceRight() 方法接收一个方法作为累加器,数组中的每个值(从右至左)开始合并,最终为一个值。除了与reduce执行方向相反外,其他完全与其一致.

entries()

entries() 方法基于ECMAScript 2015(ES6)规范,返回一个数组迭代器对象,该对象包含数组中每个索引的键值对。

var array = ["a", "b", "c"];
var iterator = array.entries();
console.log(iterator.next().value); // [0, "a"]
console.log(iterator.next().value); // [1, "b"]
console.log(iterator.next().value); // [2, "c"]
console.log(iterator.next().value); // undefined, 迭代器处于数组末尾时, 再迭代就会返回undefined
复制代码

entries 也受益于鸭式辨型,如下:

var o = {0:"a", 1:"b", 2:"c", length:3};
var iterator = Array.prototype.entries.call(o);
console.log(iterator.next().value); // [0, "a"]
console.log(iterator.next().value); // [1, "b"]
console.log(iterator.next().value); // [2, "c"]
复制代码

keys()

keys() 方法基于ECMAScript 2015(ES6)规范,返回一个数组索引的迭代器。(浏览器实际实现可能会有调整)

var array = ["abc", "xyz"];
var iterator = array.keys();
console.log(iterator.next()); // Object {value: 0, done: false}
console.log(iterator.next()); // Object {value: 1, done: false}
console.log(iterator.next()); // Object {value: undefined, done: false}
复制代码

要注意的是索引迭代器会包含那些没有对应元素的索引

var array = ["abc", , "xyz"];
var sparseKeys = Object.keys(array);
var denseKeys = [...array.keys()];
console.log(sparseKeys); // ["0", "2"]
console.log(denseKeys);  // [0, 1, 2]
复制代码

values()

values() 方法基于ECMAScript 2015(ES6)规范,返回一个数组迭代器对象,该对象包含数组中每个索引的值。其用法基本与上述 entries 方法一致。兼容性方面,目前没有浏览器实现了该方法

var array = ["abc", "xyz"];
var iterator = array.values();
console.log(iterator.next().value);//abc
console.log(iterator.next().value);//xyz
复制代码

Symbol.iterator()

该方法基于ECMAScript 2015(ES6)规范,同 values 方法功能相同。

var array = ["abc", "xyz"];
var iterator = array[Symbol.iterator]();
console.log(iterator.next().value); // abc
console.log(iterator.next().value); // xyz      
复制代码

总结

数组遍历的方法中

  • 返回值有的是数组,有的是一个值,有的是布尔值,有的是一个数组迭代器对象;

  • every,some是用来判断的,返回一个布尔值;filter,find,findIndex都是用来筛选的,并且返回满足条件的元素或元素组成的数组,不同的是,filter返回所有满足条件的数组成的数组,find与findIndex返回第一个满足条件的元素或元素的索引;

  • Array.prototype的所有方法均具有鸭式辨型这种神奇的特性,它们不仅可以用来处理数组对象,还可以处理类数组对象;

  • ES6中的新增方法应用对象更广泛,可以用于所有的具有Iterator接口的可迭代对象,比如数组,类数组对象,Map和Set结构;

  • 对于各种遍历方法的效率问题,这篇文章(详解JS遍历)对JS中遍历方法进行了测试比较,得出的结论就是JavaScript原生的方法的效率高于各种封装的方法;

  • forEach()与map()的不同点:

    • map()创建了新数组,不改变原数组;forEach()可以改变原数组。

    • 遇到空缺的时候map()虽然会跳过,但保留空缺;forEach()遍历时跳过空缺,不保留空缺。

    • map()按照原始数组元素顺序依次处理元素;forEach()遍历数组的每个元素,将元素传给回调函数。

    forEach()为数组中的每个元素添加属性

    var arr = [
        {name : 'kiwi', age : 12},
        {name : 'sasa', age : 22},
        {name : 'alice', age : 32},
        {name : 'joe', age : 42}
    ]
    arr.forEach(function(ele, index){
        if(index > 2){
            ele.sex = 'boy';
        }else{
            ele.sex = 'girl';
        }
        return arr1
    })
    console.log(arr)//元素组发生改变
    //[{name: "kiwi", age: 12, sex: "girl"},{name: "sasa", age: 22, sex: "girl"},{name: "alice", age: 32, sex: "gi
    复制代码

    ** 遇到空缺比较**

    ['a', , 'b'].forEach(function(ele,index){
        console.log(ele + '. ' + index);
    })
    //0.a
    //2.b
    ['a', , 'b'].map(function(ele,index){
        console.log(ele + '. ' + index);
    })
    //['0.a', , '2.b']
    复制代码
  • for-of与for-in的不同点

    • for...of循环不会循环对象的key,只会循环出数组的value,因此for...of不能循环遍历普通对象,对普通对象的属性遍历推荐使用for...in。如果实在想用for...of来遍历普通对象的属性的话,可以通过和Object.keys()搭配使用,先获取对象的所有key的数组然后遍历:
    var student={
        name:'wujunchuan',
        age:22,
        locate:{
        country:'china',
        city:'xiamen',
        school:'XMUT'
        }
    }
    for(var key of Object.keys(student)){
        //使用Object.keys()方法获取对象key的数组
        console.log(key+": "+student[key]);
    }
    /*
    //如果是下面这样,会报错提示 “TypeError: student is not iterable”
    for(var key of student){
        //使用Object.keys()方法获取对象key的数组
        console.log(key+": "+student[key]);
    }
    */
    复制代码
    • for...in循环出的是key,for...of循环出的是value;推荐在循环对象属性的时候,使用for...in,在遍历数组的时候的时候使用for...of。注意,for...of是ES6新引入的特性。修复了ES5引入的for...in的不足

参考:

  1. 深度长文】JavaScript数组所有API全解密
  2. 【干货】js 数组详细操作方法及解析合集
  3. 详解JS遍历
关注下面的标签,发现更多相似文章
评论