错误的 JavaScript 代码:JavaScript 开发人员最常犯的 10 个错误

已发表: 2022-03-11

今天,JavaScript 是几乎所有现代 Web 应用程序的核心。 特别是在过去的几年中,我们见证了各种强大的基于 JavaScript 的库和框架的激增,这些库和框架用于单页应用程序 (SPA) 开发、图形和动画,甚至是服务器端 JavaScript 平台。 JavaScript 已经真正在 Web 应用程序开发领域变得无处不在,因此掌握的技能越来越重要。

乍一看,JavaScript 可能看起来很简单。 事实上,将基本的 JavaScript 功能构建到网页中对于任何有经验的软件开发人员来说都是一项相当简单的任务,即使他们是 JavaScript 新手。 然而,这种语言比人们最初认为的要微妙、强大和复杂得多。 事实上,JavaScript 的许多细微之处都会导致一些常见问题,使其无法正常工作——我们在此讨论其中的 10 个——在成为 JavaScript 开发大师的过程中,了解和避免这些问题很重要。

常见错误 #1:不正确this引用

我曾经听一位喜剧演员说:

我不是真的在这里,因为除了“t”之外,这里还有什么?

这个笑话在很多方面都体现了开发人员对 JavaScript 的this关键字经常存在的混淆类型。 我的意思是, this真的是这样,还是完全不同? 还是未定义?

随着这些年来 JavaScript 编码技术和设计模式变得越来越复杂,回调和闭包中自引用范围的扩散也相应增加,这是“这个/那个混淆”的一个相当常见的来源。

考虑这个示例代码片段:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };

执行上述代码会导致以下错误:

 Uncaught TypeError: undefined is not a function

为什么?

这都是关于上下文的。 出现上述错误的原因是,当您调用setTimeout()时,实际上是在调用window.setTimeout() 。 因此,传递给setTimeout()的匿名函数是在window对象的上下文中定义的,该对象没有clearBoard()方法。

一个传统的、与旧浏览器兼容的解决方案是简单地将您对this的引用保存在一个变量中,然后该变量可以被闭包继承; 例如:

 Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };

或者,在较新的浏览器中,您可以使用bind()方法传入正确的引用:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };

常见错误 #2:认为存在块级范围

正如我们的 JavaScript 招聘指南中所讨论的,JavaScript 开发人员之间的一个常见混淆来源(因此也是一个常见的错误来源)是假设 JavaScript 为每个代码块创建了一个新的范围。 尽管在许多其他语言中都是如此,但在 JavaScript 中却不是这样。 例如,考虑以下代码:

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?

如果您猜测console.log()调用将输出undefined或抛出错误,那么您猜错了。 信不信由你,它将输出10 。 为什么?

在大多数其他语言中,上面的代码会导致错误,因为变量i的“生命”(即范围)将被限制在for块中。 然而,在 JavaScript 中,情况并非如此,即使在for循环完成后变量i仍保留在范围内,并在退出循环后保留其最后一个值。 (顺便说一下,这种行为被称为变量提升)。

不过值得注意的是,对块级作用域的支持正在通过新的let关键字进入 JavaScript。 let关键字已经在 J​​avaScript 1.7 中可用,并且计划从 ECMAScript 6 开始成为官方支持的 JavaScript 关键字。

JavaScript 新手? 阅读范围、原型等。

常见错误 #3:造成内存泄漏

如果您没有有意识地编写代码来避免它们,那么内存泄漏几乎是不可避免的 JavaScript 问题。 它们的发生方式有很多种,因此我们将重点介绍其中一些更常见的情况。

内存泄漏示例 1:对已失效对象的悬空引用

考虑以下代码:

 var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second

如果你运行上面的代码并监控内存使用情况,你会发现你有一个巨大的内存泄漏,每秒泄漏一兆字节! 甚至手动 GC 也无济于事。 所以看起来我们每次调用replaceThing时都会泄漏longStr 。 但为什么?

让我们更详细地研究一下:

每个theThing对象都包含自己的 1MB longStr对象。 每一秒,当我们调用replaceThing时,它都会保留对priorThing 中先前theThing对象的priorThing 。 但是我们仍然认为这不会是一个问题,因为每次通过之前,先前引用的priorThing都会被取消引用(当通过priorThing priorThing = theThing;重置priorThing 时)。 而且,仅在replaceThing的主体和unused的函数中引用,实际上从未使用过。

所以我们再次想知道为什么这里有内存泄漏!?

