五、运算符、循环和计时器

在前几章中,我们回顾了 JavaScript 开发中使用的基本工具。 我们研究了 ide、代码编辑器和JSLint,这是一个 JavaScript 代码验证器,它不仅向我们展示了代码中包含的问题,而且还为我们提供了关于如何改进代码的警告和建议。

我们还学习了console.timeconsole.timeEnd方法,它们允许我们快速测试代码执行性能。 最后,我们学习了如何创建一个基本的构建系统,以确保我们的最终代码库是优化的和无 bug 的。

重要的是,所有这些工具和技术对于编写高性能代码是必不可少的,不是因为您知道的 JavaScript,而是因为您不知道的 JavaScript。 JavaScript 是一种任何人都可以学会并开始编写代码的语言,而不需要知道面向对象编程或知道模式,如Model View Controller(MVC); 然而,随着时间的推移,它被修改以适应这些高级编程概念(黑客或其他)。

易于使用的语言的另一面是,它很容易编写错误,甚至是非优化代码; 如果我们在编写复杂的 JavaScript,这种效果会增加一倍甚至三倍。 正如前面章节所提到的,JavaScript 开发人员的一个普遍特征是:我们是人,我们会犯错。 这在很大程度上只是开发人员缺乏意识,这就是为什么在我们编写完美的高性能 JavaScript 之前,使用构建系统和代码检查器(如 JSLint)是如此重要; 如果我们不这样做,这些工具就会覆盖我们。

在本章中,我们将把工具和构建系统放在后面,并将深入探讨 JavaScript 性能的概念,将主题分成两章,从以下主题开始:

  • 运营商
  • 循环
  • 计时器

运算符

在本节中,我们将学习使用比较运算符创建for循环的有效方法。

比较运算符

比较运算符==是 JavaScript 开发中常见的运算符(通常在if语句中); 它将一个对象与另一个对象等同,并返回一个布尔值(truefalse)。 它非常简单,在基于 c 语言的语言中非常常见。

因此,很容易利用这个运算符并在大型代码库中使用它。 实际情况是,与使用===严格比较运算符相比,equals 运算符速度较慢,===严格比较运算符也比较对象类型和对象值。 由于 JavaScript 解释器在检查相等之前不需要确认类型,所以它的操作速度比使用 double equals 运算符快。

严格更快吗?

让我们用和console.time方法来测试一下。 在下面的截图中,我们有一个05_01.js代码示例; 我们也可以在 Packt Publishing 网站上提供的这本书的示例文件中看到这个示例:

Is strict faster?

这里,我们在第 5、6 和 7 行有三个变量; 其中两个变量是引用 PI 值的浮点数,最后一个变量是具有相同 PI 值的字符串。 然后在第 12 行有一个带有test变量的匿名函数,它用一个双等号运算符将两个浮点数等价。 围绕这个函数,在第 9 行和第 14 行分别有console.timeconsole.timeEnd函数。

让我们在 Chrome 浏览器中运行; 打开 Chrome 紧随其后的是开发工具从更多的工具选择about:blank选项卡,然后打开【显示】选项卡片段在右边列面板来源。 Snippets选项卡就像一个用于测试 JavaScript 代码的刮板; 右键单击标签内容区域,选择New。 保存带有名称的代码片段,并从示例中复制代码,如下面的截图所示:****

**Is strict faster?

接下来,右键单击左侧栏中的代码片段并单击Run。 您会注意到控制台出现在开发人员工具窗口的底部。 我们还可以看到一条Check PI: 0.016ms控制台消息。 这向我们展示了在这个简单的求值上运行比较运算符需要 0.016 毫秒。 如果我们将比较运算符改为严格比较运算符来查看结果会是什么呢?

在更改运算符时,我们可以看到第二个console.time消息是Check PI: 0.007ms。 当然,这是一个简单的示例,但它证明了使用严格类型检查和严格比较运算符运行代码更快。

Is strict faster?

循环

在本节中,我们将详细学习创建for循环的有效方法。

循环如何影响性能

循环是一种非常常见的遍历大数据或对象的方法,并遍历 DOM 对象或数据段的每个实例。 假设我们有一个简单的循环,它生成一个p段落标记,并在循环中附加一个i整数的内部文本值,最大限制为9000。 让我们看一下下面的代码示例,看看这是如何实现的。 我已经创建了一个简单的 HTML5 页面,带有一个script标签,其中包括第 10 行代码,如下所示:

How loops affect performance

