第4章:算法与流程控制

循环的类型

for循环是JS中最常用的循环结构。它由四部分组成:初始化、前测条件、后执行体、循环体。

for循环初始化中的var语句创建一个函数级的变量,JS只有函数级作用域,因此在for循环中定义一个变量相当于在循环体外定义一个新变量。

循环性能

在JS提供的四种循环类型中,只有for-in循环比其他明显要慢。

由于每次迭代操作同时搜索实例或原型属性,for-in循环的每次迭代都会产生更多开销,所以比其他循环类型要慢。

不要使用for-in来遍历数组成员。

如果循环类型与性能无关,有两个可选因素:

  1. 每次迭代处理的事物
  2. 迭代的次数

减少迭代的工作量

一个提升循环整体速度的好方法是限制循环中耗时操作的数量。

优化循环的第一步是要减少对象成员及数组项的查找次数。

基于函数的迭代

原生数组方法:forEach(),此方法遍历一个数组的所有成员,并在每个成员上执行一个函数。要运行的函数作为参数传给forEach(),并在调用时接收三个参数,分别是:当前数组项的值、索引以及数组本身。

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

尽管基于函数的迭代提供了一个更为便利的迭代方法,但它仍然比基于循环的迭代要慢一些。对每个数组项调用外部方法所带来的开销是速度慢的主要原因。在所有情况下,基于循环的迭代比基于函数的迭代快8倍。

条件语句

if-else对比switch

使用if-else还是switch,最流行的方法是基于测试条件的数量来选择:条件数量越大,越倾向于使用switch

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

优化if-else

优化if-else的目标是:最小化到达正确分支前所需判断的条件数量。最简单的优化方法是确保最可能出现的条件放在首位。

if-else中的条件语句应该总是按照从最大概率到最小概率的顺序排列,以确保运行速度最快。

另一种减少条件判断次数的方法是把if-else组织成一系列嵌套的if-else语句。使用单个庞大的if-else通常会导致运行缓慢,因为每个条件都需要判断。

查找表

有时候优化条件语句的最佳方案是避免使用if-elseswitch。当有大量离散值需要测试时,if-elseswitch比使用查找表慢很多。JS中可以使用数组和普通对象来构建查找表,通过查找表访问数据会快很多。

当使用查找表时,必须完全抛弃条件判断语句。这个过程变成数组项查询或对象成员查询。查找表的一个主要有点是:不用书写任何条件判断语句,即便候选值数量增加时,也几乎不会产生额外的性能开销。

递归

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

调用栈限制

JS引擎支持的递归数量与JS调用栈大小直接相关。只有IE例外,它的调用栈与系统空闲内存有关。

递归模式

当遇到调用栈大小限制时,第一步先检查代码中的递归实例。

有两种递归模式值得注意:第一种是直接递归模式,第二种是隐伏模式。大多数调用栈错误都与这两种模式有关。最常见的导致栈溢出的原因是不正确的终止条件,因此定位模式错误的第一步是验证终止条件。

迭代

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

Memoization

减少工作量是最好的优化技术。Memoization是一种避免重复工作的方法,它缓存前一个计算结果供后续计算使用,避免了重复工作。