高性能JavaScript(算法和流程控制)
在大多与编程语言中,代码的执行时间大部分消耗在循环中,是提升性能必须关注的要点之一
循环的类型
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 < 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(算法和流程控制)的更多相关文章
- javascript高性能编程-算法和流程控制
代码整体结构是执行速度的决定因素之一. 代码量少不一定运行速度快, 代码量多也不一定运行速度慢. 性能损失与代码组织方式和具体问题解决办法直接相关. 倒序循环可以提高性能,如: ...
- 高性能JavaScript笔记二(算法和流程控制、快速响应用户界面、Ajax)
循环 在javaScript中的四种循环中(for.for-in.while.do-while),只有for-in循环比其它几种明显要慢,另外三种速度区别不大 有一点需要注意的是,javascript ...
- JavaScript运算符与流程控制
JavaScript运算符与流程控制 运算符 赋值运算符 使用=进行变量或常量的赋值. <script> let username = "YunYa"; < ...
- 高性能javascript学习笔记系列(4) -算法和流程控制
参考高性能javascript for in 循环 使用它可以遍历对象的属性名,但是每次的操作都会搜索实例或者原型的属性 导致使用for in 进行遍历会产生更多的开销 书中提到不要使用for in ...
- JavaScript学习笔记——流程控制
javascript流程控制流程:就是程序代码的执行顺序.流程控制:通过规定的语句让程序代码有条件的按照一定的方式执行. 一.顺序结构 按照书写顺序来执行,是程序中最基本的流程结构. 二.选择结构(分 ...
- Javascript初识之流程控制、函数和内置对象
一.JS流程控制 1. 1.if else var age = 19; if (age > 18){ console.log("成年了"); }else { console. ...
- JavaScript之if流程控制演练,if写在区间内怎么解决
什么是编程?通俗意见上来讲,就是把人的思维与步骤通过代码的形式书写展示出来,JavaScript的流程控制包含条件判断if,switch选择,循环for while:if(表达式 条件)=>真{ ...
- JavaScript 运算,流程控制和循环
算数运算符 算术运算符 描叙 运算符 实例 加 + 10 + 20 = 30 减 - 10 – 20 = -10 乘 * 10 * 20 = 600 除 / 10 / 20 = 0.5 取余数 % 返 ...
- 算法和流程控制 --《高性能JavaScript》
总结: 1.for, while, do-while循环性能相当,并没有一种循环类型明显快于或满于其他类型. 2.避免使用for-in循环,除非要遍历一个属性数量未知的对象. 3.改善循环性能的最佳形 ...
随机推荐
- 关于OC中的几种延迟执行方式
第一种: [UIView animateWithDuration: delay: options: animations:^{ self.btn.transform = CGAffineTransfo ...
- Mysql 索引原理及优化
本文内容主要来源于互联网上主流文章,只是按照个人理解稍作整合,后面附有参考链接. 一.摘要 本文以MySQL数据库为研究对象,讨论与数据库索引相关的一些话题.特别需要说明的是,MySQL支持诸多存储引 ...
- War文件部署
其实,开始要求将源码压缩成War文件时,一头雾水! 公司项目要求做CAS SSO单点登录 也就是这玩意.... 其实war文件就是Java中web应用程序的打包.借用一个老兄的话,“当你一个web应用 ...
- Error:Could not determine the class-path for interface com.android.builder.model.AndroidProject.
Android Studio导入Eclipse项目报错Error:Could not determine the class-path for interface com.android.builde ...
- 读书笔记(02) - 可维护性 - JavaScript高级程序设计
编写可维护性代码 可维护的代码遵循原则: 可理解性 (方便他人理解) 直观性 (一眼明了) 可适应性 (数据变化无需重写方法) 可扩展性 (应对未来需求扩展,要求较高) 可调试性 (错误处理方便定位) ...
- jsp链接orcl
自己整的!好用滴!!希望能帮到一些初学者! package lobsterwwww; import java.sql.Connection; import java.sql.DriverManager ...
- Spring Log4j2 log4j2.xml
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-c ...
- BEA-290074 <Deployment service servlet received file download request for file "security/SerializedSystemIni.dat". The file may exist, but download of this file is not allowed.>
Bug 19766239 - WLS 12.1.3 - MS NOT STARTING - 'DOWNLOAD OF THIS FILE IS NOT ALLOWED' Issue is fixed ...
- Scrum 冲刺博客
博客链接集合 Alpha阶段敏捷冲刺 敏捷冲刺一 敏捷冲刺二 敏捷冲刺三 敏捷冲刺四 敏捷冲刺五 敏捷冲刺六 敏捷冲刺七 Alpha阶段敏捷冲刺总结 Alpha阶段敏捷冲刺总结
- PowerBuilder编程新思维1:扩展(Lua)
前言 PowerBuilder作为开发工具退出一线行列已经很久了,在2019年来谈这样一款老旧的编程工具是否有意义?诚然,PB有着太多硬伤,但还是有它的用武之地的.而且今天讲的这个“新思维”大部分内容 ...