Skip to content

遍历数组方法的区别

一、ES5数组遍历

  • forEach、map、filter、some、every传递的参数有:

    🍪 currentValue:必须,当前元素。

    🍪 index:可选。当前元素的索引值。

    🍪 arr:可选、当前元素所属的数组对象。

    🍪 thisValue:可选。传递给函数的值一般用this值,如果这个参数为空,undefined会传递给this值。

js
  // 比如:forEach:
  arr.forEach((currentValue,index,arr)=>{},thisValue)
  • reduce、reduceRight传递的参数有:

    🍪 total:上一次调用回调返回的值,或者提供的初始值。

    🍪 currentValue:必须,当前元素。

    🍪 index:可选。当前元素的索引值。

    🍪 arr:可选、当前元素所属的数组对象。

    🍪 initialValue:表示传递给函数的初始值(作为第一次调用callback的第一个参数)

js
// 比如:reduce:
arr.reduce((total,currentValue,index,arr)=>{},initialValue)

1.1 for

  • 基本实现:
js
const arr = [0, 1, 3, 2]

for (let i = 0; i < arr.length; i++) {
	console.log(arr[i]);
}
console.log(i)// undefined
js
const arr = [0, 1, 3, 2]

for (var i = 0; i < arr.length; i++) {
	console.log(arr[i]);
}
console.log(i)// 4

上面的两个遍历有啥区别?细心的你就会发现,上面两个遍历就是let与var的区别。非常叻仔的你就会发现这是作用域的问题(块级作用域与全局作用域)。

🍪 块级作用域:let声明的变量只能在当前作用域内有效,也就是上面的for代码块里有效,所以在for作用外调用的变量是没声明的。

🍪 全局作用域:var定义的i是for函数的全局变量,每次递增不会重新声明,由于每次递增没有发现i声明,但是for的全局存在i,此时会通过作用域链去找i,找到的时候for循环已经执行完毕,退出循环了。

  • for性能消耗

    🍪 普通版for循环

    js
    let data = new Array(100000).fill(0);
    // 开始for这个计时器的计时
    console.time('for');
    for (let i = 0; i < data.length; i++) {
    }
    // 结束for这个计时器的计时
    console.timeEnd('for');
    // 2.5ms

    🍪 优化版循环

    js
    console.time('for');
    for (let i = 0, len = data.length; i < len; i++) {
    }
    console.timeEnd('for');
    // 2.17ms

1.2 forEach

  • 从Array.forEach源码角度去理解forEach

    js
    function myForEach(arr, callback) {
      let T, k;
      if (arr === null) {
        throw new TypeError('this is null or not defined');
      }
      // 用于处理若传入的arr为非数组的情况
      const obj = Object(arr);
      const len = obj.length >>> 0;// 无符号右移:十进制转化为二进制
      if (typeof callback !== 'function') {
        throw new TypeError(`${callback} is not a function`);
      }
      // 保证函数参数不止一个
      if (arguments.length > 1) {
        T = callback;
      }
      k = 0;
      while (k < len) {
        // 如果指定属性在指定的对象或其原型链中,则in运算符返回true
        // 用于过滤未初始化值
        if (k in obj) {
          const kValue = obj[k];
          // kValue, k, obj对应着forEach回调函数3个参数,数组当前项、当前索引、数组对象本身
          // call:将callback的this指向其自己的内部
          callback.call(T, kValue, k, obj);
        }
        k++;
      }
      return undefined;
    }
    const arr = [1, 2, 3];
    myForEach(arr, (item) => {
      console.log(item);
    })
  • forEach特点

    🍪 forEach方法不会改变原数组,也没有返回值。

    🍪 forEach无法使用break,continue跳出循环,使用return时,效果和在for循环中使用continue一致。

    🍪 forEach方法无法遍历对象仅仅适用于数组的遍历。

    🍪 forEach() 对于空数组是不会执行回调函数的

  • forEach的基本实现

    js
    arr.forEach((currentValue,index,arr)=>{},thisValue)
    
    const arr = [2, 3, 5, 1, 6];
    arr.forEach((item, index, array) => {
    // array也就是arr
        console.log(index, item, array);
    })
  • forEach性能消耗

    js
    console.time('forEach');
    data.forEach(item => {
    });
    console.timeEnd('forEach');
    // 9.18ms

