likes
comments
collection
share

别再恶搞forEach了,它就是单纯的从头遍历到尾,它没有那么多问题

作者站长头像
站长
· 阅读数 23

最近看了好几篇关于forEach的文章,问如何跳出forEach循环,还有问forEachfor循环有什么区别,看着我是一脸无奈,他们是猩猩吗?

如何中断forEach

先来说说跳出forEach,你都要跳出forEach,那么可不可以不要用forEach呀?可不可以先去看看文档再来去使用它呀?MDN->forEach赶紧先去看看文档。

先来说一下文档中没有一句提到通过抛出异常来中断forEach循环的,而是告诉你如果你要中断forEach循环就不要使用forEach,来我们看看有哪些方式可以代替forEach:

const arr = [];

// 原型方法
arr.filter(() => {});
arr.map(() => {});
arr.find(() => {});
arr.findIndex(() => {});
arr.lastIndexOf(() => {});
arr.reduce(() => {});
arr.reduceRight(() => {});
arr.some(() => {});
arr.every(() => {});

// js语法
for (let i = 0; i < arr.length; i++) {

}

for (let arrElement of arr) {

}

for (let index in arr) {

}

来看看,有这么多的方式可以替代forEach,为什么非要抓着forEach不放呢?

看评论区有说应付面试的,你面试题就不能问问数组的原型方法有哪些可以迭代数组的?就不能问问for offor in的区别?

不吐槽了,上面有这么多方案都可以达到和forEach相同的效果,那么可以中断迭代的方案有哪些呢?

在讲解方案之前,我先带大家简单的认识一下forEachforEachArray的原型方法,首先它是内置的方法,它接收一个回调函数,这个回调函数接收三个参数,第一个是当前迭代的对象,第二个是当前迭代的索引,第三个是原数组,同时他还有第二个参数,就是回调函数的this,百闻不如一见:

/**
 * forEach 的函数签名
 * @param {function} callback 回调函数
 *                   callback 的函数签名
 *                   @param {any} currentValue 当前元素
 *                   @param {number} index 当前元素的索引
 *                   @param {array} array 当前数组
 *                   
 * @param {any} thisArg 回调函数中的 this 指向
 */
// forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;

// forEach 的用法
arr.forEach(function (item, index, thisArr) {
    console.log(item, index, thisArr);
    console.log(this);
}, {a: 'a'});

认识完了之后,接下来就是用上面的方案替换forEach循环

  1. 第一波推荐js基础,for 循环
function forEachCallback(item, index, thisArr) {
  console.log(item, index, thisArr);
  console.log(this);
}

// forEach 的用法
arr.forEach(forEachCallback, {a: 'a'});

// 转换为 for 循环
for (let i = 0; i < arr.length; i++) {
  forEachCallback.call({a: 'a'}, arr[i], i, arr);
}

for (let arrElement of arr) {
  forEachCallback.call({a: 'a'}, arrElement, arr.indexOf(arrElement), arr);
}

for (let index in arr) {
  forEachCallback.call({a: 'a'}, arr[index], index, arr);
}

上面的代码可以说是手写了一半的forEach了,现在的问题是怎么中断这个循环,可以看到业务逻辑还是放到方法里面了,方法里面写return没用,写break报错(这些都是很基础的知识点了),这样也就能理解为什么forEach不能被中断了。

现在都写了for循环了,只需要把业务代码直接移到循环体里面,然后对应的写break不就可以中断了。

下面就是误人子弟的代码,后面都用这个例子来转换

try {
  arr.forEach((item) => {
    // 我们假设有5%的几率会出现异常,然后中断循环
    if (Math.random() < 0.05) {
      throw new Error('中断循环');
    }
    console.log(item);
  });
} catch (e) {
}

转换为for循环

for (let i = 0; i < arr.length; i++) {
  if (Math.random() < 0.05) {
    break;
  }
  console.log(arr[i]);
}

for (let arrElement of arr) {
  if (Math.random() < 0.05) {
    break;
  }
  console.log(arrElement);
}

for (const index in arr) {
  if (Math.random() < 0.05) {
    break;
  }
  console.log(arr[index]);
}
  1. someevery

    来认识一下这两个Array的原型方法:

    • some:测试是否有一个元素通过了回调函数的测试。
    /**
     * some 的函数签名
     * @param {function} callback 回调函数
     *                  callback 的函数签名
     *                  @param {any} currentValue 当前元素
     *                  @param {number} index 当前元素的索引
     *                  @param {array} array 当前数组
     * @return {boolean} 返回值为 true 时,停止循环,返回 true
     */
    // some(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean;
    
    // some 的用法
    const result = arr.some((item, index, thisArr) => {
      console.log(item, index, thisArr);
      return item === 5;
    });
    
    • every:测试是否所有的元素都通过了回调函数的测试。
    /**
     * every 的函数签名
     * @param {function} callback 回调函数
     *                 callback 的函数签名
     *                 @param {any} currentValue 当前元素
     *                 @param {number} index 当前元素的索引
     *                 @param {array} array 当前数组
     *                 @return {boolean} 返回值为 false 时,停止循环,返回 false
     */
    // every(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean;
    
    // every 的用法
    const result = arr.every((item, index, thisArr) => {
      console.log(item, index, thisArr);
      return item === 5;
    });
    