那么,这个代码过程是如何密集的呢? 对于初学者来说,如果我们查看第 17 行,我们可以看到一个名为ptag的变量,它是用来在 DOM 中创建空白段落标记的。 然后在循环中将整数的当前值应用于ptag变量的innerText属性; 最后,我们使用在循环中指定的值将新创建的段落标记应用到 DOM 中。 对于性能测试,我们还将for循环封装在console.time包装器方法中,以检查性能速度。 如果我们在 Chrome 中运行这个,我们应该得到一个页面,其中一行是在for循环中创建的每个数字,还有一个console.time方法和一个process time标签,如下图所示:

How loops affect performance

看看我们的process time标签,我们可以看到处理这段代码大约需要 18 毫秒。 这并不伟大,但我们可以让它变得更好; 让我们更新代码,将ptag变量和i整型变量移到for循环之外,这样就不会在for循环的每次迭代中重新创建它们。 让我们通过更新代码来看看这是什么样子,如下面的截图:

How loops affect performance

注意,在第 16 行中,我们将iptag变量移到了循环之外,我们重新分配了在循环中创建的值和对象,而不是为每次循环创建一个唯一的作用域。 如果我们重新运行页面,我们应该会看到相同的 body 标签得到了更新,但性能值比以前略小; 在下列情况下,它应该在 15-17 毫秒范围内运行:

How loops affect performance

反循环性能神话

在 JavaScript 开发者圈子里出现了一个新的想法,那就是反向for循环的概念。 一个反向for循环就像一个循环,但是循环是向后计数而不是向前计数。

反向循环背后的思想是,通过向后计数,一些 JavaScript 解释器运行循环更快。 让我们测试一下,看看这是否真的提高了for循环的速度。 首先,让我们创建一个for循环,从9000开始向前计数; 除了添加一个名为result的外部变量外,我们不会在for循环中包含任何逻辑。

反向循环和标准for循环中,使用带有递增的result变量,我们可以确定是否按照应该的方式计数,并在9000结束时触发一行代码。 在我们的例子中,一个console.timeEnd函数,如下面的代码所示,位于它自己的 HTML 页面中,底部有一个 script 标记。

The reverse loop performance myth

让我们看看代码示例。 在第 13 行,我们可以看到,我们在开始for循环之前声明了result变量,而在第 14 行,我们启动了具有Time Up标签的console.time包装器方法。 在第 15 行,我们启动for循环,并在第 16 行增加result。 最后,在第 18 行,我们有一个条件,我们询问结果是否等于 9000,并在第 19 行执行我们的timeEnd函数。

如果我们使用body标签中的for循环脚本加载页面,那么开发人员工具中的控制台应该输出以下信息:

The reverse loop performance myth

因此,我们的console.time对象告诉我们,我们的标准for循环的最大值9000大约需要 0.15 毫秒来处理谷歌 Chrome。 在 HTML 页面中不包含任何其他内容(它不在服务器上托管),可以确保网络延迟不是一个因素。 这是一个很好的基线,我们可以用它来比较反向循环。

现在,让我们测试一个反向for循环; 在这里,我们创建了一个更新版本的for循环,包括我们的result变量。 这与前面的过程类似,但让我们看看下一个截图中的代码示例:

The reverse loop performance myth

如果我们看一下代码示例中的第 15 行,我们可以看到我们对这一行做了一点修改,这样循环计数是向后的而不是向前的。 我们首先设置增量变量i(在本例中),其值为9000,然后测试i是否大于0。 如果是,则将i值降低 1。

在第 17 行,我们仍然像以前一样增加result变量。 这样,不是使用for循环的递减变量iresult变量作为循环外的计数存在,向上计数。 这被称为反向循环。 当在第 18 行中result=9000时,在第 19 行中执行console.timeEnd函数。

让我们在 Chrome 浏览器的开发工具选项中测试,看看我们得到了什么价值,如下所示:

The reverse loop performance myth

因此,我们可以看到开发工具产生的结果,而反向循环的处理时间大约是 0.16 毫秒,与for循环相比,这并没有太大的差异。 在许多情况下,反向for循环对于大多数 JavaScript 项目来说并不是必需的,除非我们需要向后计算一个项目。

定时器

在这里,我们将详细学习优化 JavaScript 计时器。

什么是计时器,它们如何影响性能?

计时器是 JavaScript 的内置函数,它允许执行内联 JavaScript 代码或允许函数在 JavaScript 应用生命周期之后或期间的特定时间点被调用。

计时器是 JavaScript 开发人员的一个很好的工具,但在性能方面它们也有自己的问题。 考虑 JavaScript 语言是单线程的这一事实,这意味着应用中的每一行代码不能与应用中的另一段代码完全同时触发。 为了解决这个问题,我们使用了一个名为setTimeout的内置函数。