要了解发生了什么,我们需要更好地了解 JavaScript 在底层是如何工作的。 实现闭包的典型方式是每个函数对象都有一个指向表示其词法范围的字典式对象的链接。 如果在replaceThing中定义的两个函数实际上都使用priorThing ,那么即使priorThing被一遍又一遍地分配,它们都获得相同的对象很重要,因此两个函数共享相同的词法环境。 但是,一旦任何闭包使用了一个变量,它就会最终进入该范围内所有闭包共享的词法环境中。 正是这种细微的差别导致了这种粗糙的内存泄漏。 (有关这方面的更多详细信息,请点击此处。)

内存泄漏示例 2:循环引用

考虑这个代码片段:

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }

在这里, onClick有一个闭包,它保持对element的引用(通过element.nodeName )。 通过将onClick也分配给element.click ,循环引用被创建; 即: element -> onClick -> element -> onClick -> element

有趣的是,即使从 DOM 中删除element ,上面的循环自引用也会阻止elementonClick被收集,从而导致内存泄漏。

避免内存泄漏:你需要知道的

JavaScript 的内存管理(特别是垃圾回收)主要基于对象可达性的概念。

假设以下对象是可访问的并称为“根”:

  • 从当前调用堆栈中的任何位置引用的对象(即当前正在调用的函数中的所有局部变量和参数,以及闭包范围内的所有变量)
  • 所有全局变量

至少只要可以通过引用或引用链从任何根访问对象,它们就会保存在内存中。

浏览器中有一个垃圾收集器(GC),用于清理不可达对象占用的内存; 即,当且仅当GC 认为对象不可访问时,对象才会从内存中删除。 不幸的是,很容易得到实际上不再使用但 GC 仍然认为是“可访问”的已失效的“僵尸”对象。

相关: Toptal 开发人员的 JavaScript 最佳实践和技巧

常见错误#4:对平等的混淆

JavaScript 的便利之一是它会自动将布尔上下文中引用的任何值强制转换为布尔值。 但是在某些情况下,这可能会让人感到困惑,因为它很方便。 例如,已知以下一些内容会困扰许多 JavaScript 开发人员:

 // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...

关于最后两个,尽管是空的(这可能会导致人们相信它们会评估为false ),但{}[]实际上都是对象,并且任何对象都将被强制转换为 JavaScript 中的布尔值true ,符合 ECMA-262 规范。

正如这些示例所展示的,类型强制转换的规则有时会一清二楚。 因此,除非明确需要类型强制,否则通常最好使用===!== (而不是==!= ),以避免类型强制的任何意外副作用。 ( ==!=在比较两个事物时会自动执行类型转换,而===!==在不进行类型转换的情况下执行相同的比较。)

完全作为一个侧面——但由于我们正在谈论类型强制和比较——值得一提的是,将NaN任何东西(甚至是NaN !)进行比较总是会返回false因此,您不能使用相等运算符( =====!=!== )来确定值是否为NaN 相反,使用内置的全局isNaN()函数:

 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true

常见错误 #5:低效的 DOM 操作

JavaScript 使操作 DOM 变得相对容易(即添加、修改和删除元素),但没有做任何事情来提高这样做的效率。

一个常见的示例是一次添加一系列 DOM 元素的代码。 添加 DOM 元素是一项昂贵的操作。 连续添加多个 DOM 元素的代码效率低下并且可能无法正常工作。

当需要添加多个 DOM 元素时,一种有效的替代方法是改用文档片段,从而提高效率和性能。

例如:

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

除了这种方法本身提高了效率之外,创建附加的 DOM 元素的成本很高,而在分离时创建和修改它们然后附加它们会产生更好的性能。

常见错误 #6:在for循环中不正确地使用函数定义

考虑这段代码:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }

根据上面的代码,如果有 10 个输入元素,点击其中任何一个都会显示“This is element #10”! 这是因为,当对任何元素调用onclick时,上述 for 循环已经完成,并且i的值已经是 10(对于所有元素)。

但是,我们可以通过以下方式纠正上述代码问题以实现所需的行为:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }

在这个修改后的代码版本中,每次我们通过循环时都会立即执行makeHandler ,每次接收i+1的当前值并将其绑定到一个作用域num变量。 外部函数返回内部函数(它也使用这个作用域的num变量)并且元素的onclick设置为该内部函数。 这确保了每个onclick接收并使用正确的i值(通过作用域num变量)。

常见错误 #7:未能正确利用原型继承

惊人的高比例的 JavaScript 开发人员未能完全理解,因此无法充分利用原型继承的特性。

这是一个简单的例子。 考虑这段代码:

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };

看起来相当简单。 如果您提供名称,请​​使用它,否则将名称设置为“默认”; 例如:

 var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

但是如果我们这样做呢:

 delete secondObj.name;

然后我们会得到:

 console.log(secondObj.name); // -> Results in 'undefined'

但是,将其恢复为“默认”不是更好吗? 如果我们修改原始代码以利用原型继承,这很容易做到,如下所示:

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';

