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

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

4.1 循环

4.1.1 循环的类型

主要有四种循环类型:forwhiledo-whilefor-in

// 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--);
  1. 减少迭代次数:使用“达夫设备(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

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

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

4.2.2 优化if-else

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

  1. 将最可能出现的条件放在首位,最小概率出现的的条件放在末尾;
  2. 使用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];
}

欢迎大佬纠错指导,欢迎同行交流学习。

GitHub:https://github.com/Code4GL
:https://www.jianshu.com/u/7f5541a6b6d2
知乎:https://www.zhihu.com/people/code4gl/activities
公众号:code_everything
QQ:771841496
邮箱:[email protected]

扫码关注公众号code_everything

你可能感兴趣的:(《高性能JavaScript》第四章 算法和流程控制)