微信搜寻 【大迁世界】, 我会第一工夫和你分享前端行业趋势,学习路径等等。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

明天,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,循环援用被创立;即: elementonClickelementonClickelement...

乏味的是,即便 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);    // Falseconsole.log(NaN === NaN);   // Falseconsole.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 examplefor (var i = 0; i < n; i++) {    elements[i].onclick = function() {        console.log("This is element #" + i);    };}

依据下面的代码,如果有10input 元素,点击任何一个都会显示 "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,而不是 MyObjectobj 实例!

因而,如果咱们真的须要为一个对象的现有办法创立一个援用,咱们须要确保在该对象的名字空间内进行,以保留 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 namespaceobj.whoAmI();  // Outputs "MyObj" (as expected)obj.w();       // Outputs "MyObj" (as expected)

问题9:为 setTimeoutsetInterval 提供一个字符串作为第一个参数

首先,须要晓得的是为 setTimeoutsetInterval 提供一个字符串作为第一个参数,这自身并不是一个谬误。它是齐全非法的JavaScript代码。这里的问题更多的是性能和效率的问题。很少有人解释的是,如果你把字符串作为setTimeoutsetInterval的第一个参数,它将被传递给函数结构器,被转换成一个新函数。这个过程可能很慢,效率也很低,而且很少有必要。

将一个字符串作为这些办法的第一个参数的代替办法是传入一个函数。

setInterval("logTime()", 1000);setTimeout("logMessage('" + msgValue + "')", 1000);

更好的抉择是传入一个函数作为初始参数:

setInterval(logTime, 1000);     setTimeout(function() {          logMessage(msgValue);     }, 1000);

问题10:未应用 "严格模式"

"严格模式"(即在JavaScript源文件的结尾包含 "use strict";)是一种被迫在运行时对JavaScript代码执行更严格的解析和错误处理的形式,同时也使它更平安。

然而,不应用严格模式自身并不是一个 "谬误",但它的应用越来越受到激励,不应用也越来越被认为是不好的模式。

以下是严格模式的一些次要益处:

  • 使得调试更容易。原本会被疏忽或无感知的代码谬误,当初会产生谬误或抛出异样,揭示咱们更快地发现代码库中的JavaScript问题,并疏导更快地找到其起源。
  • 避免意外的全局变量。在没有严格模式的状况下,给一个未声明的变量赋值会主动创立一个具备该名称的全局变量。这是最常见的JavaScript谬误之一。在严格模式下,试图这样做会产生一个谬误。
  • 打消this 强迫性。在没有严格模式的状况下,对 nullundefinedthis 值的援用会主动被强制到全局。在严格模式下,援用nullundefinedthis值会产生谬误。
  • 不容许反复的属性名或参数值。严格模式在检测到一个对象中的反复命名的属性(例如,var object = {foo: "bar", foo: "baz"};)或一个函数的反复命名的参数(例如,function foo(val1, val2, val1){})时抛出一个谬误,从而捕捉到你的代码中简直必定是一个谬误,否则你可能会节约很多工夫去追踪。
  • 使得eval()更加平安。eval()在严格模式和非严格模式下的行为形式有一些不同。最重要的是,在严格模式下,在eval()语句中申明的变量和函数不会在蕴含的范畴内创立。(在非严格模式下,它们是在蕴含域中创立的,这也可能是JavaScript问题的一个常见起源)。
  • 在有效应用delete的状况下抛出谬误。delete 操作符(用于从对象中删除属性)不能用于对象的非可配置属性。当试图删除一个不可配置的属性时,非严格的代码将无声地失败,而严格模式在这种状况下将抛出一个谬误。

代码部署后可能存在的BUG没法实时晓得,预先为了解决这些BUG,花了大量的工夫进行log 调试,这边顺便给大家举荐一个好用的BUG监控工具 Fundebug。

起源:https://www.toptal.com/javasc...

交换

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。