微信搜索 【大迁国际】, 我会第一时刻和你分享前端职业趋势,学习途径等等。
本文 GitHub github.com/qq449245884… 已录入,有一线大厂面试完好考点、资料以及我的系列文章。

今天,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(" trn" == 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);
    };
}

依据上面的代码,假如有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 namespace
obj.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。

来历:www.toptal.com/javascript/…

沟通

有梦想,有干货,微信搜索 【大迁国际】 重视这个在凌晨还在刷碗的刷碗智。

本文 GitHub github.com/qq449245884… 已录入,有一线大厂面试完好考点、资料以及我的系列文章。