天,JavaScript 是幾乎所有現代 Web 應用的核心。這就是為什么JavaScript問題,以及找到導致這些問題的錯誤,是 Web 發者的首要任務。
用于單頁應用程序(SPA)開發、圖形和動畫以及服務器端JavaScript平臺的強大的基于JavaScript的庫和框架已不是什么新鮮事。在 Web 應用程序開發的世界里,JavaScript確實已經無處不在,因此是一項越來越重要的技能,需要掌握。
起初,JavaScript 看起來很簡單。事實上,對于任何有經驗的前端開發人員來說,在網頁中建立基本的JavaScript功能是一項相當簡單的任務,即使他們是JavaScript新手。然而,這種語言比人們最初認為的要細致、強大和復雜得多。事實上,JavaScript的許多微妙之處導致了許多常見的問題,這些問題使它無法工作--我們在這里討論了其中的10個問題--在尋求成為JavaScript開發大師的過程中,這些問題是需要注意和避免的。
隨著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'!
};
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引擎廣泛支持了多年。
如果沒有有意識地編寫代碼來避免內存泄漏,那么內存泄漏幾乎是不可避免的JavaScript問題。它們的發生方式有很多種,所以我們只重點介紹幾種比較常見的情況。
考慮以下代碼:
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被反復賦值,所以兩個函數都共享相同的詞法環境。但是一旦一個變量被任何閉包使用,它就會在該作用域內所有閉包共享的詞法環境中結束。而這個小小的細微差別正是導致這個可怕的內存泄露的原因。
考慮下面代碼:
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仍然認為它們是 "可達的"。
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 操作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元素是很昂貴的,而在分離的情況下創建和修改它們,然后再將它們附加上,就會產生更好的性能。
考慮下面代碼:
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 就會調用時傳進來的的當時值,這樣在點擊元素時,就能顯示正確的序號。
考慮下面代碼:
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'
考慮下面代碼:
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)
首先,需要知道的是為 setTimeout 或 setInterval 提供一個字符串作為第一個參數,這本身并不是一個錯誤。它是完全合法的JavaScript代碼。這里的問題更多的是性能和效率的問題。很少有人解釋的是,如果你把字符串作為setTimeout或setInterval的第一個參數,它將被傳遞給函數構造器,被轉換成一個新函數。這個過程可能很慢,效率也很低,而且很少有必要。
將一個字符串作為這些方法的第一個參數的替代方法是傳入一個函數。
setInterval("logTime()", 1000);
setTimeout("logMessage('" + msgValue + "')", 1000);
更好的選擇是傳入一個函數作為初始參數:
setInterval(logTime, 1000);
setTimeout(function() {
logMessage(msgValue);
}, 1000);
"嚴格模式"(即在JavaScript源文件的開頭包括 "use strict";)是一種自愿在運行時對JavaScript代碼執行更嚴格的解析和錯誤處理的方式,同時也使它更安全。
但是,不使用嚴格模式本身并不是一個 "錯誤",但它的使用越來越受到鼓勵,不使用也越來越被認為是不好的形式。
以下是嚴格模式的一些主要好處:
來源:https://www.toptal.com/javascript/10-most-common-javascript-mistakes?utm_campaign=Weekly%20Vue%20News&utm_medium=email&utm_source=Revue%20newsletter
多的學員比較關心web前端開發面試題都有哪些?其實面試題往往都是圍繞著技術點在拓展。本文給大家分析一下web前端開發面試題之HTML常見問題有哪些?
web前端開發面試題之HTML常見問題:
01、Doctype作用?嚴格模式與混雜模式如何區分?它們有何意義?
02、HTML5 為什么只需要寫 ?
03、行內元素有哪些?塊級元素有哪些? 空(void)元素有那些?
04、頁面導入樣式時,使用link和@import有什么區別?
05、介紹一下你對瀏覽器內核的理解?
06、常見的瀏覽器內核有哪些?
07、html5有哪些新特性、移除了那些元素?如何處理HTML5新標簽的瀏覽器兼容問題?
08、如何區分 HTML 和 HTML5?
09、簡述一下你對HTML語義化的理解?
10、HTML5的離線儲存怎么使用,工作原理能不能解釋一下?
11、瀏覽器是怎么對HTML5的離線儲存資源進行管理和加載的呢?
12、請描述一下 cookies,sessionStorage 和 localStorage 的區別?
13、iframe有那些缺點?
14、Label的作用是什么?是怎么用的?(加 for 或 包裹)
15、HTML5的form如何關閉自動完成功能?
16、如何實現瀏覽器內多個標簽頁之間的通信? (阿里)
17、webSocket如何兼容低瀏覽器?(阿里)
18、頁面可見性(Page Visibility)API 可以有哪些用途?
19、如何在頁面上實現一個圓形的可點擊區域?
20、實現不使用 border 畫出1px高的線,在不同瀏覽器的Quirksmode和CSSCompat模式下都能保持同一效果。
21、網頁驗證碼是干嘛的,是為了解決什么安全問題?
22、tite與h1的區別、b與strong的區別、i與em的區別?
上述問題是從事web前端開發面試官更傾向去提問的,學員們如果對web前端面試題有疑問,可以留言區交流!
于很多剛從Java學習過渡到JavaScript學習的同學來說在定義方法的問題上最頭疼,我們Java人員來學習JavaScript是非常容易上手的,因為語法大部分都相同。但是定義方法是個例外,JavaScript中沒有方法重載的概念,匹配方法只去比較方法名,這跟Java大相徑庭。今天我們來剖析一下這個問題, 既然JavaScript中沒有重載的概念,那么我們定義兩個方法名相同的方法,會出現什么問題呢?
<script >
function calc(a , b){
alert(a + b);
}
calc(1,2);
function calc() {
alert("空參方法");
}
calc();
</script>
這里一執行會彈出兩個空參方法,而不是一個3和一個空參方法。
這是因為在JavaScript中如果定義的方法名相同,下面的會將上面的覆蓋掉。
以上是具名方法聲明的方法,我們如果用具名和匿名方法分別聲明相同的方法名的方法會是一種什么樣的情況呢?
<script >
function calc(a , b){
alert(a + b);
}
calc(1,2);
var calc = function(){
alert("空參方法");
}
calc();
</script>
出乎我們意料,竟然彈了個3又彈了個空參方法,好像互相不影響。
那請看下面這種情況:
<script >
function calc(a , b){
alert(a + b);
}
calc(1,2);
var calc = function(){
alert("空參方法");
}
calc();
function calc(a , b){
alert(a - b);
}
calc(1,2);
</script>
為什么會是這樣一個順序呢?第三個方法會將第一個方法覆蓋掉,這個現象大家能理解。 但為什么第二個方法會將第三個方法覆蓋掉呢?
是因為匿名方式聲明的方法。只有在調用的時候才真正的被加載上。而具名方法在加載之前就會將相同的方法覆蓋掉。所以在加載之前第三個方法會將第一個方法覆蓋掉。所以先彈出了一個"-1"/接著執行到了匿名方法。也就是第二個方法。這時候這個方法才加載到內存中。所以會將已經加載好的第三個方法覆蓋掉。
推薦學習視頻:JavaScript教程_JavaScript視頻教程|黑馬程序員
*請認真填寫需求信息,我們會在24小時內與您取得聯系。