someevery的两个的作用和用法都类似,不过他们的判定的结果是相反的,来看看他们是怎么中断forEach的:

arr.some((item) => {
  // some 的回调函数中,返回 true 时,停止循环,所以这里是大于才是 5%
  if (Math.random() > 0.05) {
    return true;
  }

  console.log(item);
  return false;
})

arr.every((item) => {
  // every 的回调函数中,返回 false 时,停止循环,和 some 相反,这里是小于才是 5%
  if (Math.random() < 0.05) {
    return false;
  }
  console.log(item);
  return true;
})
  1. findfindIndex

    老规矩先来认识一下这两个原型方法

    • find:返回数组中第一个通过回调函数测试的项。
    /**
     * find 的函数签名
     * @param {function} callback 回调函数
     *                 callback 的函数签名
     *                 @param {any} currentValue 当前元素
     *                 @param {number} index 当前元素的索引
     *                 @param {array} array 当前数组
     * @return {boolean} 返回值为 true 时,停止循环,返回当前元素
     */
    // find(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T | undefined;
    
    // find 的用法
    const result = arr.find((item, index, thisArr) => {
      console.log(item, index, thisArr);
      return item === 5;
    });
    
    • findIndex:返回数组中第一个通过回调函数测试项的索引。
    /**
     * findIndex 的函数签名
     * @param {function} callback 回调函数
     *                callback 的函数签名
     *                @param {any} currentValue 当前元素
     *                @param {number} index 当前元素的索引
     *                @param {array} array 当前数组
     * @return {boolean} 返回值为 true 时,停止循环,返回当前元素的索引
     */
    // findIndex(callbackfn: (value: T, index: number, array: T[]) => unknown, thisArg?: any): number;
    
    // findIndex 的用法
    const result = arr.findIndex((item, index, thisArr) => {
      console.log(item, index, thisArr);
      return item === 5;
    });
    

findfindIndex的使用方法相同,不同的是返回结果一个是项,一个是索引,来看看他们是怎么中断forEach的:

arr.find((item) => {
  if (Math.random() < 0.05) {
    return true;
  }

  console.log(item);
  return false;
})

arr.findIndex((item) => {
  if (Math.random() < 0.05) {
    return true;
  }

  console.log(item);
  return false;
})

好了,也就这么些方法可以中断数组的迭代了,以后面试再遇到这个问题,不敢怼面试官,那就上我这个标准答案,请不要再throw new Error()了,真丢人。

forEachfor循环的区别

这个问题很多人觉得,forEach是异步的,for是同步的。

我信了你的个鬼,你输出forEach的迭代项看看,有一次是乱序的吗?你有什么证据证明它是异步的?

哦对了,异步编程有一个重要的概念,就是回调函数,因为回调函数的执行时机不确定,所以这就算异步了?

好好看看我最开始写的forEachfor转换的例子,for就是同步的,同步里面执行了一个方法,如果这个方法是异步的,那么它就是异步的,如果这个方法是同步的,那么它就是同步的。

不是所有带有回调函数都是异步编程,不信邪的在forEach下面打印一个log,看看是这个log先输出还是forEach回调函数里面的逻辑先执行。

给你讲讲真实的区别吧:

  1. for infor of不知道有没有人了解其中的原理;

    • for in语句以任意顺序迭代一个对象的除 Symbol 以外的可枚举属性,包括继承的可枚举属性。
    const obj = {
      a: 'a',
      b: 'b',
      c: 'c',
      [Symbol()]: 'symbol',
    }
    
    Object.defineProperty(obj, 'b', {
      enumerable: false
    })
    
    for (let key in obj) {
      console.log('obj.' + key + ' = 我是' + obj[key])
    }
    
    • for of是借助Iterator接口来迭代对象的。
    const obj = {
      a: 'a',
      b: 'b',
      c: 'c',
      [Symbol.iterator]: function () {
        const arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
        let index = 0
        return {
          next: () => {
            return {
              value: arr[index++],
              done: index > arr.length
            }
          }
        }
      }
    }
    
    for (let item of obj) {
      console.log(item)
    }
    
  2. 普通的for循环,这个没什么好说的吧,就是定义一个结束循环的标志(和次数没关系,因为可以在循环体中重新设置用于判定的变量的值),每次循环之前都判定一下,确定结束了就不循环了。

  3. forEach可以看到上面的for infor of迭代的都是对象,forEach作用在数组上面。

