本文概述
- 常见错误1:对此的错误引用
- 常见错误2:认为存在块级范围
- 常见错误3:造成内存泄漏
- 常见错误4:对平等的困惑
- 常见错误5:无效的DOM操作
- 常见错误#6:在for循环中错误使用函数定义
- 常见错误7:无法正确利用原型继承
- 常见错误#8:创建对实例方法的错误引用
- 常见错误9:将字符串作为setTimeout或setInterval的第一个参数
- 常见错误10:无法使用”严格模式”
- 本文总结
今天, JavaScript几乎是所有现代Web应用程序的核心。特别是在过去的几年中, 见证了用于单页应用程序(SPA)开发, 图形和动画甚至服务器端JavaScript平台的各种强大的基于JavaScript的库和框架的泛滥。 JavaScript在Web应用程序开发领域中确实已变得无处不在, 因此是一种越来越重要的掌握技能。
乍一看, JavaScript似乎很简单。实际上, 对于任何有经验的软件开发人员来说, 即使他们是JavaScript的新手, 要将基本的JavaScript功能构建到网页中也是一项相当简单的任务。然而, 这种语言比起人们最初认为的语言要细腻得多, 功能更强大且更复杂。的确, 许多JavaScript的微妙之处导致了许多使其无法正常工作的常见问题(我们在这里讨论了其中的10个), 这些对于意识到并避免成为JavaScript高手的开发很重要。
常见错误1:对此的错误引用
我曾经听过一个喜剧演员说:
我不是真的在这里, 因为这里什么都在, 除了那里, 没有” t”?
这个笑话以多种方式描述了开发人员通常对JavaScript的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()的匿名函数是在没有clearBoard()方法的window对象的上下文中定义的。
传统的, 与浏览器兼容的解决方案是简单地将对此的引用保存在变量中, 然后该变量可以由闭包继承。例如。:
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()调用将输出未定义或抛出错误, 则你猜错了。信不信由你, 它会输出10。为什么?
在大多数其他语言中, 上面的代码将导致错误, 因为变量i的”寿命”(即作用域)将限于for块。但是, 在JavaScript中不是这种情况, 即使在for循环完成后, 变量i仍在作用域内, 在退出循环后仍保留其最后一个值。 (顺便说一下, 这种行为被称为可变提升)。
不过, 值得注意的是, 对块级范围的支持正通过新的let关键字进入JavaScript。 let关键字已经在JavaScript 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。但为什么?
让我们更详细地研究一下事情:
每个Thing对象都包含其自己的1MB longStr对象。每隔一秒钟, 当我们调用replaceThing时, 它会保留对beforeThing中的PriortheThing对象的引用。但是我们仍然不会认为这会成为问题, 因为每次都将取消引用先前引用的priorThing(通过priorThing = theThing;重置priorThing时)。而且, 仅在replaceThing主体和未使用的函数(实际上从未使用过)中引用。
所以我们又一次想知道为什么这里有内存泄漏!?
要了解发生了什么, 我们需要更好地了解JavaScript在幕后的工作方式。实现闭包的典型方法是, 每个函数对象都有一个指向表示其词法范围的字典式对象的链接。如果在replaceThing中定义的两个函数实际上都使用了PriorThing, 那么即使一次又一次地将PriorThing分配给它们, 都必须获得相同的对象也很重要, 因此这两个函数共享相同的词法环境。但是, 一旦任何闭包使用了变量, 该变量就会在该范围内所有闭包共享的词法环境中结束。正是这种细微差别导致了这种陈旧的内存泄漏。 (有关更多详细信息, 请点击此处。)
内存泄漏示例2:循环引用
考虑以下代码片段:
function addClickHandler(element) {
element.click = function onClick(e) {
alert("Clicked the " + element.nodeName)
}
}
在这里, onClick有一个闭包, 该闭包保留对元素的引用(通过element.nodeName)。通过还将onClick分配给element.click, 可以创建循环引用。即:元素-> onClick->元素-> onClick->元素…
有趣的是, 即使从DOM中删除了元素, 上面的循环自引用也将阻止元素和onClick的收集, 从而导致内存泄漏。
避免内存泄漏:你需要了解的内容
JavaScript的内存管理(特别是垃圾收集)主要基于对象可访问性的概念。
假定以下对象是可到达的, 并称为”根”:
- 从当前调用堆栈中任何位置引用的对象(即, 当前正在调用的函数中的所有局部变量和参数, 以及闭包作用域中的所有变量)
- 所有全局变量
对象至少要保留在内存中, 只要它们可以通过引用或引用链从任何根访问。
浏览器中有一个垃圾收集器(GC), 用于清理无法访问的对象占用的内存。也就是说, 只有在GC认为对象无法访问时, 它们才会从内存中删除。不幸的是, 很容易以实际上已经不再使用但GC仍然认为”可达”的已失效的”僵尸”对象结束。
相关:srcmini开发人员的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的then-current值并将其绑定到作用域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从其原型对象继承name属性, 该对象在默认情况下被设置为” default”。因此, 如果在没有名称的情况下调用构造函数, 则该名称将默认为default。同样, 如果从BaseObject实例中删除了name属性, 则将搜索原型链, 并从其值仍为” default”的原型对象中检索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。结果, 它的值是window, 而不是MyObject的obj实例!
因此, 如果我们确实需要创建对对象现有方法的引用, 则需要确保在该对象的名称空间中进行引用, 以保留该对象的值。例如, 一种实现方式如下:
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:将字符串作为setTimeout或setInterval的第一个参数
首先, 让我们在这里进行一些说明:将字符串作为setTimeout或setInterval的第一个参数本身本身并不是错误。这是完全合法的JavaScript代码。这里的问题不仅仅是性能和效率。很少解释的是, 在幕后, 如果将字符串作为第一个参数传递给setTimeout或setInterval, 它将被传递给函数构造函数以转换为新函数。此过程可能很慢且效率低下, 几乎没有必要。
将字符串作为这些方法的第一个参数传递的替代方法是传递一个函数。让我们来看一个例子。
那么, 这里是setInterval和setTimeout的相当典型的用法, 将字符串作为第一个参数传递:
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招聘指南中所述, “严格模式”(即, 包括”使用严格”;在JavaScript源文件的开头)是一种在运行时自愿对JavaScript代码强制执行更严格的解析和错误处理的方法使其更加安全。
公认的是, 虽然不使用严格模式本身并不是”错误”, 但越来越多的人鼓励使用它, 而忽略它已被认为是不好的形式。
以下是严格模式的一些主要优点:
- 使调试更加容易。现在, 本来可以被忽略或以静默方式失败的代码错误将生成错误或引发异常, 从而更快地提醒你代码中的问题, 并更快地将你定向到其来源。
- 防止意外的全局变量。如果没有严格模式, 则将值分配给未声明的变量会自动创建一个具有该名称的全局变量。这是JavaScript中最常见的错误之一。在严格模式下, 尝试执行此操作将引发错误。
- 消除了这种强制性。如果没有严格模式, 则对此null或undefined的this值的引用将自动强制为全局值。这可能会导致许多假冒和拔出头发的错误。在严格模式下, 引用a this值为null或未定义将引发错误。
- 不允许重复的属性名称或参数值。严格模式在检测到对象中重复的命名属性(例如var object = {foo:” bar”, foo:” baz”};)或函数的重复命名参数(例如, 函数foo( val1, val2, val1){}), 从而捕获几乎可以肯定的代码错误, 否则可能会浪费大量时间进行跟踪。
- 使eval()更安全。 eval()在严格模式和非严格模式下的行为方式有所不同。最重要的是, 在严格模式下, 在eval()语句内部声明的变量和函数不在包含范围内创建(它们在包含范围内以非严格模式创建, 这也可能是常见的问题根源)。
- 无效使用delete时引发错误。删除运算符(用于从对象中删除属性)不能用于对象的不可配置属性。尝试删除不可配置的属性时, 非严格代码将静默失败, 而在这种情况下, 严格模式将引发错误。
本文总结
就像任何技术一样, 你越能理解JavaScript为何起作用以及如何起作用以及不起作用的原因, 你的代码越牢固, 你就越能有效地利用语言的真正力量。相反, 缺乏对JavaScript范式和概念的正确理解确实是许多JavaScript问题所在。
全面熟悉该语言的细微差别和细微之处是提高你的熟练程度和提高生产率的最有效策略。当你的JavaScript无法正常工作时, 避免许多常见的JavaScript错误会有所帮助。
相关:JavaScript承诺:带有示例的教程
评论前必须登录!
注册