在大多与编程语言中,代码的执行时间大部分消耗在循环中,是提升性能必须关注的要点之一

循环的类型

for循环(它由四部分组成:初始化、前测条件、后执行体、循环体。)

for(var i = 0; i < 10; i++){

doSomething();

}

可以将 var 改成 let 因为 var i会创建一个函数级/全局变量。

while循环(while循环是最简单的循环,由前测条件和循环体组成。)

var i = 0;

while(i < 10) {

doSomething();

i++;

}

任何for循环都能改成 while 反之亦然

do-while循环(由循环体和后测条件组成。)

var i = 0;

do {

doSomething();

} while(i++ < 10)

在do-while循环中,至少会执行一次循环体,与其他三种有明显的区别

for-in循环(for-in循环是比较特殊的循环类型。它可以遍历一个对象的属性/方法名。)

for(var prop in object){

doSomething();

}

循环体每次运行时,prop会被赋值为object的一个属性/方法名(字符串),直到遍历完所有属性/方法才结束,所返回的属性包括对象实例以及从原型链中继承而来的属性。

var array = [1,2,3]
for(var prop in array) {
console.log(prop) // 打印结果 1 2 3
}
Array.prototype.isNumber = function(){
return true;
}
for(var prop in array) {
console.log(prop) // 打印结果 1 2 3 isNumber
}
var object ={
a:1,
b:2,
f1:function(){}
}
for(var prop in object) {
console.log(prop) // 打印结果 a b f1
} // 提示:不要使用 for-in 来遍历数组成员

循环性能

因为for-in循环每次迭代操作都要搜索实例或原型的属性/方法,所以其性能明显低于其他三种循环。

影响循环的性能主要是如下两个因素:

1.每次迭代处理的事务

2.迭代的次数

减少迭代工作量

典型的循环示例如下:

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]))

如此简单的循环中,即使代码不多,也要进行许多操作。下面我们看看,如何减少迭代执行时的操作。

减少对象成员及数组项的查找次数

for(var i=0, len = items.length; i < len; i++){

process(items[i])

}

这样就减少了查找属性的操作

倒序循环

通过颠倒数组的顺序,减少控制条件中的查找属性和比较操作。

for(var i = items.length; i--;){

process(items[i])

}

减少迭代次数

减少迭代次数的典型方法“达夫设备(Duff's Device)”。是一种循环体展开技术,是在一次迭代中实际执行了多次迭代的操作。示例如下(感兴趣的同学可以自行百度查询达夫设备)

console.time(0)
var a = [0, 1, 2, 3, 4];
var sum = 0;
for(var i = 0; i < 5; i++)
sum += a[i];
console.timeEnd(0) // 0: 0.011962890625ms console.time(1)
var as = [0, 1, 2, 3, 4];
var sums = 0;
sums += as[0];
sums += as[1];
sums += as[2];
sums += as[3];
sums += as[4];
console.timeEnd(1) // 1: 0.010009765625ms

因为少作了多次的for循环,很显然这段代码比前者效率略高,而且随着数组长度的增加,少作的for循环将在时间上体现更多的优势。

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);

看switch/case语句,因为没有写break,所以除了第一次外,之后的每次迭代实际上会运行8次!Duff's Device背后的基本理念是:每次循环中最多可调用8次process()。循环的迭代次数为总数除以8。由于不是所有数字都能被8整除,变量startAt用来存放余数,便是第一次循环中应调用多少次process()。

此算法一个稍快的版本取消了switch语句,将余数处理和主循环分开:

var i = items.length % 8;
while(i){
process(items[--i])
}
i = items.length
var j = Math.floor(items.length / 8)
while(j--){
process(items[--i])
process(items[--i])
process(items[--i])
process(items[--i])
process(items[--i])
process(items[--i])
process(items[--i])
process(items[--i])
}

尽管这种方式用两次循环代替了之前的一次循环,但它移除了循环体中的switch语句,速度比原始循环更快。

基于函数的迭代

数组forEach方法,遍历数组的所有成员,并在每个成员上执行一次函数。示例如下:

items.forEach(function (value, index , array) {

process(value)

})

三个参数分别是:当前数组项的值,索引和数组本身。

各大浏览器都原生支持该方法,同时各种JS类库也都由类似的实现。但由于要调用外部方法,带来了额外的开销,所以性能比之前介绍的集中循环实现慢很多。

条件语句

if-else对比switch

由于各浏览器针对if-else和switch进行了不同程度的优化,很难简单说那种方式更好,只有在判断条件数量很大时,switch的性能优势才明显。一般来说判断条件较少时使用if-else更易读,当条件较多时switch更易读。

优化if-else

使用if-else,实际也存在很大的性能差距。这是因为到达正确分支时,所需要执行的判断条件数量不同造成的。主要的的优化方法有如下几种:

1.最可能出现的条件放首位。

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

