《高性能JavaScript》第四章 算法和流程控制

JavaScript循环、条件与递归性能优化

4.1 循环

4.1.1 循环的类型

主要有四种循环类型:for、while、do-while和for-in。

--------------------------------------------------------------------
注:如果你对python感兴趣,我这有个学习Python基地,里面有很多学习资料,感兴趣的+Q群:895817687
--------------------------------------------------------------------

    // 1、for循环:初始化、前测条件、后执行体和循环体组成。
    for (var i=0; i < 10; i++){
     // 循环体
    }
    // 2、while循环:前测条件和循环体组成。
    var i = 0;
    while(i < 10){
     // 循环体
     i++;
    }
    // 3、do-while循环:循环体和后测条件组成。
    var i = 0;
    do {
     // 循环体
    } while (i++ < 10);
    // 4、for-in循环:可以枚举任何对象的属性名。
    for (var prop in object){
     // 循环体
    }

注意:for循环初始化的var语句会创建一个函数级别的变量,而不是循环级。由于JavaScript只有函数级作用域,因此for循环中定义一个新变量相当于在循环体外定义一个新变量。

4.1.2 循环性能

循环的性能提升主要由这几个方面入手:
1、循环的类型:除了for-in循环比其他三种循环明显要慢外,其他几种性能都差不多。所以除非明确需要迭代一个属性数量未知的对象,否则避免使用for-in循环;
2、减少迭代的工作量:将需要多次查找的属性存入局部变量中重复使用,并使用倒序循环;

    // 原始版本
    for (var i=0; i < items.length; i++){
        process(items[i]);
    }
    var j=0;
    while (j < items.length){
        process(items[j++]]);
    }
    var k=0;
    do {
        process(items[k++]);
    } while (k < items.length);
    // 最小化属性查找:在大多数浏览器中能节省25%运行时间
    for (var i=0, len=items.length; i < len; i++){
        process(items[i]);
    }
    var j=0,
    count = items.length;
    while (j < count){
        process(items[j++]]);
    }
    var k=0,
    num = items.length;
    do {
        process(items[k++]);
    } while (k < num);
    //最小化属性查找并反转:比原始版本快50%~60%
    for (var i=items.length; i--; ){
        process(items[i]);
    }
    var j = items.length;
    while (j--){
        process(items[j]]);
    }
    var k = items.length-1;
    do {
        process(items[k]);
    } while (k--);

3、减少迭代次数:使用“达夫设备(Duff’s Device)”模式限制迭代次数。Duff’s Device背后的基本理念:每次循环最多可调用8次process(),循环迭代的次数为总数除以8。

    // 原始版Duff’s Device
    var iterations = Math.floor(items.length / 8),
    startAt = items.length % 8,
    i = 0;
    do {
        switch(startAt){
            case 0: process(items[i++]);
            case 7: process(items[i++]);
            case 6: process(items[i++]);
            case 5: process(items[i++]);
            case 4: process(items[i++]);
            case 3: process(items[i++]);
            case 2: process(items[i++]);
            case 1: process(items[i++]);
        }
        startAt = 0;
    } while (--iterations);
    // 升级版Duff’s Device:尽管这种方式用两次循环代替之前一次循环,但移除了循环体中的switch语句,速度比原始更快。
    var i = items.length % 8;
    while(i){
        process(items[i--]);
    }
    i = Math.floor(items.length / 8);
    while(i){
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
        process(items[i--]);
    }

性能优化:迭代次数<1000,和常规循环结构比性能提升微不足道;迭代次数>1000,Duff’s
Device模式性能会有明显提升。在5000次迭代中,性能比常规提升70%。

4.1.3 基于函数的迭代

ECMA-262标准第四版介绍了本地数组对象的一个新方法forEach()。此方法遍历一个数组的所有成员,并在每个成员上执行一个函数。在每个元素上执行的函数作为 forEach()的参数传进去,并在调用时接收三个参数,它们是:数组项的值,数组项的索引,和数组自身。

    items.forEach(function(value, index, array){
        process(value);
    });

性能优化:在所有情况下,基于循环的迭代比基于函数的迭代快8倍。因此在运行速度严格要求时,基于函数的迭代不是合适的选择。

4.2 条件语句

4.2.1 if-else对比switch

多数情况下,switch比if-else运行的要快,但只有条件数量很大时才快的比较明显。这两句的主要性能区别是:当条件增加时,if-else性能负担增加的程度比switch要多。

性能优化:if-else适用于判断两个离散值或几个不同的值域;当判断多个离散值时,switch语句是更佳选择。

4.2.2 优化if-else

优化目标:最小化到达正确分支前所需判断的条件数量。
优化原则:

