高效的JS流程控制(一)

136 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

代码的整体结构是影响运行速度的主要因素,代码量多不代表运行速度就慢。

循环

在大多数编程语言中,代码执行时间大部分消耗在循环中。循环处理是最常见的编程模式之一,也是提升性能值得关注的要点。

循环一共有四种类型

  • for

    for循环是 JS 中最常用的循环结构。

  • while

    while 循环是最简单的前侧循环,由一个前测条件和一个循环体构成。

  • do-while

    do-while循环是 JS 中唯一一种后测循环,在 do-while 循环中,循环体至少会运行一次。

  • for-in

    它可以枚举任何对象的属性名,循环体每次运行时所返回的属性包括对象实例属性以及从原型链中继承而来的属性。

  • for-of

    它可以迭代对象的属性值(不支持遍历普通对象),循环体每次运行时返回的是自身的所有属性值。

减少循环操作量

由于每次迭代操作会同时搜索实例或原型属性,for-in 循环的每次迭代都会产生更多的开销,所以比其他循环类型要慢。 在使用循环的时候有两个值得关注的点

  • 每次迭代处理的事务
  • 迭代次数 通过减少这两种的一个或全部的时间开销,就提升了循环的整体性能。

很明显,如果一次循环迭代要花很长时间去执行,那么多次循环意味着需要更多时间。一个提升循环整体速度的好方法是现在循环中耗时操作的数量。

通过对比不同的循环方式

// 原始版本
for(let i = 0; i < items.length; i++){
    run(items[i])
}

let j = 0;

while(j < items.length){
    run(items[j++])
}

let k = 0;

do{
    run(items[k++])
} while(k < items.length);

在上面的循环中,每次运行循环时都会产生如下操作:

  1. 在控制条件中查找一次属性: items.length
  2. 在控制条件中执行一次数值的比较:i < items.length
  3. 一次比较查找查看控制条件计算结果是否为true: i < items.length == true
  4. 一次自增:i++
  5. 一次数组查找:items[i]
  6. 一次函数调用:run()

在这些简单的循环中,代码运行的速度很大程度取决于 run() 对每个数组项的操作,因此减少每次迭代中的操作总数能大幅提高循环的总体性能。

// 优化

const len = items.length;

for(let i = 0; i < len; i++){
    run(items[i])
}

let j = 0;
while(j < len){
    run(items[j++])
}

let k = 0;

do{
    run(items[k++])
} while(k < len);

在重写后只在循环运行前对数组长度进行一次属性查找。这使得控制条件可直接读取局部变量,所以速度有效提升。
通过颠倒数组的顺序来提升提高循环性能,如果数组的顺序与执行的任务无关,因此从最后一项开始向前处理也能带来一些速度的提升。

// 再次优化

const len = items.length;

for(let i = len; i > 0; i--){
    run(items[i])
}

let j = len;
while(j--){
    run(items[j++])
}

let k = len;

do{
    run(items[k])
} while(k--);

在上面的循环中,每次运行循环时都会产生如下操作:

  1. 在控制条件中执行一次数值的比较:i == true
  2. 一次自增:i--
  3. 一次数组查找:items[i]
  4. 一次函数调用:run()

新的循环代码在每次迭代中减少了两次操作,随着迭代次数增加,性能的提升更趋于明显。

减少迭代次数

循环体运行时会带来一次小的性能消耗,这会增加总体运行时间。减少迭代次数能获得更加显著的性能提升。

    let i = items.length % 8;
    
    while(i){
        run(items[i--])
    }
    
    i = Math.floor(items.length % 8);
    
    while(i){
        run(items[i--]);
        run(items[i--]);
        run(items[i--]);
        run(items[i--]);
        run(items[i--]);
        run(items[i--]);
        run(items[i--]);
        run(items[i--]);
    }
   

每次循环中最多调用8次run()。循环的迭代次数为总数除以8.由于不是所有数字都能被8整除,变量 startAt 用来存放余数,表示第一次循环中应该调用多少次run()。
如果是12次,那么第一次循环会调用 run() 4次,第二次循环中调用 run() 8次。用两次循环替代12次循环。