高性能Javascript【四】算法和流程控制

【本文系外部转载,原文地址:http://www.hotels2map.com/blog/?p=160
Javascript代码执行时间大部分消耗在循环、条件判断、递归等语句中,这些也是其性能优化的要点。
  • for、while和do-while循环性能特性相似,所以没有一种循环类型明显快于或慢于其他类型。
  • 避免使用for-in循环,除非你需要遍历一个属性数量未知的对象。
  • 改善循环性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。
  • 通常来说,switch总比if-else快,但并不总是最佳解决方案。
  • 在判断条件较多时,使用查找表比if-else和switch更快,查找表最快。
  • 浏览器的调用大小限制了递归算法在JavaScript中的应用;溢出错误会导致其他代码中断运行。
  • 如果你遇到溢出错误,可以将递归方法改为循环迭代算法,或使用Memoization来避免重复计算。

循环

Javascript中的四种循环:for循环、while循环、do-while循环、for-in循环。for、while和do-while循环性能特相差不大,其中只有for-in循环要明显慢一些(1/7),因为他需要同时搜索对象的实例和原型属性。因此,除非明确需要遍历属性数未知的对象,否则因避免使用for-in循环(用for循环或查找表代替)。

var props = ["prop1", "prop2"],i = 0;
while (i < props.length){
  process(object[props[i]]);
}

改善循环性能的最佳方式:减少每次迭代的运算量;减少每次循环的迭代次数。

减少每次迭代的运算量

//原始循环
for (var i=0; i < items.length; i++){
  process(items[i]);
}

每次循环体(迭代)需要执行的操作:

  1. 一次控制条件中的属性查询(items.length)
  2. 一次控制条件中的数值大小比较(i < items.length)
  3. 一次控制条件是否为true的比较(i < items.length == true)
  4. 一次自增操作(i++)
  5. 一次数组查找(items[i])
  6. 一次函数调用(process(items[i]))

1.用变量缓存需要的对象或者数组成员:减少对数组项或对象成员的查找次数,优化上面的第一步操作。(提升25%、ie中50%)

//减少属性查询
for (var i=0, len=items.length; i < len; i++){
process(items[i]);
}

2.颠倒数组的循环顺序,从后往前:减少比较次数,从2次比较(是否小于总数、是否为true)较少到一次(是否为true),优化了上面的第二步操作。(提升50%-60%)

//减少属性并颠倒顺序
for (var i=items.length; i--; ){
  process(items[i]);
}

现在每次循环体需要执行的操作变成了:

  1. 一次控制条件是否为true的比较(i == true)
  2. 一次自减操作(i++)
  3. 一次数组查找(items[i])
  4. 一次函数调用(process(items[i]))

每次循环减少了2步操作,当循环的数量级很大的时候,性能提升会很显著。

减少每次循环迭代的次数

一次数量级很大的循环的开销是比较大的,可以把一个大循环拆开成几个小循环,最常见的模式是“达夫设备(Duff’s Device.)”:把一个大的循环展开为几个小的循环,总的循环次数不变。

//Jeff Greenberg 在2/2001把Duff’s Device从C语言移植到Javascript中
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把大循环分成每个迭代8次的小循环,8的余数再执行一次小循环。原理:在switch语句中,每个case不break的话,她后面的case都会被执行。这种方法在迭代次数超过1000以上时,效果比较明显。

假设一个800003次的循环,会被分成100001个小循环,第一个小循环迭代3次(余数为3,执行case 3、case 2、case 1),其余的100000次(总数除以8取整)每次迭代8次。

//Jeff Greenberg优化版
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--]);
}

Duff’s device的各种优化版本性能比较http://jsperf.com/duffs-device,^位运算符,浮点数与整数(整数优)、除法与乘法的性能比较(乘法优)。

基于函数的迭代

foreach方法会先遍历成员,再在每个成员上执行一个函数,这是基于函数的迭代,因为要调用外部的函数方法,基于函数的迭代要慢(是循环迭代的1/8)。

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

条件语句

if-else与switch

使用if-else还是switch?if-else:件数量很小时(2个或少数几个);件数量比较大时,switch(超过2个)。

  • 从易读性考虑:由条件数量决定,条件数量很小时,if-else容易读懂,当条件数量比较大时,switch语句更容易读懂。
  • 从性能考虑:switch比if-else运行要快,尤其是条件数量不断增大时,if-else的消耗要比switch多(1.大部分语言中的switch语句采用了分枝表索引优化,2.switch比较时使用全等操作符,不会发生类型转换的消耗)。

优化if-else

优化目标:用最小的条件判断次数,达到正确的分支。

方法:

1.把出现几率最多条件放在首位,按出现几率的大小顺序往后。

if (value < 5) {
//do something
} else if (value > 5 && value < 10) {
//do something
} else {
//do something
}

这个if-else,只有在value的值大部分情况下都小于5的时候,只需要一个条件判断,性能才是最优的。如果value的值为8的话,需要2次条件判断,消耗时间会增加。

2.if-else嵌套

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次条件判断,就可以找到正确的分支,当value的值平均分布的时候,这样的嵌套可以节约大约50%的时间。

3.查找表

当有大量的离散值需要判读的时候,最好的方法避免使用if-else和switch,查找表(Lookup Tables)更快,Javascript中的查找表通过数组或对象的成员查询来实现,完全抛弃了条件判断。查找表的消耗和成员的多少没有关系,数量增大时几乎不会产生额外的开销;尤其是当条件判断中的value和对象成员之间存在对应关系时,查找表的优势最为突出:下例中的 results[value],不用进行任何判读,直接获取results的value成员。

//数组
var results = [result0, result1, result2, result3...]
//查找成员查找
return results[value];
//对象
var obj = {
num1:"value1",
num2:"value2",
num3:"value3",
...
}
//对象成员查找
return obj[value];

递归

最常见的递归就是阶乘了:

function factorial(n){
  if (n == 0){ //终止条件
    return 1;
  } else {
    return n * factorial(n-1);
  }
}

递归中最关键的是终止条件,还有浏览器的调用栈限制(Call Stack Limits),也就是说浏览器有最大的递归次数限制。因此递归的出问题的时候可以从终止条件和递归次数限制两个方面来分析和改善。

解决调用栈限制的方法,可以用循环迭代来替代递归,而且循环比迭代中反复运行一个函数开销要小,虽然有时候循环要比迭代慢一些,但这样可以避开调用栈限制。

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 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([]); //in case of odd number of items
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] = []; //in case of odd number of items
}
return work[0];
}

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];
}
var fact6 = memfactorial(6);
//Memoization 封装
function memoize(fundamental, cache){
  cache = cache || {};
  var shell = function(arg){
    if (!cache.hasOwnProperty(arg)){
      cache[arg] = fundamental(arg);
    }
    return cache[arg];
  };
  return shell;
}
//阶乘
function factorial(n){
  if (n == 0){
    return 1;
  } else {
    return n * factorial(n-1);
  }
}
//Memoization 阶乘
var memfactorial = memoize(factorial, { "0": 1, "1": 1 });
var fact6 = memfactorial(6);

tips:

  1. 在for循环的初始化中定义一个新变量,等于在循环体外定义新变量,这个变量是函数级的,不是循环级别,这个变量会影响到循环体以外。
  2. 不要使用for-in来遍历数组成员,for-in会同时遍历数组成员以外的成员属性。
var a = ["one","two"];
a.name= "name";
for (p in a) {
  alert(a[p]);
}

此外Jeff Greenberg也有一篇关于Javascript优化的文章,推荐一下。http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值