将最可能出现的条件放在首位,最小概率出现的的条件放在末尾;
使用if-else嵌套:
嵌套过程中使用二分法把值域分成一系列区间,逐步缩小范围。
从而减少条件判断次数。

  • 反例
    // 最多要判断10次
    if (value == 0){
        return result0;
    } else if (value == 1){
        return result1;
    } else if (value == 2){
        return result2;
    } else if (value == 3){
        return result3;
    } else if (value == 4){
        return result4;
    } else if (value == 5){
        return result5;
    } else if (value == 6){
        return result6;
    } else if (value == 7){
        return result7;
    } else if (value == 8){
        return result8;
    } else if (value == 9){
        return result9;
    } else {
        return result10;
    }
  • 正例
    // 最多判断4次
    if (value < 6){
        if (value < 3){
            if (value == 0){
                return result0;
            } else if (value == 1){
                return result1;
            } else {
                return result2;
            }
        } else {
            if (value == 3){
                return result3;
            } else if (value == 4){
                return result4;
            } else {
                return result5;
            }
        }
    } else {
        if (value < 8){
            if (value == 6){
                return result6;
            } else {
                return result7;
            }
        } else {
            if (value == 8){
                return result8;
            } else if (value == 9){
                return result9;
            } else {
                return result10;
            }
        }
    }

4.2.3 查找表

  • 描述

JavaScript中可以使用数组和普通对象来构建查找表,特别是在条件语句数量很大的时候,性能比条件语句快很多。

  • 原因

当你使用查找表时,必须完全抛弃条件语句。这个过程变成数组项查询或者对象成员查询。主要优点:不用书写任何条件语句,即便候选值数量增加时,也几乎不会产生额外的开销。

  • 反例
    if (value == 0){
        return result0;
    } else if (value == 1){
        return result1;
    } else if (value == 2){
        return result2;
    } else if (value == 3){
        return result3;
    } else if (value == 4){
        return result4;
    } else if (value == 5){
        return result5;
    } else if (value == 6){
        return result6;
    } else if (value == 7){
        return result7;
    } else if (value == 8){
        return result8;
    } else if (value == 9){
        return result9;
    } else {
        return result10;
    }
  • 正例
    // 将返回值存入数组
    var results = [result0, result1, result2, result3, result4, result5, result6, result7, result8, result9, result10]
    // 利用数组返回当前结果
    return results[value];

4.3 递归

4.3.1 调用栈限制

JavaScript引擎支持的递归数量与JavaScript调用栈大小直接相关。如果超出了浏览器的调用栈限制,就会抛出调用栈溢出的异常。

4.3.2 递归模式

递归一般有两种模式:直接递归模式和隐伏模式。第二种模式在出现问题时,很难定位。

    // 1、直接调用模式
    function recurse(){
        recurse();
    }
    recurse();
    // 2、隐伏模式
    function first(){
        second();
    }
    function second(){
        first();
    }
    first();

4.3.3 迭代

  • 描述

任何递归能实现的算法,迭代也能实现。

  • 原因

使用优化后的循环替代长时间运行的递归函数可以提升性能,因为运行一个循环比反复调用一个函数的开销要少很多。

  • 反例
    function merge(left, right){
        var result = [];
        while (left.length > 0 && right.length > 0){
            if (left[0] < right[0]){
                result.push(left.shift());
            } else {
                result.push(right.shift());
            }
        }
        return result.concat(left).concat(right);
    }
    function mergeSort(items){
        if (items.length == 1) {
            return items;
        }
        var middle = Math.floor(items.length / 2),
        left = items.slice(0, middle),
        right = items.slice(middle);
        return merge(mergeSort(left), mergeSort(right));
    }
  • 正例
    function merge(left, right){
        var result = [];
        while (left.length > 0 && right.length > 0){
            if (left[0] < right[0]){
                result.push(left.shift());
            } else {
                result.push(right.shift());
            }
        }
        return result.concat(left).concat(right);
    }
    // 使用迭代后比递归要慢一些,但不会受到调用栈限制。是避免栈溢出错误的方法之一。
    function mergeSort(items){
        if (items.length == 1) {
            return items;
        }
        var work = [];
        for (var i=0, len=items.length; i < len; i++){
            work.push([items[i]]);
        }
        work.push([]); // 如果数组长度为奇数
        for (var lim=len; lim > 1; lim = (lim+1)/2){
            for (var j=0,k=0; k < lim; j++, k+=2){
                work[j] = merge(work[k], work[k+1]);
            }
            work[j] = []; // 如果数组长度为奇数
        }
        return work[0];
    }

4.3.4 Memoization

Memoization是一种避免重复工作的方法,它缓存前一个计算结果后供后续计算使用。
以阶乘函数为例:

    function factorial(n){
        if (n == 0){
            return 1;
        } else {
            return n * factorial(n-1);
        }
    }

// 使用过程中factorial()函数被调用了18次,而所有必要的计算在第一行代码里已经处理掉了。
var fact6 = factorial(6);
var fact5 = factorial(5);
var fact4 = factorial(4);

使用Memoization后,将第一次计算的结果缓存起来,后期再调用的时候,直接从缓存中读取结果。

    function memfactorial(n){
        if (!memfactorial.cache){
            memfactorial.cache = { "0": 1, "1": 1 };
        }
        if (!memfactorial.cache.hasOwnProperty(n)){
            memfactorial.cache[n] = n * memfactorial (n-1);
        }
        return memfactorial.cache[n];
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值