当前位置: 首页 > 科技观察

十个常见的JavaScript问题

时间:2023-03-14 11:52:16 科技观察

如今,JavaScript几乎是每个现代Web应用程序的核心。这就是为什么JavaScript问题和找出导致它们的错误是Web开发人员的首要任务。用于单页应用程序(SPA)开发、图形和动画以及服务器端JavaScript平台的强大的基于JavaScript的库和框架并不是什么新鲜事。JavaScript在Web应用程序开发领域真正变得无处不在,因此它成为一项越来越重要的技能。起初,JavaScript可能看起来很简单。事实上,将基本的JavaScript功能构建到网页中对于任何有经验的软件开发人员来说都是一项相当简单的任务,即使他们是JavaScript的新手。然而,这种语言比人们最初认为的要微妙、强大和复杂得多。事实上,JavaScript的许多微妙之处会导致许多阻碍其完成工作的常见问题——我们在这里讨论其中的10个问题,以及在成为一名优秀的JavaScript开发人员的过程中要注意什么并避免这些问题。问题1:错误引用this随着JavaScript编码技术和设计模式多年来变得越来越复杂,回调和闭包中的自引用范围也相应增加,这就是造成JavaScript问题的“this/that混淆”“的一个相当常见的来源。考虑以下代码:"?},0);};执行以上代码会出现如下错误:UncaughtTypeError:undefinedisnotafunction出现以上错误的原因是调用setTimeout()时,实际上调用的是window.setTimeout()。因此,传递给setTimeout()的匿名函数是在窗口对象的上下文中定义的,它没有clearBoard()方法。传统的、兼容旧浏览器的解决方案是将这个引用存储在一个变量中,然后可以由闭包继承,如下所示:Game.prototype.restart=function(){this.clearLocalStorage();变种自我=这个;//保存对'this'的引用,同时它仍然是this!这。timer=setTimeout(function(){self.clearBoard();//哦好吧,我知道“self”是谁!},0);};或者,在较新的浏览器中,您可以使用bind()方法传入适当的引用:Game.prototype.restart=function(){this.clearLocalStorage();this.timer=setTimeout(this.reset.bind(this),0);//绑定到'this'};Game.prototype.reset=function(){this.clearBoard();//啊哈,回到正确的“this”的上下文中!};问题2:认为存在块级作用域JavaScript开发人员中一个常见的混淆来源(也是一个常见的错误来源)是假设JavaScript为每个代码块创建了一个新的作用域。虽然在许多其他语言中都是如此,但在JavaScript中却并非如此。考虑以下代码:for(vari=0;i<10;i++){/*...*/}console.log(i);//它输出什么?如果您猜到调用console.log()会输出undefined或抛出错误,那么您猜错了。答案是输出10,为什么?在大多数其他语言中,上述代码会导致错误,因为变量i的“生命”(甚至范围)将被限制在for块中。但在JavaScript中,情况并非如此,即使在for循环完成后,变量i仍在范围内,在退出循环后保留其最后一个值。(顺便说一句,这种行为被称为变量提升。在JavaScript中对块级作用域的支持是通过let关键字实现的。Let关键字已被浏览器和后端JavaScript(如Node.js引擎)广泛支持很多年了。问题3:创建内存泄漏内存泄漏是一个几乎无法避免的JavaScript问题,如果没有有意识地编写代码来避免它们的话。它们发生的方式有很多种,所以我们只强调几个比较常见的情况。内存泄漏示例1:对不存在的对象的悬挂引用考虑以下代码:vartheThing=null;varreplaceThing=function(){varpriorThing=theThing;varunused=function(){//'unused'是唯一的地方'priorThing'被引用。//但'unused'永远不会被调用if(priorThing){console.log("hi");}};theThing={longStr:newArray(1000000).join('*'),//创建一个1MB的对象someMethod:function(){console.log(someMessage);}};};setInterval(rep花边,1000);//每秒调用“replaceThing”。如果你运行上面的代码并监视内存使用情况,你会发现你有明显的内存泄漏,每秒泄漏整整一兆字节!甚至手动垃圾收集器(GC)也无能为力。因此,似乎每次我们调用replaceThing时都会泄漏longStr。但为什么?每个theThing对象都包含自己的1MBlongStr对象。每一秒,当我们调用replaceThing时,它都会在priorThing中保留对先前theThing对象的引用。但我们仍然认为这不会成为问题,因为每次通过时,先前引用的priorThing将被取消引用(当通过priorThing=theThing;重置priorThing时)。而且,它只是在replaceThing的主体和未使用的函数中被引用,但实际上,它从未被使用过。所以我们又想知道为什么这里会出现内存泄漏。为了理解发生了什么,我们需要更好地理解JavaScript的内部工作原理。闭包的典型实现方式是每个函数对象都有一个指向表示其词法范围的类字典对象的链接。如果在replaceThing中定义的两个函数实际上都使用了priorThing,那么重要的是它们都获得相同的对象,即使priorThing被重复赋值,所以两个函数共享相同的词法环境。但是一旦一个变量被任何闭包使用,它最终会进入该范围内所有闭包共享的词法环境中。而这种细微差别就是导致这种可怕的内存泄漏的原因。内存泄漏示例2:循环引用考虑以下代码:functionaddClickHandler(element){element.click=functiononClick(e){alert("Clickedthe"+element.nodeName)}}这里,onClick有一个闭包,保持对元素的引用(通过element.nodeName)。通过将onClick分配给element.click,创建了一个循环引用;即:element→onClick→element→onClick→element...有意思的是,上面的循环自引用阻止了element即使从dom中移除并回收了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([])//...关于最后两个,虽然它们是空的(你可能thinkTheyarefalse),{}和[]实际上是对象,任何对象在JavaScript中都会被强制转换为布尔值“true”,这与ECMA-262规范是一致的。正如这些示例所示,类型强制转换的规则有时非常清晰。因此,除非明确要求类型强制转换,否则最好使用===和!==(而不是==和!=)来避免强制转换的意外副作用。(==和!=自动执行类型转换,而===和!==做相反的事情。)还有一点需要注意:将NaN与任何东西(甚至NaN)进行比较都会得到false。因此,双等号(==,==,!=,!==)不能用来判断一个值是否为NaN。如果需要,可以使用内置的全局isNaN()函数。console.log(NaN==NaN);//Falseconsole.log(NaN===NaN);//Falseconsole.log(isNaN(NaN));//True问题5:效率低下的DOM操作使用JavaScript操作DOM(即添加、修改和删除元素)相对容易,但操作起来效率不高。例如,每次添加一系列DOM元素。添加DOM元素是一项昂贵的操作。在一行中添加多个DOM元素的代码效率低下。当需要添加多个DOM元素时,一个有效的替代方法是使用文档片段来代替,这样可以提高效率和性能。vardiv=document.getElementsByTagName("my_div");varfragment=document.createDocumentFragment();for(vare=0;e'默认'console.log(secondObj.name);//->'unique'但是,如果你这样做:deletesecondObj.name;你得到:console.log(secondObj.name);//'undefined'当使用delete删除属性时,会返回一个undefined,那如果我们也想返回default呢?利用原型继承如下:BaseObject=function(name){if(typeofname!=="undefined"){this.name=name;}};BaseObject.prototype.name='默认';BaseObject派生自其原型对象继承name属性,取值为default。因此,如果调用构造函数时没有命名,name将默认为default。同样,如果name属性从BaseObject的实例中移除,那么将找到原型链的名称,并且其值仍将是默认值。所以'varthirdObj=newBaseObject('unique');console.log(thirdObj.name);//->结果为'unique'deletethirdObj.name;console.log(thirdObj.name);//->结果为“默认”问题8:为实例方法创建错误引用考虑以下代码:varMyObject=function(){}MyObject.prototype.whoAmI=function(){console.log(this===窗口?“窗口”:“MyObj”);};varobj=newMyObject();现在,为了方便起见,我们创建了对whoAmI方法的引用,以便通过whoAmI()而不是更长的obj.whoAmI()调用它。varwhoAmI=obj.whoAmI;为了确保没有问题,让我们打印出whoAmI看看:console.log(whoAmI);output:function(){console.log(this===window?"window":"MyObj");}好的,看起来没问题。接下来,看看我们调用obj.whoAmI()和whoAmI()时的区别。obj.whoAmI();//输出“MyObj”(如预期的那样)whoAmI();//输出“窗口”(呃-哦!)出了什么问题?当我们分配varwhoAmI=obj.whoAmI时,新变量whoAmI被定义在全局命名空间中。结果this的值是window,不是MyObject的obj实例!因此,如果我们真的需要创建对对象现有方法的引用,我们需要确保在该对象的命名空间内进行,以保留this值。一种方法是:varMyObject=function(){}MyObject.prototype.whoAmI=function(){console.log(this===window?"window":"MyObj");};varobj=新的对象();obj.w=obj.whoAmI;//仍在obj命名空间中obj.whoAmI();//输出“MyObj”(如预期的那样)obj.w();//Outputs"MyObj"(asexpected)//Outputs"MyObj"(asexpected)问题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源文件的开头包含“usestrict”;)是一种自愿的运行方式,对JavaScript代码执行更严格的解析和错误处理,同时也使它更安全。然而,不使用严格模式本身并不是“错误”,但越来越鼓励使用它,而不使用它越来越被认为是不好的形式。以下是严格模式的一些主要好处:使调试更容易。原本不会被注意到或未被注意到的代码错误现在会产生错误或抛出异常,从而更快地提醒我们代码库中的JavaScript问题,并更快地找到它们的源头。防止意外的全局变量。在没有严格模式的情况下,为未声明的变量赋值会自动创建一个具有该名称的全局变量。这是最常见的JavaScript错误之一。在严格模式下,尝试这样做会产生错误。消除这种强迫症。在没有严格模式的情况下,对null或undefinedthis值的引用会自动强制转换为全局。在严格模式下,引用null或undefinedthis值会产生错误。不允许重复的属性名称或参数值。严格模式检测对象中的重复命名属性(例如,varobject={foo:"bar",foo:"baz"};)或函数的重复命名参数(例如,functionfoo(val1,val2,val1){}),从而捕获几乎可以肯定是代码中的错误,否则您可能会浪费大量时间进行跟踪。使eval()更安全。eval()在严格模式和非严格模式下的行为方式存在一些差异。最重要的是,在严格模式下,在eval()语句中声明的变量和函数不会在包含范围内创建。(在非严格模式下,它们是在包含域中创建的,这也可能是JavaScript问题的常见来源)。在无效使用delete的情况下抛出错误。delete运算符(用于从对象中删除属性)不能用于对象的不可配置属性。尝试删除不可配置的属性时,非严格代码会静默失败,而在这种情况下,严格模式会抛出错误。写在最后,以上是我今天分享给大家的10个JavaScript中最常见的问题。不知道这10道题,你有不会的吗?如果有,请认真研究,如果没有,请当作复习。另外,如果你觉得今天的文章让你学到了新知识,觉得有用,请记得点赞我,联系我,把这篇文章分享给你的好朋友。最后,感谢阅读,祝编程愉快!