1.3 map

  • 从Array.map源码角度去理解

    js
    Array.prototype.myMap = function (callback) {
      var T, A, k;
      if (this == null) {
        throw new TypeError('this is null or not defined');
      }
      var O = Object(this);
      var len = O.length >>> 0;
      if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
      }
      if (arguments.length > 1) {
        T = arguments[1];
      }
      // 创建数组对象
      A = new Array(len);
      k = 0;
      while (k < len) {
        var kValue, mappedValue;
        if (k in O) {
          mappedValue = callback.call(T, kValue, k, O);
          A[k] = mappedValue;
        }
        k++;
      }
      return A;
    };
    
    const arrMap = [
      { name: '孙悟空', sex: 0, org: '' },
      { name: '猪八戒', sex: 0, org: '' },
      { name: '唐僧', sex: 0, org: '' },
      { name: '沙僧', sex: 0, org: '' }];
    arrMap.myMap((item) => {
      console.log(item);
      return item;
    });
  • map的特点

    🍪 map方法不会对空数组进行检测。

    🍪 map方法遍历数组是返回一个新数组,不会改变原数组

    🍪 map方法有返回值,可以return出来,map的回调函数中支持return返回值。

    🍪 map方法无法遍历对象,仅适用于数组的遍历。

  • map的基本实现

    js
    let arr = [2, 3, 5, 1, 6];
    let newArr = arr.map((item, index, array) => item = item * 2);
    console.log(arr, newArr);
    //  [2, 3, 5, 1, 6],[4, 6, 10, 2, 12]
  • map性能消耗

    js
    console.time('map');
    data.map(item => {
    });
    console.timeEnd('map');
    // 20.6ms

1.4 filter

  • filter的特点

    🍪 filter方法会返回一个新的数组,不会改变原数组。

    🍪 filter方法不会对空数组进行检测。

    🍪 filter方法适用于检测数组。

  • filter的基本实现

    js
    let arr = [2, 3, 5, 1, 6];
    let newArr = arr.filter((item, index, array) => item>3);
    console.log(arr, newArr);
    //  [2, 3, 5, 1, 6],[5,6]
  • filter的特质

    🍪 特别地,filter方法可以用来移除数组中的undefined、null、NAN等值

    js
    let arr = [1, undefined, 2, null, 3, false, '', 4, 0];
    let newArr = arr.filter(Boolean);
    console.log(newArr);// [1,2,3,4]
  • filter性能消耗

    js
    console.time('filter');
    data.filter(item => item < 0);
    console.timeEnd('filter');
    // 16.2ms

1.5 some、every

  • 特点

    🍪两个方法都不会改变数组,会返回一个布尔值

    🍪两个方法都不会对空数组进行检测

    🍪两个方法都仅适用于检测数组

  • 基本实现

    🍪 some:数组中只要有一个元素符合判断条件的,返回true,否则false

    🍪 every:数组中只有每个元素都符合判断条件的就返回true,否则false

    js
    const arr = [1,2,45,-1,0];
    arr.some(item=>item<0);// true;
    arr.every(item=>item<0);// false;

1.6 reduce、reduceRight

  • 特点

    🍪 两个方法都不会改变数组。

    🍪 两个方法如果添加初始值,就会改变原数组,会将这个初始值放在数组的最后一位。

    🍪 两个方法对于空数组是不会执行回调函数的。

  • 基本实现

    🍪 reduce:正序遍历,接收一个函数作为累加器,数组中的每个值从左到右开始累加,最终计算为一个值。

    js
    let arr = [1, 2, 3, 4]
    let sum = arr.reduce((prev, cur, index, arr) => {
        return prev + cur
    }, 5)
    console.log(arr, sum);// [1,2,3,4] 15

    🍪 reduceRight:与reduce用法一样,但它逆序遍历,数组中每个值从右到左开始累加,最终计算为一个值。

1.7 for...in

  • 特点

    🍪 for...in方法不仅会遍历当前的对象所有的可枚举属性,还会遍历其原型链上的属性

  • 基本实现

    js
    const obj = { a:1,b:2,c:3}
    for(let i in obj){
      console.log('键名',i);
      console.log('键值',obj[i]);
    }
  • for in性能消耗

    js
    console.time('for in');
    for (item in data) {
    
    }
    console.timeEnd('for in');
    // 178ms

二、ES6遍历

2.1 find、findIndex

  • find:通过函数内判断的数组的第一个元素的值。

    js
    let arr = [2,1,3,5,4];
    arr.find(item=>item>2);// 3
  • findIndex:找到数组中符合条件的第一个元素位置。

    js
    let arr = [2,1,3,5,4];
    arr.findIndex(item=>item>2);// 索引为2

2.2 for...of

  • for of特点

    🍪 for of方法只会遍历当前对象的属性不会遍历其原型链上的属性

    🍪 for of方法适用遍历数组、类数组、字符串、map、set等有迭代器对象的集合。

    🍪 for of方法不支持遍历普通对象,因为其没有迭代器对象。如果想要遍历一个对象的属性,可以用for in

    🍪 可以使用break、continue、return来中断循环遍历。

    🍪 对于for of的循环,可以由break, continereturn终止,这种情况下,迭代器关闭。

  • for of基本实现

    js
    let arr = [    
            {id:1, value:'hello'},    
            {id:2, value:'world'},    
            {id:3, value:'JavaScript'}
    ]
    for (let item of arr) {  
            console.log(item)
    }
    // 输出结果:{id:1, value:'hello'}  {id:2, value:'world'} {id:3, value:'JavaScript'}
  • for of性能消耗

    js
    console.time('for of');
    for (item of data) {
    
    }
    console.timeEnd('for of');
    // 20ms

