作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
编者注:本文由我们的编辑团队于2023年1月18日更新. 它已被修改,以包括最近的来源,并与我们目前的编辑标准保持一致.
今天,JavaScript几乎是所有现代web应用程序的核心. That’s why JavaScript issues, 和 finding 的 mistakes that cause 的m, 是 at 的 为efront 为 web developers.
强大的基于javascript的库和框架 single page application (SPA) 开发、图形和动画以及服务器端JavaScript平台并不是什么新鲜事物. JavaScript在web应用程序开发领域已经变得无处不在,因此是一项越来越重要的技能.
乍一看,JavaScript似乎很简单. 事实上, 对于任何有经验的软件开发人员来说,在网页中构建基本的JavaScript功能是一项相当简单的任务, even if 的y’re 新 to JavaScript. 然而,语言却明显更加微妙, 强大的, 而且比人们最初想象的要复杂. 事实上, JavaScript的许多微妙之处可能会导致一些常见问题,使其无法正常工作——我们在这里讨论其中的10个问题. 重要的是要意识到这些问题,并避免在你的旅程成为一个 master JavaScript developer.
这
JavaScript开发人员对以下方面不乏困惑 JavaScript的 这
关键字.
随着JavaScript编码技术和设计模式在过去几年中变得越来越复杂, 回调和闭包中的自引用作用域也相应增加了, which 是 a fairly common source of “这
confusion” causing JavaScript issues.
Consider 这 example code snippet:
const 游戏 = 函数() {
这.clearLocalStorage = 函数() {
控制台.log("Clearing local storage...");
};
这.clearBoard = 函数() {
控制台.日志(“清算委员会...");
};
};
游戏.原型.重启= 函数(){
这.clearLocalStorage ();
这.timer = setTimeout(函数() {
这.clearBoard (); // What is "这"?
}, 0);
};
const my游戏 = 新 游戏();
my游戏.重启();
执行上述代码会导致以下错误:
未捕获的类型错误:这个.clearBoard is 不 a 函数
为什么? 一切都取决于环境. 你得到这个错误的原因是,当你调用 setTimeout ()
, you 是 actu所有y invoking 窗口.setTimeout ()
. 因此,将匿名函数传递给 setTimeout ()
is being defined in 的 context of 的 窗口
对象,它没有 clearBoard ()
方法.
传统的、兼容旧浏览器的解决方案是简单地保存对的引用 这
在可由闭包继承的变量中,e.g.:
游戏.原型.重启= 函数(){
这.clearLocalStorage ();
const 自我 = 这; // Save reference to '这', while it’s still 这!
这.timer = setTimeout(函数(){
自我.clearBoard (); // Oh, OK, I do know who '自我' is!
}, 0);
};
或者,在较新的浏览器中,您可以使用 bind ()
方法 to pass in 的 proper reference:
游戏.原型.重启= 函数(){
这.clearLocalStorage ();
这.定时器= setTimeout(此.重置.bind(这), 0); // Bind to '这'
};
游戏.原型.Reset = 函数(){
这.clearBoard (); // OK, back in 的 context of 的 right '这'!
};
正如我们在 JavaScript招聘指南, JavaScript开发人员中一个常见的混淆来源(因此也是常见的bug来源)是假设JavaScript为每个代码块创建一个新的作用域. 虽然这在许多其他语言中是正确的,但它是 不 在JavaScript中为真正的. 例如,考虑下面的代码:
为 (var i = 0; i < 10; i++) {
/* ... */
}
控制台.log(i); // What will 这 output?
如果你猜 控制台.日志()
调用将输出 未定义的
或者抛出一个错误,你猜错了. Believe it 或不, it will output 10
. 为什么?
在大多数其他语言中,上面的代码会导致错误,因为“生命”(i.e.(作用域) i
would be restricted 到 为
块. 但在JavaScript中,情况并非如此 i
remains in scope even after 的 为
循环已完成,在退出循环后保留其最后一个值. (This behavior is known as 变量起重.)
JavaScript中对块级作用域的支持可以通过 的 让
关键字. 的 让
关键字已被浏览器和后端JavaScript引擎(如Node)广泛支持.多年来一直是Js.如果这对你来说是新闻,那就值得花时间仔细阅读一下 scopes, 原型s, 和 more.
内存泄漏在JavaScript中几乎是不可避免的问题,如果你没有有意识地去避免它们的话. 它们发生的方式有很多种, 所以我们只强调两种更常见的情况.
注意:这个例子只适用于遗留的JavaScript引擎——现代的引擎有足够聪明的垃圾收集器(gc)来处理这种情况.
Consider 的 following code:
var 要发生 = null;
var replaceThing = 函数(){
var priorThing = 要发生; // Hold on 到 prior thing
var 未使用的 = 函数(){
// '未使用的'是唯一引用'priorThing'的地方;
// but '未使用的' never gets invoked
if (priorThing) {
控制台.日志(“嗨”);
}
};
要发生 = {
longStr: 新 Array(1000000).join('*'), // Create a 1MB object
somee方法: 函数(){
控制台.日志(someMessage);
}
};
};
setInterval(replaceThing, 1000); // Invoke 'replaceThing' once every second
如果运行上述代码并监视内存使用情况, 您将发现有一个严重的内存泄漏—每秒整整兆字节! 即使是手动垃圾收集器也无济于事. So it looks like we 是 leaking longStr
每一次 replaceThing
被称为. 但是为什么?
内存泄漏在JavaScript中几乎是不可避免的问题,如果你没有有意识地去避免它们的话.
块quote>Let’s examine things in more detail:
每一个 要发生
object contains its own 1MB longStr
object. Every second, when we c所有 replaceThing
, it holds on to a reference 到 prior 要发生
对象 priorThing
. 但我们仍然认为这不会是个问题, 自, 每次通过, 前面提到的 priorThing
would be dereferenced (when priorThing
是通过 priorThing = 要发生;
). 此外,它只在正文中被引用 replaceThing
在函数中 未使用的
, which is, in fact, never used.
所以我们再次疑惑为什么这里会有内存泄漏.
为了理解发生了什么,我们需要更好地理解JavaScript的内部工作原理. 闭包通常由链接到表示其词法范围的字典样式对象的每个函数对象实现. If both 函数s defined inside replaceThing
实际使用 priorThing
,它们都得到相同的对象是很重要的,即使 priorThing
多次赋值,以便两个函数共享相同的词法环境. 但是一旦变量被任何闭包使用, 它最终出现在该范围内所有闭包共享的词法环境中. 正是这个细微的差别导致了这种粗糙 内存泄漏.
Consider 这 code 片段:
函数 addClickH和ler(元素) {
元素.点击 = 函数 onClick(e) {
alert("Clicked 的 " + 元素.节点名)
}
}
在这里, onClick
has a closure that keeps a reference to 元素
(通过 元素.节点名
). 通过分配 onClick
to 元素.点击
, 的 circular reference is created, i.e., 元素
→ onClick
→ 元素
→ onClick
→ 元素
…
有趣的是,即使 元素
从DOM中删除,上面的循环自引用将阻止 元素
和 onClick
从被收集,从而会成为内存泄漏.
JavaScript的内存管理(特别是它的 垃圾收集)很大程度上是基于对象可达性的概念.
的 following objects 是 assumed to be 可获得的 它们被称为“根”:
只要对象可以通过引用或引用链从任何根访问,对象就会保存在内存中.
的re is a garbage collector in 的 browser that cleans memory occupied by un可获得的 objects; in o的r words, objects will be removed from memory 当且仅当 的 GC believes that 的y 是 un可获得的. 不幸的是, 很容易得到不再使用的“僵尸”对象,但GC仍然认为它们是可访问的.
JavaScript的一个便利之处在于,它将自动强制在布尔上下文中引用的任何值为布尔值. 但在某些情况下,这种做法既方便又令人困惑. 例如,对于许多JavaScript开发人员来说,下面的表达式是很麻烦的:
// 所有 of 的se evaluate to '真正的'!
控制台.日志(假 == '0');
控制台.日志(null == 未定义的);
控制台.Log (" \t\r\n" == 0);
控制台.Log (" == 0 ");
//这些也是!
if ({}) // ...
if ([]) // ...
With regard 到 last two, 尽管是空的(这可能会让你相信他们会评估为) 假
),这两个 {}
和 []
它们实际上是对象吗 任何 对象将被强制为的布尔值 真正的
in JavaScript, consistent 与 的 ecma - 262规范.
正如这些例子所表明的那样,类型强制转换的规则有时可能像泥一样清晰. 因此,除非明确需要类型强制转换,否则通常最好使用它 ===
和 !==
(而不是 ==
和 !=
),以避免类型强制转换的任何意外副作用. (==
和 !=
在比较两个事物时自动执行类型转换,而 ===
和 !==
在没有类型转换的情况下进行相同的比较.)
既然我们在讨论类型强制转换和比较,比较就值得一提了 南
与 任何东西 (甚至 南
!)将 总是 返回 假
. 因此,不能使用相等运算符(==
, ===
, !=
, !==
) to determine whe的r a value is 南
或不. Instead, use 的 built-in 全球 is南 ()
功能:
控制台.log(南 == 南); // False
控制台.log(南 === 南); // False
控制台.log(is南(南)); // True
JavaScript使得操作DOM相对容易.e.(添加、修改和删除元素),但并没有提高这样做的效率.
一个常见的例子是每次添加一个DOM元素的代码. 添加DOM元素是一项开销很大的操作, 而连续添加多个DOM元素的代码效率很低,很可能不能很好地工作.
当需要添加多个DOM元素时,一个有效的替代方法是使用 文档片段 相反,这将 improve efficiency 和 per为mance.
例如:
div =文档.getElementById(“my_div”);
Const片段=文档.createDocumentFragment ();
Const 元素s = document.querySelector所有 (' a ');
为 (让 e = 0; e < elems.length; e++) {
片段.列表末尾(elem [e]);
}
div.列表末尾(片段.版本(真正的));
除了固有的提高效率的这种方法, 创建附加DOM元素的成本很高, 而创建和修改它们,而分离,然后附加它们产生更好的性能.
为
循环考虑这段代码:
Var 元素s = document.getElementsByTagName('input');
Var n = 元素s.length; // Assume we have 10 元素s 为 这 example
为 (var i = 0; i < n; i++) {
元素(我).On点击 = 函数() {
控制台.log("This is 元素 #" + i);
};
}
根据上面的代码,如果有10个输入元素,单击 任何 其中一个会显示" This is 元素 #10 "! This is because, by 的 time on点击
为 任何 of 的 元素s, 的 above 为
循环将已经完成,并且值 i
已经10岁了吗 所有 ).
下面是我们如何纠正这个JavaScript问题,以实现所需的行为:
Var 元素s = document.getElementsByTagName('input');
Var n = 元素s.length; // Assume we have 10 元素s 为 这 example
var makeH和ler = 函数(全国矿工工会){//外部函数
返回 函数() { // Inner 函数
控制台.log("This is 元素 #" + 全国矿工工会);
};
};
为 (var i = 0; i < n; i++) {
元素(我).on点击 = makeH和ler(i+1);
}
In 这 revised version of 的 code, makeH和ler
在每次通过循环时立即执行, 的当前值 i+1
和 binding it to a scoped 全国矿工工会
变量. 外部函数返回内部函数(也使用此作用域) 全国矿工工会
变量) 和 的 元素’s on点击
is set to that inner 函数. 这确保了每个 on点击
receives 和 uses 的 proper i
值(通过作用域) 全国矿工工会
变量).
相当多的JavaScript开发人员无法完全理解, 和 的re为e fully leverage, 的 features of prototypal inheritance.
这里有一个简单的例子:
BaseObject = 函数(名字) {
If (typeof 名字 !== "未定义"){
这.Name = Name;
} else {
这.Name = “默认”
}
};
This seems fairly straight为ward. 如果您提供了名称,请使用它,否则将名称设置为' 默认的 '。. 例如:
var firstObj = 新 BaseObject();
var secondObj = 新 BaseObject('unique');
控制台.日志(firstObj.名字); // -> Results in “默认”
控制台.日志(secondObj.名字); // -> Results in 'unique'
But what if we were to do 这:
删除secondObj.名称;
我们得到:
控制台.日志(secondObj.名字); // -> Results in '未定义的'
但是,如果这个恢复为“默认”不是更好吗?? 如果我们修改原始代码以利用原型继承,这很容易做到, 如下:
BaseObject = 函数 (名字) {
如果(typeof的名字 !== "未定义"){
这.Name = Name;
}
};
BaseObject.原型.Name = “默认”;
有了这个版本, BaseObject
继承了 名字
它的属性 原型
object, where it is set (by 默认的) to “默认”
. 因此,如果调用构造函数时没有名称,则该名称将默认为 默认的
. 类似地,如果 名字
property is removed from an 的实例 BaseObject
,原型链将被搜索,然后 名字
property will be retrieved from 的 原型
object where its value is still “默认”
. 现在我们得到:
var thirdObj = 新 BaseObject('unique');
控制台.日志(thirdObj.名字); // -> Results in 'unique'
删除thirdObj.名称;
控制台.日志(thirdObj.名字); // -> Results in “默认”
让我们定义一个简单的对象,并创建它的实例,如下所示:
var MyObjectFactory = 函数() {}
MyObjectFactory.原型.显示本用户信息 = 函数() {
控制台.日志(这个);
};
var obj = 新 MyObjectFactory();
现在,为了方便起见,让我们创建一个对 显示本用户信息
方法,这样我们就可以通过 显示本用户信息 ()
而不是更长的 obj.显示本用户信息 ()
:
var 显示本用户信息 = obj.显示本用户信息;
为了确保我们存储了一个对函数的引用,让我们打印出新的值 显示本用户信息
变量:
控制台.日志(显示本用户信息);
输出:
函数(){
控制台.日志(这个);
}
到目前为止看起来还不错.
但是看看我们调用时的不同 obj.显示本用户信息 ()
versus our convenience reference 显示本用户信息 ()
:
obj.显示本用户信息 (); // Outputs "MyObjectFactory {...}”(果然如此)
显示本用户信息 (); // Outputs "窗口" (uh-oh!)
出了什么问题?? 我们的 显示本用户信息 ()
C所有在 全球 名称空间,所以 这
设置为 窗口
(或者,在严格模式下,to 未定义的
), 不 到 obj
的实例 MyObjectFactory
! In o的r words, 的 value of 这
norm所有y depends on 的 c所有ing context.
箭头函数((params) => {}
而不是 函数(params) {}
提供静态 这
它不是基于调用上下文的 这
是正则函数. This gives us a workaround:
var MyFactoryWithStaticThis = 函数() {
这.显示本用户信息 = () => { // Note 的 arrow 不ation here
控制台.日志(这个);
};
}
var objWithStaticThis = 新 MyFactoryWithStaticThis();
var 显示本用户信息WithStaticThis = objWithStaticThis.显示本用户信息;
objWithStaticThis.显示本用户信息 (); // Outputs "MyFactoryWithStaticThis" (as usual)
显示本用户信息WithStaticThis(); // Outputs "MyFactoryWithStaticThis" (arrow 不ation benefit)
您可能已经注意到,即使我们得到了匹配的输出, 这
是对工厂的引用而不是对实例的引用. 而不是试图进一步解决这个问题, 值得考虑不依赖于JavaScript的方法 这
(甚至 新
) at 所有, as explained in 作为一个JS开发者,这就是让我夜不能寐的原因.
setTimeout
or setInterval
对于初学者,让我们在这里弄清楚一些事情:提供一个字符串作为 setTimeout
or setInterval
is 不 这本身就是一个错误. 这是完全合法的JavaScript代码. 这里的问题更多的是性能和效率. 通常被忽略的是,如果将字符串作为第一个参数传递给 setTimeout
or setInterval
, it will be passed 到 函数构造器 to be converted into a 新 函数. 这个过程可能是缓慢和低效的,很少是必要的.
传递字符串作为这些方法的第一个参数的替代方法是传入一个 函数. 让我们来看一个例子.
那么,这里将是一个相当典型的用法 setInterval
和 setTimeout
,通过… 字符串 作为第一个参数:
setInterval("logTime()", 1000);
setTimeout("logMessage(' ' + msgValue + ' ')", 1000);
的 better choice would be to pass in a 函数 as 的 initial argument, e.g.:
setInterval(logTime, 1000); // Passing 的 logTime 函数 to setInterval
setTimeout(函数(){//向setTimeout传递一个匿名函数
logMessage(msgValue); // (msgValue is still accessible in 这 scope)
}, 1000);
正如我们的 JavaScript招聘指南,“严格模式”(i.e.,包括 使用严格的;
(在JavaScript源文件的开头)是一种在运行时自愿对JavaScript代码执行更严格的解析和错误处理的方法, 也是一种使代码更安全的方法.
诚然,没有使用严格模式并不是一个真正的“错误”,但它的使用越来越多 被鼓励 它的遗漏越来越被认为是一种糟糕的形式.
以下是严格模式的一些主要好处:
这
强迫. 没有严格模式, a reference to a 这
值为null或未定义的将自动强制到 全球This
变量. This can cause m任何 frustrating bugs. In strict mode, referencing a 这
值为空或未定义将抛出错误.var object = {foo: "bar", foo: "baz"};
)或函数的重复命名参数(例如.g., 函数 foo(val1, val2, val1){}
), 从而捕获代码中几乎肯定存在的错误,否则您可能会浪费大量时间来跟踪这些错误.eval ()
更安全的. 的re 是 some differences in 的 way eval ()
在严格模式和非严格模式下运行. 最重要的是,在严格模式下,变量和函数声明在 eval ()
声明是 不 created in 的 containing scope. (他们 是 在包含范围内以非严格模式创建, 这也可能是JavaScript的常见问题来源.)删除
. 的 删除
操作符(用于从对象中删除属性)不能用于对象的不可配置属性. 当试图删除不可配置属性时,非严格代码将静默失败, 而严格模式在这种情况下会抛出错误.As is 真正的 与 任何 technology, 你就能更好地理解JavaScript为什么和如何工作和不工作, 你的代码越可靠,你就越能有效地利用语言的真正力量. 相反, 缺乏对JavaScript范例和概念的正确理解是许多JavaScript问题的根源.彻底熟悉语言的细微差别和微妙之处是提高你的熟练程度和提高效率的最有效策略.
开发人员在编写JavaScript代码时常犯的错误包括错误地思考“这”关键字的工作原理, 关于块作用域的错误假设, 和 a failure to avoid 内存泄漏s. 随着时间的推移,如果遵循旧的编码模式,JavaScript的演变留下了许多陷阱.
您可以通过在实际代码中使用最佳实践来提高JavaScript技能, 阅读语言中固有的细微差别,以了解其更高级的功能和局限性.
有缺陷的代码在表面上看起来完全无害. 通过学习常见的JavaScript问题, 您可以开始理解是什么导致某些编码模式出现问题,以及如何在自己的代码中避免它们.
有些人可能认为JavaScript语言本身就是有问题的. 事实上, 它有它的缺点, 但它也是无处不在的,所以如果你(像今天的大多数开发人员一样)必须使用某种形式的JavaScript代码,那么了解如何导航它们是值得的.
Ryan是一名建筑师、企业家和开发人员. 他擅长于构建云可伸缩、可扩展的软件系统.
World-class articles, delivered weekly.
<为m aria-label="Sticky subscribe 为m" class="-Ulx1zbi P7bQLARO _3vfpIAYd">Subscription implies consent to our 隐私政策
World-class articles, delivered weekly.
<为m aria-label="Bottom subscribe 为m" class="-Ulx1zbi P7bQLARO _3vfpIAYd">Subscription implies consent to our 隐私政策