高性能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.改善循环性能的最佳形 ...
随机推荐
- OS之内存管理 --- 虚拟内存管理(一)
虚拟内存的背景 在基本的内存管理策略中,所有的策略的相同点都是:每个进程在执行之前需要完全处于内存中.那有没有一种方法可以不需要将进程所有页面加载到内存中就可以开始运行进程呢?有没有可能在进程需要某些 ...
- IIS 301重定向 报错 地址后面有eurl.axd
错误发生的原因是当ASP.NET检测到Web站点配置为使用ASP.NET 4.0,本地ASP.NET 4.0 的组件会传递一个不能扩展的 URL到ASP.NET的管理程序作进一步处理.但是,如果一个低 ...
- odoo开发 相关知识点
(1)导入模块可以起别名: (2) 新的模型前端要调用显示有关联的另一个模型的相关字段 (3) 传递上下文 搜索视图打开默认按照接收的参数搜索显示: 发起端视图 上下文写法: 目标端 触发显示,搜索视 ...
- Win7删除网络位置那些不用的网络位置(驱动器)
1.初始状态: 映射成功的网络位置如下图 2.要删除这个网络位置:点击"打开网络和共享中心",然后如下图设置: 3.重启电脑之后,删除的"网络位置"不会在资源管 ...
- Mysql的优化一则
目的在于这么一个sql语句: SELECT w.* FROM wallpaper w inner join wallpaper_category_relation r ON w.wallpaper_i ...
- Flyweight享元模式(结构型模式)
1.面向对象的缺点 虽然OOP能很好的解决系统抽象的问题,并且在大多数的情况下,也不会损失系统的性能.但是在某些特殊的业务下,由于对象的数量太多,采用面向对象会给系统带来难以承受的内存开销.示例代码如 ...
- 微信正式开放内测“小程序”,不开发APP的日子真的来了?
关注,QQ群,微信应用号社区 511389428 微信正式开放内测“小程序”,不开发APP的日子真的来了? 明星公司 缪定纯 • 2016-09-22 09:05 讨论了很久的微信应用号终于来了,不过 ...
- dockerfile简述
作用 Dockerfile的内容是一坨可以执行的代码(或者说是指令)(docker的DSL),这些代码使得创建镜像的操作可以复用以及自动化. 指令格式 Dockerfile的指令格式很简单: INST ...
- JVM(二)JVM的结构
1.JVM的结构包括 (1)类加载器 (2)执行引擎 (3)运行时数据区 (4)本地库接口 类加载器:包括启动类加载器.扩展类加载器.应用程序类加载器.自定义加载器. 执行引擎:在执行JAVA代码的时 ...
- 自制“低奢内”CSS3注册表单,包含JS验证哦。请别嫌弃,好吗?。
要求 必备知识 基本了解CSS语法,初步了解CSS3语法知识.和JS/JQuery基本语法. 开发环境 Adobe Dreamweaver CS6 演示地址 演示地址 预览截图(抬抬你的鼠标就可以看到 ...