JavaScript中对循环语句的优化技巧深入探讨

循环是所有编程语言中最为重要的机制之一,几乎任何拥有实际意义的计算机程序(排序、查询等)都里不开循环。 而循环也正是程序优化中非常让人头疼的一环,我们往往需要不断去优化程序的复杂度,却因循环而纠结在时间复杂度和空间复杂度之间的抉择。

在 javascript 中,有3种原生循环,for () {}, while () {}和do {} while (),其中最为常用的要数for () {}。

然而for正是 javascript 工程师们在优化程序时最容易忽略的一种循环。

我们先来回顾一下for的基本知识。
javascript 的for语法继承自c语言,for循环的基本语法有两种使用方法。

1. 循环数组

for循环的基本语法

复制代码 代码如下:


for ( /* 初始化 */2 /* 判断条件 */2 /* 循环处理 */ ) {
  //... 逻辑代码
}

我们以一段实例代码来进行详细说明。

复制代码 代码如下:


var array = [1, 2, 3, 4, 5];
var sum   = 0;

for (var i = 0, len = array.length; i < len; ++i) {
  sum += array[i];
}

console.log('The sum of the array\'s items is %d.', sum);
//=> The sum of the array's items is 15.

在这段代码中,我们首先定义并初始化了一个用存储待累加项的数组和一个总和整形变量。 接下来,我们开始进行循环。在该for循环的初始化代码中,我们也定义并初始化了两个变量: i(计数器)和len(循环数组长度的别名),当i小於len时,循环条件成立,执行逻辑代码;每次逻辑代码执行完毕以后,i自增1。

在循环的逻辑代码中,我们把当前循环的数组项加到总和变量中。
这个循环用流程图表示为如下:

JavaScript中对循环语句的优化技巧深入探讨



从这个流程图中我们不难发现,程序中真正的循环体不仅有我们的逻辑代码,还包含了实现循环自身的执行判断和循环处理。
这样,我们的优化思路就清晰了,我们可以从四个方面进行优化。

1.循环体前的初始化代码
2.循环体中的执行判断条件
3.逻辑代码
4.逻辑代码后的处理代码

ps: 其中第一点和第二点存在重要关系。


1.1 优化初始化代码和执行判断条件

我们先来看看一段大家都非常熟悉的代码。

复制代码 代码如下:


// wrong!
for (var i = 02 i < list.length2 ++i) {
  //... 逻辑代码
}


相信现在大部分写着 javascript 的工程师依然使用着这段看似狠正常的循环方法,但为什麼我在这里说它是错误的呢?
我们把这个循环的所有东西都拆开来看看:

1.初始化代码 - 这段循环只定义并初始化了一个计数器变量。
2.执行判断条件 - 当计数器小於list的长度时成立。
3.处理代码 - 计数器自增1。

我们再回顾一下上面的流程图,发现有什麼倪端没?
真正的循环体不仅有我们的逻辑代码,还包含了实现循环自身的执行判断和处理代码。 也就是说,i < list.length这个判断条件是每一次循环前都要执行的。而 javascript 中,对对象的属性或方法进行读取时,需要进行一次查询。
似乎明白了点什麼了吧?这个判断条件存在两个操作:1. 从list数组中查询length属性;2. 比较i与list.length的大小。
假设list数组含有 n 个元素,则程序需要在这个循环的执行判断中进行 2n 次操作。

如果我们把代码改成这样:

复制代码 代码如下:


// Well
for (var i = 0, len = list.length; i < len; ++i) {
  //...
}

在这段改进后的代码中,我们在循环体执行前的初始化代码中, 增加定义并初始化了一个len变量,用於存储list.length的值(关於变量、表达式、指针和值的相关内容将在第二篇中讨论)。 这样,我们在循环体中的执行判断中就无需再次对list数组进行属性查询,操作数为原先的一半。

以上步骤我们完善了算法的时间复杂度,而如果要继续优化空间复杂度的话,要如何做呢? 如果你的逻辑代码不受循环顺序限制,那你可以尝试以下优化方式。

复制代码 代码如下:


for (var i = list.length - 1; i >= 0; --i) {
  //...
}

这段代码通过把循环顺序倒置,把i计数器从最后一个元素下标(list.length - 1)开始,向前循环。 以达到把循环所需变量数减到 1 个,而且在执行判断中,降低了变量查询的次数,减少了执行 cpu 指令前的耗时。

1.2 优化逻辑代码

在循环中,我们得到循环当前的数组元素自然是为了对其或利用其进行一些操作,这不免会出现对该元素数次的调用。

复制代码 代码如下:


var array = [
  { name: 'Will Wen Gunn', type: 'hentai' },
  { name: 'Vill Lin', type: 'moegril' }
];

for (var i = array.length - 1; i >= 0; --i) {
  console.log('Name: %s', array[i].name);
  console.log('He/She is a(n) %s', array[i].type);

console.log('\r\n');
}
/*=>
  Name: Vill Lin
  He/She is a(n) moegril

Name: Will Wen Gunn
  He/She is a(n) hentai
 */

这段代码中,程序需要对每个数组元素的name和type属性进行查询。 如果数组有 n 个元素,程序就进行了 4n 次对象查询。

复制代码 代码如下:


1. array[i]
2. array[i].name
3. array[i]
4. array[i].type

相信此时你一定想到了解决方法了吧,那就是把当前数组元素的值赋值到一个变量中,然后在逻辑代码中使用它。

复制代码 代码如下:


var array = [
  { name: 'Will Wen Gunn', type: 'hentai' },
  { name: 'Vill Lin', type: 'moegril' }
];
var person = null;

for (var i = array.length - 1; i >= 0 && (person = array[i]); --i) {
  console.log('Name: %s', person.name);
  console.log('He/She is a(n) %s', person.type);

console.log('\r\n');
}
person = null;

这样看起来的确美观了不少。

复制代码 代码如下:


 1. array[i] => var person
 2. person.name
 3. person.type

有点像 emcascript5 中的foreach,不过这两者之间差别狠大,这里不多做解释。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wgpjgz.html