在这个版本中, BaseObject继承了它的prototype对象的name属性,它被设置(默认)为'default' 。 因此,如果在没有名称的情况下调用构造函数,则名称将默认为default 。 同样,如果从BaseObject的实例中删除name属性,则将搜索原型链并从其值仍为'default'prototype对象中检索name属性。 所以现在我们得到:

 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'

常见错误 #8:创建对实例方法的错误引用

让我们定义一个简单的对象,并创建它并实例化它,如下所示:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();

现在,为方便起见,让我们创建对whoAmI方法的引用,大概这样我们就可以仅通过whoAmI()而不是更长的obj.whoAmI()来访问它:

 var whoAmI = obj.whoAmI;

并且为了确保一切看起来都很顺从,让我们打印出我们的新whoAmI变量的值:

 console.log(whoAmI);

输出:

 function () { console.log(this === window ? "window" : "MyObj"); }

嗯不错。 看起来不错。

但是现在,看看我们调用obj.whoAmI()与我们方便的引用whoAmI()时的区别:

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

什么地方出了错?

这里的假货是,当我们进行赋值时var whoAmI = obj.whoAmI; ,新变量whoAmI被定义在全局命名空间中。 结果,它的this值是window而不是MyObjectobj实例!

因此,如果我们真的需要创建对对象现有方法的引用,我们需要确保在该对象的命名空间内进行,以保留this的值。 这样做的一种方法是,例如,如下:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)

常见错误 #9:提供一个字符串作为setTimeoutsetInterval的第一个参数

首先,让我们在这里明确一点:提供一个字符串作为setTimeoutsetInterval的第一个参数本身并不是一个错误。 它是完全合法的 JavaScript 代码。 这里的问题更多是性能和效率。 很少解释的是,在后台,如果您将字符串作为第一个参数传递给setTimeoutsetInterval ,它将被传递给函数构造函数以转换为新函数。 此过程可能缓慢且效率低下,并且很少需要。

将字符串作为第一个参数传递给这些方法的替代方法是传入一个函数。 让我们看一个例子。

那么,这里将是setIntervalsetTimeout的一个相当典型的用法,将字符串作为第一个参数传递:

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

更好的选择是传入一个函数作为初始参数; 例如:

 setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);

常见错误 #10:未能使用“严格模式”

正如我们的 JavaScript 招聘指南中所解释的,“严格模式”(即,包括'use strict';在 JavaScript 源文件的开头)是一种在运行时自愿对 JavaScript 代码强制执行更严格的解析和错误处理的方法,以及使其更安全。

诚然,不使用严格模式本身并不是一个“错误”,但它的使用越来越受到鼓励,而省略它也越来越被认为是不好的形式。

以下是严格模式的一些主要好处:

  • 使调试更容易。 原本会被忽略或静默失败的代码错误现在会生成错误或抛出异常,从而更快地提醒您代码中的问题并更快地引导您找到它们的源头。
  • 防止意外的全局变量。 如果没有严格模式,为未声明的变量赋值会自动创建一个具有该名称的全局变量。 这是 JavaScript 中最常见的错误之一。 在严格模式下,尝试这样做会引发错误。
  • 消除this威压。 如果没有严格模式,对 null 或 undefined 的this值的引用会自动强制到全局。 这可能会导致许多假头和拔出你的头发的错误。 在严格模式下,引用 aa this null 或 undefined 值会引发错误。
  • 不允许重复的属性名称或参数值。 严格模式在检测到对象中重复的命名属性(例如, var object = {foo: "bar", foo: "baz"}; )或函数的重复命名参数(例如, function foo(val1, val2, val1){} ),从而捕获几乎可以肯定是代码中的错误,否则您可能会浪费大量时间来跟踪。
  • 使 eval() 更安全。 eval()在严格模式和非严格模式下的行为方式存在一些差异。 最重要的是,在严格模式下,在eval()语句中声明的变量和函数不会在包含范围内创建(它们在非严格模式下在包含范围内创建的,这也可能是问题的常见来源)。
  • delete的无效使用引发错误。 delete运算符(用于从对象中删除属性)不能用于对象的不可配置属性。 当尝试删除不可配置的属性时,非严格代码会静默失败,而严格模式会在这种情况下抛出错误。

包起来

与任何技术一样,您越了解 JavaScript 工作和不工作的原因和方式,您的代码就越可靠,您就越能够有效地利用该语言的真正力量。 相反,缺乏对 JavaScript 范式和概念的正确理解确实是许多 JavaScript 问题所在。

彻底熟悉语言的细微差别是提高熟练度和提高生产力的最有效策略。 当您的 JavaScript 不工作时,避免许多常见的 JavaScript 错误会有所帮助。

相关: JavaScript Promises:示例教程