六、构造器、原型和数组

现在,我们已经习惯于在不使用检查器或 IDE 测试代码的情况下优化 JavaScript,现在是时候深入研究更复杂的优化了,特别是涉及到内存和对象创建的时候。 在本章中,我们将讨论如何使用构造器、原型和数组来优化更大的 JavaScript 代码库。

我们计划在本章中涵盖以下主题:

  • 使用构造器和实例函数进行构建
  • 使用原型的备用构造器
  • 阵列的性能

使用构造器和实例函数构建

在这里,我们将以以下方式学习使用构造器和实例函数构建:

简单说句话

根据技能水平的不同,我们中的一些人可能知道,也可能不知道 JavaScript 中的原型是什么。 如果您是听说过 JavaScript 中的原型但没有在日常生活中使用它们的读者之一,那么您不必担心,因为我们将快速介绍基本概念以及如何将它们应用于 JavaScript 性能。

如果你知道闭包,继承,父母和孩子关系,等等是什么,觉得你属于后一类,所以想跳过这一章,我鼓励你继续阅读,至少浏览这一章, 我们作为 JavaScript 开发人员,在使用 JavaScript 多年的过程中,往往会忘记一些常见的概念,而持续关注那些影响性能的因素。

关心和提供函数名

仔细看看下面所示的这个简单的函数,看看您是否发现这个函数有什么不寻常之处。

The care and feeding of function names

现在,当我们查看代码时,我们可以看到一个名为AuthorName的简单函数,它持有author参数。 函数使用一个use strict声明中讨论第二章,提高代码性能 JSLint【显示】,这迫使开发工具或任何其他类似的检查员来治疗范围错误的警告。 然后使用return关键字返回author参数。**

这看起来很正常; 然而,令许多 JavaScript 开发人员感到困惑的是函数名的结构。 注意,AuthorName以大写a开头。 在 JavaScript 中,当我们用大写字母声明一个函数名时,实际上是在告诉 JavaScript 解释器我们在声明一个构造器。

构造器只是一个 JavaScript 函数,它的工作方式与其他函数相同。 我们甚至可以使用简单的console.log功能打印作者的名字到控制台,如下图所示,使用开发人员工具:

The care and feeding of function names

如果我们使用这个代码在about:blank开发工具控制台或虚拟 HTML 页面中运行,我们将看到控制台输出作为名称,正如我们所期望的。 问题是,为了有效地使用构造器,我们需要将它们与new关键字一起使用。

现在您可能会问,我们如何才能知道我们现有的 JavaScript 代码是否使用了构造器。 想象一个非常大的代码库,到处都是函数; 如果甚至开发人员工具选项都没有告知我们需要使用new而不是static函数调用的实例,我们如何检查这一点?

幸运的是,有一个办法。 如果我们记得第 2 章使用 JSLint提高代码性能,JSLint 可以告诉我们是否需要使用new关键字。 我已经添加了前面的代码示例,并在 JSLint 中启用了控制台浏览器对象。 看看 JSLint 在以下截图中给出的错误:

The care and feeding of function names

正如我们从 JSLint 的中看到的,在第 11 行我们给出了一个错误,Missing 'new'作为我们唯一的错误,表明我们有一个构造器,我们需要这样使用它。

理解实例

现在解决这个问题的简单方法是将AuthorName函数的名称改为驼峰形式; 即,我们将A改为小写(a)。 但这里我们要把它作为一个例子,你可能会问为什么? 在 JavaScript 中,每次我们写一个对象,一个变量,一个函数,一个数组,等等,我们都在创建对象。

通过使用实例,我们降低了对象的使用。 在 JavaScript 中,一个实例在内存中只计算一次。 例如,假设我们使用document.getElementById()方法。 每个与该对象保存在一起的变量都有一个内存计数,但是,如果它在用new关键字声明的对象中,该计数只被计数一次,而不是在getElementById()每次出现时重用。 通过使用new关键字,我们创建了构造器的一个实例(在本例中是AuthorName),它允许我们以与通常使用它相同的方式重用该函数。

使用 new 创建实例

创建一个新实例非常简单; 我们可以简单地调用一个新的实例来运行一个函数,如下面的截图所示,在我们的console.log函数中使用new关键字,在第 11 行:

Creating instances with 'new'