但是其实js里面万物皆对象,你以为的数组其实也是对象,你可以试试[1,2,3]["1"]看看值是什么,别说什么隐式类型转换,js对象的key到现在只有两种,一种是String,一种是SymbolMapkey可以是对象,这里面要将的门道太多了,不在这次讨论的范畴。

那么他们的区别到底在什么地方,不说废话,直接上代码:

const arr = [1, , 3, , 5, , 7, , 9, 10];

arr.forEach((item) => {
  console.log('这里是 forEach', item);
})
console.log('-------------------');


for (let i = 0; i < arr.length; i++) {
  console.log('这里是 for i', arr[i]);
}
console.log('-------------------');

for (let i in arr) {
  console.log('这里是 for in',arr[i]);
}
console.log('-------------------');

for (let i of arr) {
  console.log('这里是 for of',i);
}

自己输出上面的示例看看结果吧,我这里就直接下结论了,上面的代码示例结果也能说明forEach是同步的;

  1. forEach遇到遇到空位会跳过。
  2. 普通的for循环一撸到底,肯定是不会跳过的。
  3. for in因为空位表现为undefined,不是那种声明的undefined,是没有这个玩意,可以通过Object.hasOwn(arr, 1)来验证。
  4. for of使用的是迭代器,也是直接一撸到底。

所以区别是什么?

  1. 写法不一样,forEach明显比for循环少写很多代码,所以可以称之为简化版for循环。
  2. 语法不一样,就拿中断来举例子,forEach中不能使用breakcontinuereturnforEach中的表现形式和在for循环中的continue相同。
  3. 迭代的元素有优化,for ifor of都是一撸到底,for inforEach表现相同,但是for in迭代出来的key类型是String,只是表现相同,结果还是不同的。
  4. 用法不同,forEach使用的是回调函数,for循环操作的直接就是变量。

分析完上面的一波,再来分析一下,如果对原数组对象在循环内部操作,对循环会有什么变化吧,直接上代码:

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.forEach((item, index) => {
  if (index === 0) {
    arr.shift();
  }
  console.log(item);
});
console.log('-------------------');

arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let i = 0; i < arr.length; i++) {
  if (i === 0) {
    arr.shift();
  }
  console.log(arr[i]);
}
console.log('-------------------');

arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let item of arr) {
  if (item === 1) {
    arr.shift();
  }
  console.log(item);
}
console.log('-------------------');

arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let item in arr) {
  if (item === '0') {
    arr.shift();
  }
  console.log(arr[item]);
}
console.log('-------------------');

自己去测试一下结果,我还是直接上结论:

  1. forEach当前项不会改变,但是后面所有的项都会变化。
  2. for i整个结果表现正确。
  3. for of表现形式和forEach相同,是不是侧面说明forEach内部也是使用迭代器做的循环处理?
  4. for in整个结果表现正确。

ok,到这我们不难发现,如果我们使用forEach,然后在循环体操作原数组,那么结果是未知的,并且不能控制让结果正确,而使用for循环,是可以让结果保持正确的,所以for循环可以应用更多的复杂场景。

这个就探索到这里了,还有可以深入的地方,但是深入到这里已经够应付很多场景了。

forEach的异步回调

本来是不想说异步的,但是上面提到了异步,这里也就简单的讲一下,es6不是出了async的异步标记,用来标识一个函数是异步的函数,那么在回调函数上面标记会发生什么呢?来试试:

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const _await = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, time);
  });
}
arr.forEach(async (item, index) => {
  // 这里模拟异步执行时间
  await _await(Math.ceil(Math.random() * 100));
  console.log(item);
});

结果中可以看到输出的顺序全乱了,如果你想正序的迭代一个数组,那么异步肯定是不可取的,这是不是又侧面的说明forEach是同步的?

现在我有一个面试题,怎么保证上面的迭代能按照同步的方式进行打印出来?这个题就是给那些说forEach是异步的人来打脸的,是异步的为什么你们从来都没解决过异步同步顺序执行呢?

总结

那些狗屁文章还有公众号不要再抄来抄去了,都不通过验证就直接抄,一个错误传遍整个圈子,大家还都说对。

还有那些什么面试题,面试造火箭,你火箭的螺丝型号都没搞清楚就敢开口问,完了之后错误的螺丝还就拧到火箭上了,火箭飞的磕磕盼盼的应该很爽吧。