今天,JavaScript 是几乎所有现代 Web 应用的核心。这就是为什么JavaScript问题,以及找到导致这些问题的错误,是 Web 发者的首要任务。
用于单页应用程序(SPA)开发、图形和动画以及服务器端JavaScript平台的强大的基于JavaScript的库和框架已不是什么新鲜事。在 Web 应用程序开发的世界里,JavaScript确实已经无处不在,因此是一项越来越重要的技能,需要掌握。
起初,JavaScript 看起来很简单。事实上,对于任何有经验的前端开发人员来说,在网页中建立基本的JavaScript功能是一项相当简单的任务,即使他们是JavaScript新手。然而,这种语言比人们最初认为的要细致、强大和复杂得多。事实上,JavaScript的许多微妙之处导致了许多常见的问题,这些问题使它无法工作--我们在这里讨论了其中的10个问题--在寻求成为JavaScript开发大师的过程中,这些问题是需要注意和避免的。
问题#1:不正确的引用 this 随着JavaScript编码技术和设计模式多年来变得越来越复杂,回调和闭包中的自引用作用域也相应增加,这是造成JavaScript问题的 "this/that 混乱 "的一个相当普遍的来源。
考虑下面代码:
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中却不是。考虑一下下面的代码:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // 输出什么? 如果你猜测console.log()的调用会输出 undefined 或者抛出一个错误,那你就猜错了。答案是输出10。为什么呢?
在大多数其他语言中,上面的代码会导致一个错误,因为变量i的 "生命"(即使作用域)会被限制在for块中。但在JavaScript中,情况并非如此,即使在for循环完成后,变量i仍然在作用域内,在退出循环后仍保留其最后的值。(顺便说一下,这种行为被称为变量提升(variable hoisting)。
JavaScript中对块级作用域的支持是通过let关键字实现的。Let关键字已经被浏览器和Node.js等后端JavaScript引擎广泛支持了多年。
问题#3:创建内存泄漏 如果没有有意识地编写代码来避免内存泄漏,那么内存泄漏几乎是不可避免的JavaScript问题。它们的发生方式有很多种,所以我们只重点介绍几种比较常见的情况。
内存泄漏实例1:对不存在的对象的悬空引用 考虑以下代码:
var theThing = null; var replaceThing = function () { var priorThing = theThing; var unused = function () { // 'unused'是'priorThing'被引用的唯一地方。 // 但'unused'从未被调用过 if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // 创建一个1MB的对象 someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // 每秒钟调用一次 "replaceThing"。 如果你运行上述代码并监测内存使用情况,你会发现你有一个明显的内存泄漏,每秒泄漏整整一兆字节!而即使是手动垃圾收集器(GC)也无济于事。因此,看起来我们每次调用 replaceThing 都会泄漏 longStr。但是为什么呢?
每个theThing对象包含它自己的1MB longStr对象。每一秒钟,当我们调用 replaceThing 时,它都会在 priorThing 中保持对先前 theThing 对象的引用。
但是我们仍然认为这不会是一个问题,因为每次通过,先前引用的priorThing将被取消引用(当priorThing通过priorThing = theThing;被重置时)。而且,只在 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...
有趣的是,即使 element 被从DOM中移除,上面的循环自引用也会阻止 element 和onClick被收集,因此会出现内存泄漏。
避免内存泄漏:要点 JavaScript的内存管理(尤其是垃圾回收)主要是基于对象可达性的概念。
以下对象被认为是可达的,被称为 "根":
从当前调用堆栈的任何地方引用的对象(即当前被调用的函数中的所有局部变量和参数,以及闭包作用域内的所有变量)
所有全局变量
只要对象可以通过引用或引用链从任何一个根部访问,它们就会被保留在内存中。
浏览器中有一个垃圾收集器,它可以清理被无法到达的对象所占用的内存;换句话说,当且仅当GC认为对象无法到达时,才会将其从内存中删除。不幸的是,很容易出现不再使用的 "僵尸 "对象,但GC仍然认为它们是 "可达的"。
问题4:双等号的困惑 JavaScript 的一个便利之处在于,它会自动将布尔上下文中引用的任何值强制为布尔值。但在有些情况下,这可能会让人困惑,因为它很方便。例如,下面的一些情况对许多JavaScript开发者来说是很麻烦的。
// 下面结果都是 'true' console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0);
// 下面也都成立 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 JavaScript问题5:低效的DOM操作 使用 JavaScript 操作DOM(即添加、修改和删除元素)是相对容易,但操作效率却不怎么样。
比如,每次添加一系列DOM元素。添加一个DOM元素是一个昂贵的操作。连续添加多个DOM元素的代码是低效的。
当需要添加多个DOM元素时,一个有效的替代方法是使用 document fragments来代替,从而提高效率和性能。
var div = document.getElementsByTagName("my_div");
var fragment = document.createDocumentFragment();
for (var e = 0; e < elems.length; e++) { // ele