setTimeout方法采用两个参数来延迟代码块的执行; 第一个是带有代码的函数名,或者是一行 JavaScript 代码本身,后面跟着一个整数,该整数指定我们希望延迟代码执行的范围,以毫秒为单位。

表面上看,setTimeout功能似乎无害,但考虑一下这个。 假设我们有两个函数,都由一个setTimeout函数触发,每个for循环将for循环的递增值打印到控制台窗口。 每个函数都有不同的最大值,在for循环的第一个较大的函数之后,将稍微调用较低的计数函数。 让我们看看这里的代码示例:

What are timers and how do they affect performance?

我们可以看到这是一个空的 HTML5 页面,带有脚本标签,第 9 行是我们的代码。 在第 13 行和第 20 行,我们有两个类似的函数的开头:一个叫delay300000(),另一个叫delay3000(),每个函数都包含一个for循环,使用console.info语句将循环的每个步骤打印到控制台。 console.info语句是一种控制台打印,它只是格式化控制台行以指示信息。

现在,在第 27 行,我们将在window.onload函数中触发这两个函数,较大的延迟函数在页面加载后 50 毫秒被调用,较短的函数在 150 毫秒被调用。 让我们在 Chrome 中尝试一下,看看在 Dev Tools 中会发生什么,如下所示:

What are timers and how do they affect performance?

在这里,当我们将所有这些行打印到控制台时,我们可以注意到相当的延迟。 我们还可以看到,我们在给定的超时时间内触发了这两种情况。 在上面的截图中,我们可以看到我们的delay3000()直到我们的更大的功能delay300000()完成后才被触发。

单螺纹工作

遗憾的是,在普通的 JavaScript 中,我们根本无法同时“多线程”这两个函数,但我们可以在代码中合并一些callback方法。 callback方法只是一个 JavaScript 函数,当函数完成时触发。 让我们设置我们的delay300000()函数,以便在delay3000()方法完成后调用它。 这是它的样子:

Working around single-threading

看看我们的代码示例,我们可以在第 13 行看到我们添加了一个名为callback的参数。 重要的是要知道,在这里,我们的callback方法的命名并不重要,但包含一个函数的占位符参数是重要的。 作为回调函数的占位符函数是Delay3000()

注意我们如何在第 22 行重命名Delay3000,并将d大写。 这样做的目的是向 JavaScript 解释器表明这是一个构造器,这个函数需要在内存中初始化。 这可以通过将函数名的第一个字母大写来实现。 你可能会想起在第二章【显示】,提高代码性能 JSLint,如果我们用大写函数名 JSLint 将返回一个警告说它“认为”构造器使用即使是一个普通的函数。 为了防止解释器对自己进行事后批评,我们希望确保我们按照预期编写函数和对象。

最后,我们更新了onload函数的逻辑,删除了delay3000额外的setTimeout,并在setTimeout函数的delay300000()函数中添加了新命名的Delay3000(没有括号)作为参数。 让我们在浏览器中再次运行它,并查看控制台的输出。

如果我们向下滚动靠近控制台日志底部的(在处理初始delay300000()函数调用之后),我们可以看到在完成初始函数之后出现了Delay3000日志消息。 使用回调函数是一种有效管理应用线程的好方法,可以确保重载应用的正确加载,允许在初始函数完成后传递参数。

关闭循环

最后,正如我们在这个callback方法示例中所看到的,出于性能原因,使用大量伸缩循环通常不是一个好主意。 总是寻找更好、更有效的方法来打破大的循环,并调用其他函数来帮助平衡工作负载。

此外,我鼓励您查看 JavaScriptpromises,这是 EcmaScript 6 的一个特性。 虽然在本书中还没有完全准备好讨论,但在写作时,承诺仍是实验性的。 亲爱的读者,我鼓励您继续跟踪并了解 JavaScript 回调的后续将是什么。 您可以在 Mozilla 的开发者网络网站https://developer.mozilla.org/en-US/了解更多关于的承诺。

小结

在本章中,我们学习了条件,以及严格比较如何有效地帮助 JavaScript 在运行时更好地执行。 我们还学习了循环和如何优化循环,防止我们的代码库不需要的对象在for循环中被反复重复,从而使我们的代码尽可能地高效。

最后,我们还学习了 JavaScript 应用中的计时器和单线程,以及如何使用回调来保持代码在重载时尽可能平稳地运行。 接下来,我们将讨论数组和原型创建性能,并找出如何在 JavaScript 中最好地使用它们。**