如果我们在一个空白页面或一个简单的 HTML 页面中运行此代码,我们将看到我们的日志并没有按照我们期望的方式输出。 但是我们可以在 Chrome 的开发工具控制台面板上看到一个对象返回AuthorName {}。 这表明我们实际上是在记录一个对象的新实例,而不是作者的名字。

为了正确显示这个名称,我们需要一个关键字来声明对这个构造器实例的引用。 为此,我们将使用this关键字; 在 JavaScript 中,this是一个在作用域中执行的精确点的引用。

JavaScript 中的this关键字是指在使用脚本时,脚本执行点存在的作用域和变量。 例如,当您在函数中使用this关键字时,它可以引用同样在同一作用域中(或函数内部)的变量。 通过使用this关键字,我们可以在代码执行的某个点指向变量和对象。

作用域是一段带有自己变量和属性的 JavaScript 代码。 作用域可以包括单个 JavaScript 对象的全局作用域(即整个 JavaScript 文件),函数级作用域(其中变量和属性在函数中设置),或者如前所述,包括构造器,因为构造器也是函数。

让我们用this关键字重写AuthorName构造器,这样我们就可以引用我们的作用域并将我们的值打印到控制台面板。 我们需要在构造器中创建一个初始化式,以便返回作用域变量。 初始化式(有时称为init函数)在构造器中指定某些变量,并在创建时赋值属性。

在这里,我们创建一个变量与一个this关键字前缀表明我们引用实例构造器在我们之后,我们的函数称为init,等于一个函数,就像我们会使用一个变量声明一个函数。 让我们在下面的截图中查看一下代码:

Creating instances with 'new'

看看第 13 行和第 15 行; 在 13 上,我们声明一个变量author1,使用一个new``AuthorName构造器作为字符串参数Chad Adams。 在本例中,author1AuthorName构造器的一个实例,其中Chad Adams作为唯一参数。

还要注意,在console.log的第 15 行上,我们有一个init()函数,它是构造器的内部函数。 我们也可以在构造器中创建其他函数,例如打印自定义日志信息,如下图所示:

Creating instances with 'new'

正如第 11 行所示,我们现在添加了一个helloInfo()函数,作用域为AuthorName构造器,该函数使用author参数打印出一个自定义消息。 然后,在第 20 行,我们通过简单地调用构造器的函数在console.log之外调用它,构造器有自己的console.info函数包装在里面。

这有助于将我们的逻辑限制在代码库中的单个对象中,并保持代码的良好组织。 这叫做对象定向; 它非常适合重用代码,但可能会导致 JavaScript 的性能问题。 我们来举个例子。 在这里,我们有两个相同代码的示例,每个示例都封装在console.timeconsole.timeEnd函数中。 下面的截图显示了我们审核过的代码和渲染代码的结果时间:

Creating instances with 'new'

所以总的时间大约是 2.5 毫秒。 这个不是太糟糕,但现在让我们看看如果我们使用简单的非构造器会发生什么,以及呈现相同输出的速度会是什么。 如下面的截图所示,我将构造器分开并创建了两个单独的函数。

我还在次要函数中调用了主要的authorName函数,就像在我们的console.log函数中一样,以打印作者的名字。 让我们运行下一个截图中更新后的代码,看看它的运行速度比构造器方法快还是慢。 但是,请记住,根据我们系统的速度和浏览器,结果可能会有所不同。

Creating instances with 'new'

因此,对于静态函数,我们的结果将停留在 4 毫秒左右,这比我们的实例构建对象要长。 所以,这是 JavaScript 中静态函数比原型函数更好的用法!

使用原型替代构造器

在这里,我们将学习使用原型的备用构造器的概念。

从记忆角度理解原型

我们已经介绍了如何在构造器中创建实例函数,我们还学习了如何在一个构造器中使用this关键字。 但是,还有一件事需要讨论:在构造器外部附加另一个实例方法的能力,这在很多方面都很有帮助。 首先,如果需要,它允许我们作为开发人员在预先编写的构造器之外创建函数。 其次,它还使我们的内存使用量较小。 在深入研究之前,让我们重新编写构造器代码来使用原型,如下面的截图所示:

Understanding prototypes in terms of memory

现在查看更新后的代码,我们可以看到构造器被删除了,但被拉出了构造器:然后它们被移动到AuthorName函数的原型中,使用的函数名与前面使用的相同。 现在,你可以注意到,在第 10 行和第 13 行,我们可以在原型函数中使用这个,因为我们引用构造器的实例来打印该实例的特定变量。