2.3 keys()、values()、entries()

  • keys() 返回数组的索引值(对象键名)。
  • values() 返回数组的元素(对象键值)。
  • entries() 返回数组的键值对。

Object.keys()方法返回的数组中的值都是字符串,也就是说不是字符串的key值会转化为字符串 结果数组中的属性值都是对象本身可枚举的属性,不包括继承来的属性。

js
const arr = [
            { name: '孙悟空', sex: 0, org: '' },
            { name: '猪八戒', sex: 0, org: '' },
            { name: '唐僧', sex: 0, org: '' },
            { name: '沙僧', sex: 0, org: '' }];
for (let item of arr.keys()) {
    console.log(item);// 0 1 2 3
}
for (let item of arr.values()) {
    console.log(item);// 整个数组对象
}
for (let item of arr.entries()) {
    console.log(item);// [数组每个对象的索引,数组中的每个对象]
}

面试官会问什么?

1.map与forEach本身能终止循环吗?

它们本身是不能终止循环,而是通过抛出new throw error()try...catch去捕获这个错误才可以终止循环。

js
let list=[1,2,3,4,5,6];
try{
  list.map(item=>{
     if(item===3){
          throw new Error()
         }
     console.log(item)
  })
} catch {}
// 1 2

es6对这个缺陷做了一个改进就是for of的出现。

2.详细说说for循环如何污染全局变量?

3.for...in和for...of有什么区别?

  • 遍历数组时

    • for...in获取到的是每一项的索引值。
    • for...of获取到的是每一项的值。
    js
    const arr = [
        {
            name: '孙悟空',
            age: 500,
            org: '花果山'
        }
    ]
    
    for (item in arr) {
        console.log(item)
    }
    // 0
    
    for (item of arr) {
        console.log(item) // {name: '孙悟空',age: 500,org: '花果山'}
    }
  • 遍历对象时

    • for...in获取到的是每一项的key值。
    • for...of不能用于遍历对象,会报错,对象数组可以。
    js
    const obj = {
        name: '孙悟空',
        age: 500,
        org: '花果山'
    };
    for (item in obj) {
        console.log(item)
    }
    // name age org
    
    for (item of obj) {
        // 报错
        console.log(item)
    }

4.在循环 for、for-in、forEach、for-of 、map中改变item的值,会发生什么?

了解过js都知道迭代器吧,就是遍历的特性,对于item.next().value进行操作。

🍪 如果原来的值是引用类型,那么iterator.next().value 和 arr[i]表示的是同一个对象,所以可以改变原来的item。

🍪 如果原来的值是基础类型,那么iterator.next().value 和 arr[i]分别指向了一个基础类型的值,所以不会改变原来的item。

  • 改变item元素值

    js
    const arr = [
                { name: '孙悟空', org: '花果山' },
                9,
                'swk',
                function f() {
                    console.log(3);
                },
                [9, 9, 9, 9],
                new Date()
            ];
    // 数组元素全变为'西天取经'
    for (let i = 0; i < arr.length; i++) {
        arr[i] = '西天取经';
    }
    
    // arr数组元素没有变
    arr.forEach((item) => {
        item = '西天取经';
    })
    
    // arr数组元素没有变
    arr.map((item) => {
        item = '西天取经';
    })
    
    // arr数组元素没有变
    for (let item in arr) {
        item = '西天取经';
    }
    
    // arr数组元素没有变
    for (let item of arr) {
        item = '西天取经';
    }
  • 改变item元素属性

    js
    const arr = [
                { name: '孙悟空', org: '花果山' },
                9,
                'swk',
                function f() {
                    console.log(3);
                },
                [9, 9, 9, 9],
                new Date()
            ];
    // 数组元素如下图所示
    for (let i = 0; i < arr.length; i++) {
        arr[i].org = '西天取经';
    }
    // 数组元素如下图所示
    arr.forEach((item) => {
        item.org = '西天取经';
    })
    // 数组元素如下图所示
    arr.map((item) => {
        item.org = '西天取经';
    })
    
    // arr数组不变
    for (let item in arr) {
        item.org = '西天取经';
    }
    // 数组元素如下图所示
    for (let item of arr) {
        item.org = '西天取经';
    }

    image.png

5.for、for-in、forEach、for-of 、map性能比较如何?

统一执行上面的性能代码得出以下效果:

image.png

JS数组循环的性能和效率分析

前端知识体系 · wcrane