JavaScript 中几种循环对比及性能分析

2,931 阅读2分钟

这是我参与8月更文挑战的第14天,活动详情查看8月更文挑战

前言

本文将对比几种循环,比如,探索 forEach 的底层原理和 for of 循环的底层机制;运行代码,比较所耗费的时间。

命令式编程和函数式编程

我们先来了解下什么是命令式编程和函数式编程。

命令式编程

命令式编程的主要思想是关注计算机执行的步骤, 即一步一步告诉计算机先做什么再做什么。

如果要解决以下一个问题:

求一个list里所有值的和

命令式编程可能会这么做

let list = [1,2,3,4,5];
let total = 0;

for (let i =0 ;i<list.lenght;i++){
  total += list[i]
}

console.log(total)

命令式编程,比较灵活,关注 how 如何做?初学者入门时,常用的编程方式和编程思想。

函数式编程

声明式编程是以数据结构的形式来表达程序执行的逻辑。 它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。

同样要实现“求一个list里所有值的和”,函数式编程,会怎么做?

let list = [1,2,3,4,5];

let total = list.reduce((sum,n)=>{
  return sum + n 
})

console.log(total)

函数式编程,使用方便,无法管控过程,性能上也有所消耗,是内部上实现封装,what 关注结果。

for 循环

for 循环是自己控制循环过程,明确知道循环多少次;而不确定循环次数的情况下使用 while。

情况1:基于 var 声明的时候,for 和 while 性能差不多。

我们可以在控制台运行下代码,看下具体结果

let arr = new Array(9999999).fill(0);

console.time('FOR~~');
for(var i = 0; i < arr.length; i++){}
console.timeEnd('FOR~~');

console.time('WHILE~~');
var i = 0;
while(i < arr.length) {
    i++;
}
console.timeEnd('WHILE~~');

在我的电脑,运行结果:

image.png

情况2:基于 let 声明的时候,for 循环性能更好「原理:没有创造全局不释放的变量」

我们可以将上面的循环中的 var 声明改成 let 声明,再运行下代码看下。下面是测试结果。

image.png

forEach 底层原理

我们来尝试手写一下 forEach,理解下 forEach 实现逻辑。

里面涉及到 this 和 call 等相关内容,不熟悉的同学,可以查下相关资料学习下。

Array.prototype.forEach = function forEach(callback, context) {
    // this => arr
    let self = this,
        i = 0,
        len = self.length;
    context = context == null ? window : context;
    
    for(;i < len; i++) {
        typeof callback === 'function' ? callback.call(context, self[i], i) : null;
    }
}

for in 循环的 BUG 及解决方案

for in 性能很差,迭代当前对象中所有可枚举的属性「私有属性大部分是可枚举的,公有属性(出现在所属类的原型上的)也有部分是可枚举的」查找机制上一定会搞到原型链上去,按照原型链一级级查找很耗性能

for in 循环存在的问题:

  1. 问题一:遍历顺序以数字优先
  2. 问题二:无法遍历 Symbol 属性
  3. 问题三:可以遍历到公有中可枚举的(一般是自定义属性)

运行代码

Object.prototype.fn = function fn(){}
let obj = {
    name: '追梦玩家',
    age: 18,
    [Symbol('sex')]: 'male',
    0: 200,
    1: 300
};

for(let key in obj) {
    console.log(key);
}

我们看下截图对应的输出结果,正好都说明了 for in 循环存在的问题

image.png

那我们如何解决无法遍历 Symbol 属性呢?不慌,我们需要使用到 Object.getOwnPropertySymbols

let keys = Object.keys(obj);
if(typeof Symbol !== 'undefined') {
    keys = keys.concat(Object.getOwnPropertySymbols(obj));
}
keys.forEach(key => {
    console.log('属性名:', key);
    console.log('属性值:', obj[key]);
})

那如何解决可以遍历到公有中可枚举的?其实就是考察我们的基础知识,使用 hasOwnProperty 方法即可。

image.png

for of 循环的底层机制

mdn 文档关于 for of 循环的描述

for...of语句可迭代对象(包括 ArrayMapSetStringTypedArrayarguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

重点是可迭代对象,这些数据结构实现了迭代器规范,其实 for of 循环的原理就是按照迭代器规范遍历的。

迭代器 iterator 规范「具备 next 方法,每次执行返回一个对象,具备 value/done 属性」

我们来尝试模拟实现一下。

let arr = [10, 20, 30];
// 默认的
// arr[Symbol.iterator] = function () {

// 我们重写的方法
Array.prototype[Symbol.iterator] = function () {
    let self = this,
        index = 0;

    return {
        // 必须具备 next 方法,执行一次 next 方法,拿到结构中的某一项的值
        // done: false value: 每一次获取的值
        next () {
            if (index > self.length - 1) {
                return {
                    done: true,
                    value: undefined
                }
            }
            return {
                done: false,
                value: self[index++]
            }
        }
    }
}   

关于几种循环的运行结果如图,耗费时间对比

从上图,我们可以看出使用 for 循环的性能更好,而且也值得注意的是,实际开发中,我们应该选择合适的循环方式来实现想要的效果。

文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。