2.把if-else组织成一系列嵌套的if-else,减少每个分支达到的判断次数。

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 {
return result
} // ******上述条件语句最多要判断6次,可以改写为 if (value &lt; 3) {
if (value == 0) {
return result0
} else if (value == 1) {
return result1
} else {
return result2
}
}else {
if (value == 3) {
return result4
} else if (value == 4) {
return result4
} else if (value == 5) {
return result5
} else {
return result
}
} // ******此时最多判断次数变为4次,减少了平均执行时间。

查找表

有时候使用查找表的方式比if-else和switch更优,特别是大量离散数值的情况。使用查找表不仅能提高性能还能答复降低圈复杂度和提高可读性,而且非常方便扩展。

例如上面的示例改为查找表:

var results = [result0,result1,result2,result3,result4,result5,result]

return result[value]

这里示例是数值,调用函数也同样适用,例如

var fn = {

1: function(){/* */},

2: function(){/* */},

3: function(){/* */}

}

fn[value]()

递归

递归可以把复杂的算法变得简单。例如阶乘函数:

function factorial (n) {

if (n == 0) {

return 1

} else {

return n * factorial(n -1)

}

}

但是递归函数存在着终止条件不明确或缺少终止条件,导致函数长时间运行,使得用户界面处于假死状态。而且递归还可能遇到浏览器的“调用栈大小限制(Call stack size limites)”。

调用栈限制

JS引擎支持的递归数量与JS调用栈大小直接相关。只有IE的调用栈与系统空闲内存有关,其他浏览器都是固定数量的调用栈限制。

当使用太多的递归(或者死循环),甚至超过最大调用栈限制时,就会出现调用栈异常。各浏览器报错信息如下:

IE: Stack overflow at line x

Firefox: Too much recursion

Safari: Maximum call stack size exceeded

Opera: Abort (control stack overflow)

Chrome: 不显示调用栈溢出错误

try-catch 可以捕获。

递归模式

1.函数调用自身,如之前说的阶乘。

2.隐伏模式,即循环调用。A调用B,B又调用A,形成了无限循环,很难定位。

由于递归的这些隐藏危害(出现问题很难定位),建议使用迭代、Memoization替代。

迭代

任何递归实现的算法,同样可以使用迭代来实现。迭代算法通常包含几个不同的循环,分别对应计算过程的不同方面,这也会引入他们自身的性能问题。使用优化后的循环替代长时间运行的递归函数可以提升性能。

以合并排序算法为例

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));
};

此算法中mergeSort存在频繁的递归调用,当数组长度为n时,最终会调用2*n-1次,很容易造成栈溢出错误。

使用迭代改进此算法。mergeSort代码如下:

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];
}

改进之后没有使用 递归 实现要比递归慢一些,但不会受调用栈限制的影响。

Memoization

就是缓存前一次的计算结果避免重复计算。

function factorial (n) {
if (n == 0) {
return 1
} else {
return n * factorial(n -1)
}
} var fact6 = factorial(6);
var fact5 = factorial(5);
var fact4 = factorial(4);

三个阶乘,共需要执行factorial函数18次。其实计算6的阶乘的时候,已经计算过5和4的阶乘。特别是4的阶乘被计算了3次。

利用Memoization技术重写factorial函数,代码如下:

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];
}

这是再执行6,5,4的阶乘,实际只有6的阶乘进行了递归计算,共执行factorial函数8次。5和4的阶乘直接中缓存里取出结果。

小结:

JavaScript 和其它编程语言一样,代码的写法和算法会影响运行时间。与其它语言不同的是,JavaScript可用资源有限,因此优化技术更为重要。

1.for、while、do-while 循环性能相当,并没有一种明显快于或鳗鱼其它类型。

2.避免使用 for-in 循环,除非你需要遍历一个属性数量未知的对象。

3.通常改善性能的最佳方式是减少每次迭代的运算量和减少循环迭代次数。

4.通常来说,switch 总是比 if-else 快,但并不是最佳解决方案,当判断条件较多时,使用查找表比 if-else 和 switch 更快。

5.浏览器的调用栈大小限制了递归算法在JavaScript中的应用,栈溢出错误会导致其它代码中断运行。

6.可以使用迭代算法,或使用 Memoization 来避免重复计算。

运行的代码量越大,使用这些策略所带来的性能提升也越明显。

