
现如今,JavaScript基本上上是大部份当代web应用领域程序的核心理念。这就是为何JavaScript难题和找寻引致那些难题的严重错误是web开发者的首要目标。
用作白眉林应用领域程序(SPA)开发、绘图和动画电影和服务器端JavaScript网络平台的强悍的如前所述JavaScript的库和架构并不是什么可说。JavaScript在web应用领域应用软件开发的当今世界中的确显得无所不在,因而它是几项愈来愈关键的专业技能。
起先,JavaScript可能看上去很单纯。实际上,将基本上的JavaScript机能构筑到页面中对任何人有实战经验的应用软件开发者而言都是几项十分单纯的各项任务,即便他们是JavaScript初学者。不过,此种词汇比现代人起初认为的要错综繁杂、强有力和繁杂得多。实际上,JavaScript的很多错综繁杂之处引致了很多常见难题,那些难题妨碍了它的组织工作——我们在这儿探讨了当中的10个难题,在正式成为一位杰出的JavaScript开发者的操作过程中,须要特别注意和防止那些难题。
难题1:不恰当的提及 this
随著JavaScript标识符控制技术和程序词汇多年来显得愈来愈繁杂,反弹和闭P43EB96SJ的自提及返回值也适当减少,这是导致JavaScript难题的”th
考量上面标识符:
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 its 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开发
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
难题5:低效的DOM操作
使用 JavaScript 操作DOM(即添加、修改和删除元素)是相对容易,但操作效率却不怎么样。
比如,每次添加一系列DOM元素。添加一个DOM元素是一个昂贵的操作。连续添加多个DOM元素的标识符是低效的。
当须要添加多个DOM元素时,一个有效的替代方法是使用 document fragments来代替,从而提高效率和性能。
var div = document.getElementsByTagName(“mydiv”);
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:在循环内严重错误使用函数定义
考量上面标识符:
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个 input 元素,点击任何人一个都会显示”This is element #10″。
这是因为,当任何人一个元素的onclick被调用时,上面的for循环已经结束,i的值已经是10了(对所有的元素)。
我们可以像上面这样来解决这个难题:
var elements = document.getElementsByTagName(input);
var n = elements.length;
var makeHandler = function(num){
return function(){
console.log(“This is element #”+ num);
};
};
for (var i =0; i < n; i++){
elements[i].onclick = makeHandler(i+1);
}
makeHandler 是一个外部函数,并返回一个内部函数,这样就会形成一个闭包,num 就会调用时传进来的的当时值,这样在点击元素时,就能显示恰当的序号。
难题7:未能恰当利用原型继承
考量上面标识符:
Baseobject = function(name){
if (typeof name !==”undefined”){
this.name = name;
} else {
this.name =default
}
};
上面标识符比较单纯,就是提供了一个名字,就使用它,否则返回 default:
var firstObj = new BaseObject();
var secondObj = new BaseObject(unique);
console.log(firstObj.name);//->default
console.log(secondObj.name);//->unique
但是,如果这么做呢:
delete secondObj.name;
会得到:
console.log(secondObj.name);//undefined
当使用 delete 删除该属性时,就会返回一个 undefined,那么如果我们也想返回 default 要怎么做呢?利用原型继承,如下所示:
BaseObject = function (name){
if(typeof name !==”undefined”){
this.name = name;
}
};
BaseObject.prototype.name =default;
BaseObject 从它的原型对象中继承了name 属性,值为 default。因而,如果构造函数在没有 name 的情况下被调用,name 将默认为 default。同样,如果 name 属性从BaseObject的一个实例中被移除,那么会找到原型链的 name,,其值仍然是default。所以
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”);
}
Ok,看上去没啥难题。
接着,看看当我们调用obj.whoAmI()和 whoAmI()的区别。
obj.whoAmI();// Outputs “MyObj”(as expected)
whoAmI();// Outputs “window”(uh-oh!)
什么地方出错了?当我们进行赋值时 var whoAmI = obj.whoAmI,新的变量whoAmI被定义在全局命名空间。
结果,this的值是 window,而不是 MyObject 的 obj 实例!
因而,如果我们真的须要为一个对象的现有方法创建一个提及,我们须要确保在该对象的名字空间内进行,以保留 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:为 setTimeout 或 setInterval 提供一个字符串作为第一个参数
首先,须要知道的是为 setTimeout 或 setInterval 提供一个字符串作为第一个参数,这本身并不是一个严重错误。它是完全合法的JavaScript标识符。这儿的难题更多的是性能和效率的难题。
很少有人解释的是,如果你把字符串作为setTimeout或setInterval的第一个参数,它将被传递给函数构造器,被转换成一个新函数。这个操作过程可能很慢,效率也很低,而且很少有必要。
将一个字符串作为那些方法的第一个参数的替代方法是传入一个函数。
setInterval(“logTime()”,1000);
setTimeout(“logMessage(“+ msgValue +”)”,1000);
更好的选择是传入一个函数作为初始参数:
setInterval(logTime,1000);
setTimeout(function(){
logMessage(msgValue);
},1000);
难题10:未使用”严格模式”
“严格模式”(即在JavaScript源文件的开头包括”use strict”;)是一种自愿在运行时对JavaScript程序执行更严格的解析和严重错误处理的方式,同时也使它更安全。
但是,不使用严格模式本身并不是一个”严重错误”,但它的使用愈来愈受到鼓励,不使用也愈来愈被认为是不好的形式。
下列是严格模式的一些主要好处:
使得调试更容易。原本会被忽略或无感知的标识符严重错误,现在会产生严重错误或抛出异常,提醒我们更快严重错误之一。在严格模式下,试图这样做会产生一个严重错误。消除this 强迫性。在没有严格模式的情况下,对 null 或 undefined 的 this 值的提及会自动被强制到全局。在严格模式下,提及null或undefined的this值会产生严重错误。不允许重复的属性名或参数值。严格模式在检测到一个对象中的重复命名的属性(例如,var object ={foo:”bar”, foo:”baz”};)或一个函数的重复命名的参数(例如,function foo(val1, val2, val1){})时抛出一个严重错误,从而捕捉到你的标识符中基本上上肯定是一个严重错误,否则你可能会浪费很多时间去追踪。使得eval()更加安全。eval()在严格模式和非严格模式下的行为方式有一些不同。最关键的是,在严格模式下,在eval()语句中声明的变量和函数不会在包含的范围内创建。(在非严格模式下,它们是在包含域中创建的,这也标识符将无声地失败,而严格模式在此种情况下将抛出一个严重错误。
总结
以上就是深圳蓝景实训部今天跟大家撷取的10个JavaScript中最常见的难题,无论你组织工作或者学习有没有遇过,希望能帮到你们。觉得有用的话可