开发人员面临的10个最常见的JavaScript问题
ztj100 2024-10-27 18:33 63 浏览 0 评论
今天,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++) { // 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代码执行更严格的解析和错误处理的方式,同时也使它更安全。
但是,不使用严格模式本身并不是一个 "错误",但它的使用越来越受到鼓励,不使用也越来越被认为是不好的形式。
以下是严格模式的一些主要好处:
- 使得调试更容易。原本会被忽略或无感知的代码错误,现在会产生错误或抛出异常,提醒我们更快地发现代码库中的JavaScript问题,并引导更快地找到其来源。
- 防止意外的全局变量。在没有严格模式的情况下,给一个未声明的变量赋值会自动创建一个具有该名称的全局变量。这是最常见的JavaScript错误之一。在严格模式下,试图这样做会产生一个错误。
- 消除this 强迫性。在没有严格模式的情况下,对 null 或 undefined 的 this 值的引用会自动被强制到全局。在严格模式下,引用null或undefined的this值会产生错误。
- 不允许重复的属性名或参数值。严格模式在检测到一个对象中的重复命名的属性(例如,var object = {foo: "bar", foo: "baz"};)或一个函数的重复命名的参数(例如,function foo(val1, val2, val1){})时抛出一个错误,从而捕捉到你的代码中几乎肯定是一个错误,否则你可能会浪费很多时间去追踪。
- 使得eval()更加安全。eval()在严格模式和非严格模式下的行为方式有一些不同。最重要的是,在严格模式下,在eval()语句中声明的变量和函数不会在包含的范围内创建。(在非严格模式下,它们是在包含域中创建的,这也可能是JavaScript问题的一个常见来源)。
- 在无效使用delete的情况下抛出错误。delete 操作符(用于从对象中删除属性)不能用于对象的非可配置属性。当试图删除一个不可配置的属性时,非严格的代码将无声地失败,而严格模式在这种情况下将抛出一个错误。
来源:https://www.toptal.com/javascript/10-most-common-javascript-mistakes?utm_campaign=Weekly%20Vue%20News&utm_medium=email&utm_source=Revue%20newsletter
相关推荐
- Python 操作excel的坑__真实的行和列
-
大佬给的建议__如何快速处理excelopenpyxl库操作excel的时候,单个表的数据量大一些处理速度还能接受,如果涉及多个表甚至多个excel文件的时候速度会很慢,还是建议用pandas来处理,...
- Python os.path模块使用指南:轻松处理文件路径
-
前言在Python编程中,文件和目录的操作是非常重要的一部分。为了方便用户进行文件和目录的操作,Python标准库提供了os模块。其中,os.path子模块提供了一些处理文件路径的函数和方法。本文主要...
- Python常用内置模块介绍——文件与系统操作详解
-
Python提供了多个强大的内置模块用于文件和系统操作,下面我将详细介绍最常用的几个模块及其核心功能。1.os模块-操作系统交互...
- Python Flask 建站框架实操教程(flask框架网页)
-
下面我将带您从零开始构建一个完整的Flask网站,包含用户认证、数据库操作和前端模板等核心功能。##第一部分:基础项目搭建###1.创建项目环境```bash...
- 为你的python程序上锁:软件序列号生成器
-
序列号很多同学可能开发了非常多的程序了,并且进行了...
- PO设计模式全攻略,在 UI 自动化中的实践总结(以企业微信为例)
-
一、什么是PO设计模式?PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个Page类,即一个py文件,并以页面为单位来写测试用例,实现页面对象和测试用例的...
- 这种小工具居然也能在某鱼卖钱?我用Python一天能写...
-
前两天在某鱼闲逛,本来想找个二手机械键盘,结果刷着刷着突然看到有人在卖——Word批量转PDF小工具...
- python打包成exe,程序有图标,但是任务栏和窗口都没有显示图标
-
代码中指定图标信息#设置应用ID,确保任务栏图标正确显示ifsys.platform=="win32":importctypesapp_id=...
- 使用Python构建电影推荐系统(用python做推荐系统)
-
在日常数据挖掘工作中,除了会涉及到使用Python处理分类或预测任务,有时候还会涉及推荐系统相关任务。...
- python爬取并分析淘宝商品信息(python爬取淘宝商品数据)
-
python爬取并分析淘宝商品信息背景介绍一、模拟登陆二、爬取商品信息1.定义相关参数2.分析并定义正则3.数据爬取三、简单数据分析1.导入库2.中文显示3.读取数据4.分析价格分布5.分析销售...
- OpenCV入门学习基础教程(从小白变大神)
-
Opencv是用于快速处理图像处理、计算机视觉问题的工具,支持多种语言进行开发如c++、python、java等,下面这篇文章主要给大家介绍了关于openCV入门学习基础教程的相关资料,需要的朋友可以...
- python图像处理-一行代码实现灰度图抠图
-
抠图是ps的最基本技能,利用python可以实现用一行代码实现灰度图抠图。基础算法是...
- 从头开始学python:如何用Matplotlib绘图表
-
Matplotlib是一个用于绘制图表的库。如果你有用过python处理数据,那Matplotlib可以更直观的帮你把数据展示出来。直接上代码看例子:importmatplotlib.pyplot...
- Python爬取爱奇艺腾讯视频 250,000 条数据分析为什么李诞不值得了
-
在《Python爬取爱奇艺52432条数据分析谁才是《奇葩说》的焦点人物?》这篇文章中,我们从爱奇艺爬取了5万多条评论数据,并对一些关键数据进行了分析,由此总结出了一些明面上看不到的数据,并...
- Python Matplotlib 库使用基本指南
-
简介Matplotlib是一个广泛使用的Python数据可视化库,它可以创建各种类型的图表、图形和可视化效果。无论是简单的折线图还是复杂的热力图,Matplotlib提供了丰富的功能来满足我们...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Python 操作excel的坑__真实的行和列
- Python os.path模块使用指南:轻松处理文件路径
- Python常用内置模块介绍——文件与系统操作详解
- Python Flask 建站框架实操教程(flask框架网页)
- 为你的python程序上锁:软件序列号生成器
- PO设计模式全攻略,在 UI 自动化中的实践总结(以企业微信为例)
- 这种小工具居然也能在某鱼卖钱?我用Python一天能写...
- python打包成exe,程序有图标,但是任务栏和窗口都没有显示图标
- 使用Python构建电影推荐系统(用python做推荐系统)
- python爬取并分析淘宝商品信息(python爬取淘宝商品数据)
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)