高性能JavaScript(算法和流程控制)的更多相关文章

  1. javascript高性能编程-算法和流程控制

          代码整体结构是执行速度的决定因素之一. 代码量少不一定运行速度快, 代码量多也不一定运行速度慢. 性能损失与代码组织方式和具体问题解决办法直接相关.       倒序循环可以提高性能,如: ...

  2. 高性能JavaScript笔记二(算法和流程控制、快速响应用户界面、Ajax)

    循环 在javaScript中的四种循环中(for.for-in.while.do-while),只有for-in循环比其它几种明显要慢,另外三种速度区别不大 有一点需要注意的是,javascript ...

  3. JavaScript运算符与流程控制

    JavaScript运算符与流程控制 运算符 赋值运算符 使用=进行变量或常量的赋值. <script> ​ let username = "YunYa"; ​ < ...

  4. 高性能javascript学习笔记系列(4) -算法和流程控制

    参考高性能javascript for in 循环  使用它可以遍历对象的属性名,但是每次的操作都会搜索实例或者原型的属性 导致使用for in 进行遍历会产生更多的开销 书中提到不要使用for in ...

  5. JavaScript学习笔记——流程控制

    javascript流程控制流程:就是程序代码的执行顺序.流程控制:通过规定的语句让程序代码有条件的按照一定的方式执行. 一.顺序结构 按照书写顺序来执行,是程序中最基本的流程结构. 二.选择结构(分 ...

  6. Javascript初识之流程控制、函数和内置对象

    一.JS流程控制 1. 1.if else var age = 19; if (age > 18){ console.log("成年了"); }else { console. ...

  7. JavaScript之if流程控制演练,if写在区间内怎么解决

    什么是编程?通俗意见上来讲,就是把人的思维与步骤通过代码的形式书写展示出来,JavaScript的流程控制包含条件判断if,switch选择,循环for while:if(表达式 条件)=>真{ ...

  8. JavaScript 运算,流程控制和循环

    算数运算符 算术运算符 描叙 运算符 实例 加 + 10 + 20 = 30 减 - 10 – 20 = -10 乘 * 10 * 20 = 600 除 / 10 / 20 = 0.5 取余数 % 返 ...

  9. 算法和流程控制 --《高性能JavaScript》

    总结: 1.for, while, do-while循环性能相当,并没有一种循环类型明显快于或满于其他类型. 2.避免使用for-in循环,除非要遍历一个属性数量未知的对象. 3.改善循环性能的最佳形 ...

随机推荐

  1. 对于opencv全面貌的认识和理解

    1.opencv其实最开始只有源码,也就是sources中的代码,sources中有个modules,进入里面是各个我们平常使用的模块,如下图. 进入任意一个模块,比如calib3d,其中有inclu ...

  2. 【xsy2506】 bipartite 并查集+线段树

    题目大意:有$n$个点,你需要操作$m$次.每次操作为加入/删除一条边. 问你每次操作后,这$n$个点构成的图是否是二分图. 数据范围:$n,m≤10^5$. 此题并没有强制在线,考虑离线做法. 一条 ...

  3. Metasploit数据库问题汇总

    数据库在metaspoit中是相当重要的,当做一个大型渗透测试项目的时候,收集到的信息是相当大的,当和你的同伴一起协同作战的时候,你们可能 在不同的地方,所以数据共享很重要了!而且Metasploit ...

  4. vector源码(参考STL源码--侯捷):空间分配导致迭代器失效

    vector源码1(参考STL源码--侯捷) vector源码2(参考STL源码--侯捷) vector源码(参考STL源码--侯捷)-----空间分配导致迭代器失效 vector源码3(参考STL源 ...

  5. 源码分析篇 - Android绘制流程(一)窗口启动流程分析

    Activity.View.Window之间的关系可以用以下的简要UML关系图表示,在这里贴出来,比较能够帮组后面流程分析部分的阅读. 一.Activity的启动流程 在startActivity() ...

  6. Java堆外内存之突破JVM枷锁

    对于有Java开发经验的朋友都知道,Java中不需要手动的申请和释放内存,JVM会自动进行垃圾回收:而使用的内存是由JVM控制的. 那么,什么时机会进行垃圾回收,如何避免过度频繁的垃圾回收?如果JVM ...

  7. [Python学习笔记-006] 使用stomp.py校验JMS selector的正确性

    了解Jenkins的人都知道,JMS selector是基于SQL92语法实现的,本文将介绍使用stomp.py和ActiveMQ来校验JMS selector的正确性. Q: 什么是stomp.py ...

  8. 代理(Proxy)模式 ,桥梁(Bridge)模式

    一:代理模式 1 根据名字我们就可以理解为:代替别人管理 2 什么情况下使用代理模式呢? 在软件系统中,有些对象有时候由于跨越网络或者其他的障碍,而不能够或者不想直接访问另一个对象,如果直接访问会给系 ...

  9. Spring中使用变量${}的方式进行参数配置

    在使用Spring时,有些情况下,在配置文件中,需要使用变量的方式来配置bean相关属性信息,比如下面的数据库的连接使用了${}的方式进行配置,如下所示: <bean id="data ...

  10. Flux --> Redux --> Redux React 基础实例教程

    本文的目的很简单,介绍Redux相关概念用法 及其在React项目中的基本使用 假设你会一些ES6.会一些React.有看过Redux相关的文章,这篇入门小文应该能帮助你理一下相关的知识 一般来说,推 ...