哪个更快,原型函数还是构造器?

您可能还注意到,在第 16 行到第 22 行中,我再次在函数调用中添加了和console.timeEnd函数。 所以你认为原型与标准构造器相比会更快还是更慢? 在下面的截图中,我们可以看看结果:

Which is faster, a prototype or a constructor function?

哇,发射原型需要 4.2 毫秒,而使用构造器方法需要 2.1 毫秒; 这里发生了什么? 实际上,我们在构造器之后创建了函数。 输出很慢,但这在原型中是可以预料到的,因为意图是用原型添加构造器。

此时,我们可能会想“哦,我不知道,我不应该再编写原型了!” 现在,在我们开始从项目文件中删除原型之前,我想解释一下原型的可伸缩性。 这是真的,当调用构造器时,原型可能会更慢……” 我说的小是什么意思?” 对于像这个特定示例这样的小型原型使用,我们可以看到原型运行得比传统构造器方法慢。

这就是问题所在; 对于较大的项目,大型应用中的构造器可能有 50 个函数、200 个函数,依此类推。 当我们一次又一次地调用那些较大的构造器时,仅仅调用构造器的实例就会占用相当大的内存,因为它必须准备好包含在其中的所有函数。

通过使用原型方法,这些初始构造器调用将在内存中存储一次。 对于小型原型的使用,性能优势是不明显的,因为我们使用内存,就像我们有一个简单的静态函数,但一旦它被设置,它就在内存中,不需要像静态 JavaScript 函数那样被召回或重新处理。

关于原型继承的另一件事是,尽管使用原型继承可能会导致性能问题,但它对大型代码库非常有帮助。 如果项目有范围问题或使用可能导致冲突的库,请考虑使用名称空间。 它的工作原理类似于原型类,但功能类似于简单的静态函数,前面加了一个名称空间以防止冲突。

阵列性能

在处理性能时,我们通常不会考虑数组,但这里有几点值得一提。 首先,当您试图处理大量数据时,大型数组可能会很混乱,而且会影响性能。 通常,对于数组,我们只需要担心两件事:搜索和数组大小。

优化数组搜索

让我们创建一个数组,其中有很多的值; 在这里,我创建了一个数组,名为myArray,其中有 1001 个值,一个键值字符串和数组的索引。 您可以在 Packt Publishing 网站上示例代码的Chapter_6文件夹中的06_09.js文件中获取完整版本。 以下是总数组的部分代码示例:

Optimizing array searches

在数组中查找值有两种方法; 第一个使用了indexOf()函数,这是一个特定于数组的函数,它查找每个值并返回搜索值的索引。 另一种方法是直接指定索引值,这将返回值(假设我们知道所需值的索引)。

让我们尝试一个实验,我们将使用预先制作的 1001 个值的myArray,并使用indexOf()函数遍历它们,然后再次使用一个数组。 我们已经在我们的myArray之后添加了代码,我们已经把这个代码块包裹在console.timeconsole.timeEnd函数中,如图所示,在 Chrome开发工具中呈现的时间:

Optimizing array searches

这表明我们搜索这个大数组的结果大约是 5.9 毫秒。 现在,为了进行比较,我将保留indexFound变量,尽管我们可以简单地指定数组值的下标。 我们也将使用相同的索引值进行搜索,即541。 让我们更新我们的代码,如这里所示,并查看我们的结果在 Chrome开发工具:

Optimizing array searches

看起来我们的结果削减了我们的索引搜索性能时间相当多。 所以,当你在 JavaScript 中构造数组时,只在需要时使用indexOf,如果可能的话,尝试直接调用索引。 那么为什么时间输出如此不同呢? 这是简单的; 在第二个示例中,我们手动指示数组的位置,而不是让 JavaScript 自己查找键。 这加快了 JavaScript 解释器遍历数组并提供值的速度。

小结

在本章中,我们学习了构造器的正确使用。 我们学习了 JavaScript 中使用new关键字的实例,发现使用构造器可以在限定代码范围的同时加快静态代码的速度。

我们了解了原型,以及它们如何能够很好地扩展大型应用,而在较小的项目中却没有增加什么价值。 最后,我们还学习了如何使用indexOf函数搜索数组以及数组的性能损失。

在下一章,我们将看看如何编写 JavaScript 来优化我们的项目的文档对象模型。