如何在 JavaScript 面試中過五關斬六將?

JavaScript 面試不容易。我覺得難,你也覺得不容易,大家的意見不謀而合。在 JavaScript 面試中被問問題的概率通常很高。那麼該如何破解 JS 面試?突破口在哪兒?本文旨在通過學習基本概念來指導所有有志向的 JavaScript 開發者加深他們的 JS 知識。
如何在 JavaScript 面試中過五關斬六將?


作者 | Mohammad Ayub

譯者 | 譚開朗


應對 JS 面試,本文至少算是必備常識。如果我是候選人,我會爭取很好地掌握這些概念。如果我是面試官,我認為只有掌握這些重要概念的開發者才能走得更遠。

本文對於 JS 開發者來說,是入門級指南而非資深級。不同的人有責任為更艱難的面試做好準備。面試者還需要記住,面試問題也可以源自他們的工作領域和技術(例如:React JS、WebPack、Node JS 等)。本文將介紹基本的 JS 元素,只有非常精通它們的人才能被稱為一名優秀的 JS 開發者。優秀的 JS 開發者可以是優秀的 React 開發者,反之不一定成立。遺憾的是,JS 因衍生出了大量不規範的腳本(部分屬實)而常常被人詬病。JS 協助開發者實現產品功能,滿意度較高。編程也是趣事。很少有像 John Resig(jQuery 創建者)、Brendan Eich(JS 創建者)和 Lars Bak(谷歌 Chrome 團隊)這麼偉大的 JavaScript 程序員,能夠完全理解這種語言。成功的 JS 程序員常常查閱代碼庫中基本的 JS 代碼。許多人認為很難找到一名優秀的 JS 開發者。

“虛擬機就像一種奇怪的野獸。我們沒有完美的解決方案,而是力爭優化至‘最佳點’。而優化的方法有很多。這是一場漫長的遊戲,你不會倦怠的。”——Lars Vak ,Google

為了說明 JS 面試的複雜性,看下面的 JS 表述,試著第一反應說出結果。

console.log(2.0 == “2” == new Boolean(true) == “1”)

90%的人認為輸出 false。但答案是 true。為什麼?往下看。

JavaScript 很難。如果面試官很聰明的避開類似以上的問題,我們就無能為力了。但是我們能做什麼呢?深入學習這11個基本要素,有助於應對 JS 面試。


理解 JS 函數


函數是 JS 的精華。它們是第一類公民。如果沒有深入理解函數,你的 JS 知識就像一盤散沙。JS 函數不僅僅是一個普通函數。與其他編程語言不同,函數可以賦值給變量,可以作為參數傳遞給另一個函數,也可以從另一個函數中返回。因此,函數是 JS 的第一類公民。

這裡就不贅述函數的概念了,但你知道的吧?函數就類似這樣!

console.log(square(5));
/* ... */

function square(n) { return n * n; }

這段代碼的執行結果是25。正確!再看下面的代碼:

console.log(square(5));
var square = function(n) {
return n * n;
}

乍一看,你可能會說這也輸出25。錯!相反,第一行報錯了:

TypeError: square is not a function

在 JS 中,如果將函數定義為變量,這函數名將被掛起,只有當 JS 執行到它的定義位置時才能訪問到。出乎意料了嗎?

先不管它。你可能在某些代碼中經常看到這種語法。

var simpleLibrary = function() {
var simpleLibrary = {
a,
b,
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
}
}
return simpleLibrary;
}();

是不是有點費解?它是一個函數變量,裡面的變量和函數不會汙染到全局作用域。從 jQuery 到 Lodash 之類的庫都用 $etc 表示該用法。

在這裡我想說的是“學好函數”。在使用函數的過程中可能會有很多小陷阱。瀏覽 Mozilla 介紹的函數用法吧,寫的很好(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions)。


掌握 bind, apply 和 call 的用法


這些函數在所有知名的庫中可能都可以看到。它們提供了柯里化的方法,可通過編寫不同的函數來實現功能。優秀的 JavaScript 開發者可以隨時說出這三個函數的用法。

本質上,它們是函數的原型方法,通過改變行為來實現某些功能。根據 JS 開發者 Chad 的說法,它們的用法是這樣的:

當希望延遲調用帶有特定上下文的函數時,使用 .bind(),這在事件中很有用。當希望立刻調用函數時,使用 .call() 或 .apply(),同時會修改上下文。

call 函數拯救了我!

讓我們看看上面論述代表什麼意思。假設你的數學老師要求你創建一個庫並提交它。你編寫了一個計算圓的面積與周長的抽象的庫。

var mathLib = {
pi: 3.14,
area: function(r) {
return this.pi * r * r;
},
circumference: function(r) {
return 2 * this.pi * r;
}
};

你把代碼庫提交給老師。現在運行調用該數學庫的代碼。

mathLib.area(2);
12.56

正要提交第二個代碼示例時,你恍然發覺老師要求 pi 常數精確到小數點後五位。噢天哪!你是用了3.14不是3.14159。但現在截止日期已過,不能再提交代碼了。JS 的 call 函數拯救了你。只需這樣:

mathLib.area.call({pi: 3.14159}, 2);

那麼它在執行中會取新的pi值。輸出結果是:

12.56636

如此,老師會很欣慰。你會發現 call 函數接收了兩個參數:

  • 上下文
  • 函數參數

上下文是在函數體內替換 this 的對象。接著,參數會通過函數的參數傳入。例如:

var cylinder = {
pi: 3.14,
volume: function(r, h) {
return this.pi * r * r * h;
}
};

call 是這樣用的:

cylinder.volume.call({pi: 3.14159}, 2, 6);
75.39815999999999

發現了嗎?函數參數是在上下文對象後,作為參數傳遞的。

Apply 是完全相同的用法,只是函數參數是以列表的形式傳遞。

cylinder.volume.apply({pi: 3.14159}, [2, 6]);
75.39815999999999

如果你瞭解 call 函數,那你就也瞭解 apply 函數,反之亦然。那什麼是 bind 函數?

Bind 將一個全新的 this 賦給指定的函數。Bind 與 call 或 apply 不同,bind 情況下,函數不會立即執行。

var newVolume = cylinder.volume.bind({pi: 3.14159}); // This is not instant call
// After some long time, somewhere in the wild
newVolume(2,6); // Now pi is 3.14159

Bind 函數有什麼用途?它提供了給函數傳入上下文的方法,並返回帶有更新的上下文的函數。

這意味著 this 變量就是用戶提供的變量。這在處理 JavaScript 事件時非常有用。

建議掌握這三個函數,以便用 JavaScript 編寫功能代碼。


理解 JavaScript 作用域(以及閉包)


JavaScript 的作用域就像一個潘多拉寶盒。成百上千的面試難題都是由這一簡單的概念演變而來。

作用域分為三種:

  • 全局作用域
  • 當前作用域/函數作用域
  • 塊級作用域(ES6 中有介紹)

全局作用域是我們常用的:

x = 10;
function Foo() {
console.log(x); // Prints 10
}
Foo()

當你在當前函數定義一個變量,函數作用域就出現了:

pi = 3.14;
function circumference(radius) {
pi = 3.14159;
console.log(2 * pi * radius); // Prints "12.56636" not "12.56"
}
circumference(2);

ES16 標準引入了新的塊級作用域,塊級作用域將變量的作用範圍限制在特定的括號內。

var a = 10;
function Foo() {
if (true) {
let a = 4;
}
alert(a); // alerts '10' because the 'let' keyword
}
Foo();

函數和判斷條件都被視為塊。在上面的例子中,條件判斷為真故本該彈出4。但 ES6 破壞了塊級變量的作用域,使之變成了全局作用域。

現在再來看看作用域的神奇之處。作用域可以通過閉包來實現。JavaScript 閉包就是一個函數返回另一個函數。

如果有人要求你:寫一個傳入字符串並返回單個字符的範例。一旦更新的字符串,輸出也跟著替換掉舊的。這簡稱為生成器。

function generator(input) {
var index = 0;
return {
next: function() {
if (index < input.length) {
index += 1;
return input[index - 1];
}
return "";
}
}
}

生成器是這樣執行的!

var mygenerator = generator("boomerang");
mygenerator.next(); // returns "b"
mygenerator.next() // returns "o"
mygenerator = generator("toon");
mygenerator.next(); // returns "t"

在這裡,作用域扮演著重要角色。閉包是一個返回另一個函數和封裝數據的函數。上面的字符生成器就是一個閉包。索引值在多個函數調用間保存。定義的內層函數可以訪問外層函數定義的變量。這是不同的作用域。如果在二級函數里再定義一個函數,這個函數可以訪問所有外層函數的變量。

針對 JavaScript 作用域可以問很多問題,吃透它吧。


理解 this 關鍵詞(全局,函數和對象範圍)


用 JavaScript 編碼,我們通常會用到函數和對象。如果是在瀏覽器上運行,全局上下文指的是 Window 對象。這意味著,打開瀏覽器的控制檯並輸入下面的內容,按下回車鍵,它會返回 true。

this === window;

當程序的上下文和作用域發生了改變,this 的指向也跟著改變。現在看看當前上下文的 this:

function Foo(){
console.log(this.a);
}
var food = {a: "Magical this"};
Foo.call(food); // food is this

現在,預想下面輸出。

function Foo(){
console.log(this); // prints {}?
}

不會輸出。因為在這是一個全局對象。記住,無論父級作用域是什麼,子級都會繼承父級作用域。因此它輸出 Window 對象。以上討論的三個方法實際是用來設置 this 對象的。

現在來看 this 的最後一種類型。對象作用域中的 this。如下:

var person = {
name: "Stranger",
age: 24,
get identity() {
return {who: this.name, howOld: this.age};
}
}

這裡用了 getter 語法,以參數形式去調用了一個函數。

person.identity; // returns {who: "Stranger", howOld: 24}

在這裡,this 實際指向對象本身。正如我們之前提到的,this 在不同地方的表現不同。掌握 this 的用法吧。


掌握對象的用法(Object.freeze, Object.seal)


很多人都知道這樣的對象:

var marks = {physics: 98, maths:95, chemistry: 91};

它是保存鍵值對的映射。JavaScript 對象有一個特殊屬性,可以將任何數據存儲為值。這意味著我們可以以值的形式儲存列表,另一個對象,函數等。諸如此類。

創建對象的方法有:

var marks = {};
var marks = new Object();

分別使用 JSON 對象的 stringify 和 parse 方法,可以輕鬆地將給定對象轉換成 JSON 字符串和 JSON 對象。

// returns "{"physics":98,"maths":95,"chemistry":91}"
JSON.stringify(marks);
// Get object from string
JSON.parse('{"physics":98,"maths":95,"chemistry":91}');

那麼關於對象我們需要知道什麼?使用 Object.keys 遍歷對象很容易

var highScore = 0;
for (i of Object.keys(marks)) {
if (marks[i] > highScore)
highScore = marks[i];
}

Object.value返回對象的值列表。

對象的其他重要函數包括:

  • Object.prototye(object)
  • Object.freeze(function)
  • Object.seal(function)

Object.prototye 提供了包含許多應用的更重要的函數,其中一些是:

Object.prototye.hasOwnProperty 用來查找對象中是否存在指定的屬性/鍵值。

marks.hasOwnProperty("physics"); // returns true 

marks.hasOwnProperty("greek"); // returns false

Object.prototye.instanceof 評定給定的對象是否是特性原型的類型(將在下一部分介紹,它們屬於函數)。

function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
var newCar = new Car('Honda', 'City', 2007);
console.log(newCar instanceof Car); // returns true

現在看一下另外兩個函數。Object.freeze 可以凍結對象,因此現有屬性不會被修改。

var marks = {physics: 98, maths:95, chemistry: 91};
finalizedMarks = Object.freeze(marks);
finalizedMarks["physics"] = 86; // throws error in strict mode
console.log(marks); // {physics: 98, maths: 95, chemistry: 91}

在這裡我們試圖修改凍結對象後的 physics 屬性的值。但是,JavaScript 不允許這麼做。我們可以通過下面的方法查看給定的對象是否被凍結:

Object.isFrozen(finalizedMarks); // returns true

Object.seal 和 Object.freeze 略有不同。Object.seal 允許配置已有屬性,但不允許添加新屬性,不能增刪已有屬性。

var marks = {physics: 98, maths:95, chemistry: 91};
Object.seal(marks);
delete marks.chemistry; // returns false as operation failed
marks.physics = 95; // Works!
marks.greek = 86; // Will not add a new property

我們也可以通過下面的方法檢查給定的對象是否被密封:

Object.isSealed(marks); // returns true


掌握原型繼承


古典繼承在 JavaScript 中被模擬。它是使用了原型方法。在 ES5,ES6 中看到的所有新的 class 語法都只是包裹在底層原型 OOP 的語法糖。使用 JavaScript 函數就能創建類。

var animalGroups = {
MAMMAL: 1,
REPTILE: 2,
AMPHIBIAN: 3,
INVERTEBRATE: 4
};
function Animal(name, type) {
this.name = name;
this.type = type;
}
var dog = new Animal("dog", animalGroups.MAMMAL);
var crocodile = new Animal("crocodile", animalGroups.REPTILE);

在這裡我們給類創建對象(通過 new 關鍵字)。我們可以給這些指定的類(函數)添加方法。添加類的方法可以是這樣:

Animal.prototype.shout = function() {
console.log(this.name + 'is ' + this.sound + 'ing...');
}

這裡你可能會有疑問。類中並沒有 sound 屬性。對!這裡根本沒有定義 sound 屬性。它是由繼承父類的子類傳遞的。

在 JavaScript 中,繼承是這樣實現的:

function Dog(name, type) {
Animal.call(this, name, type);
this.sound = "bow";
}

定義一個更具體的函數 Dog。在這裡,為了繼承 Animal 類,我們需要引用 call 函數(上面討論過)來傳遞 this 和其他參數。我們可以通過以下方法來實例化 German Shepard。

var pet = Dog("germanShepard", animalGroups.MAMMAL);
console.log(pet); // returns Dog {name: "germanShepard", type: 1, sound: "bow"}

我們並沒有在子函數中聲明 name 和 type, 而是調用了 Animal 函數並設置相應的屬性。pet 從父類那裡獲得了屬性(name, type)。那麼方法也能繼承嗎?讓我們一起來看看!

pet.shout(); // Throws error

什麼?為什麼會這樣?出現這種情況是因為 JavaScript 不能繼承父類的方法。如何解決這個問題呢?

// Link prototype chains
Dog.prototype = Object.create(Animal.prototype);
var pet = new Dog("germanShepard", animalGroups.MAMMAL);
// Now shout method is available

pet.shout(); // germanShepard is bowing...

像現在這樣 shout 方法是可用的。我們可以通過 object.constructor 函數來檢查 JavaScript 中指定對象的類。讓我們看看 pet 的類是什麼。

pet.constructor; // returns Animal

這個答案不夠準確。Animal 是一個父類。但 pet 到底是什麼類型?它屬於 Dog 類型。這是因為 Dog 類的構造函數。

Dog.prototype.constructor; // returns Animal

返回 Animal。我們應該把它設置為 Dog 類本身,如此一來,類的所有實例(對象)會指向它從屬於的正確類名。

Dog.prototype.constructor = Dog;

關於原型繼承,請記住以下四點:

  • 類屬性用this界定
  • 類方法使用prototype對象界定
  • 繼承屬性,請使用call函數傳遞this對象
  • 繼承方法,請使用Object.create連接父類和子類的原型
  • 始終將子類構造函數設置為自身,以獲取對象的正確標識

小注:即使在新的類語法中,也會在底層發生以上事件。知道這些對掌握JS知識很有幫助。

在 JS 中,call 函數和 prototype 對象造就了繼承。


理解回調函數和 promises


回調函數是在輸入/輸出操作完成後執行的。在 Python/Ruby 中,輸入/輸出的過程可能會阻塞代碼而不允許進一步執行。但在 JavaScript 中,因其允許異步操作,所以可以給異步函數提供回調。例如,通過操作鼠標或鍵盤等,觸發 AJAX(XMLHttpRequest)從瀏覽器調服務器接口。代碼如下:

function reqListener () {
console.log(this.responseText);
}
var req = new XMLHttpRequest();
req.addEventListener("load", reqListener);
req.open("GET", "http://www.example.org/example.txt");
req.send();

在這裡,reqListener 是回調函數,當 GET 請求成功返回時,將執行該回調函數。

Promises 是回調函數的簡潔封裝器,能優雅的執行異步代碼。本文討論了很多關於 promises 的內容。這也是JS中應該掌握的一個重要內容。


掌握正則表達式


正則表達式的用途很多。處理文本,限制用戶的輸入規則等。JavaScript 開發者應該掌握基本的正則表達式並用來解決實際問題。正則表達式是一個通用概念。接下來,一起來看看在 JS 中如何使用正則表達式。

我們可以通過以下方法創建一個新的正則表達式:

var re = /ar/;
var re = new RegExp('ar'); // This too works

上面的正則表達式表示與給定字符串匹配的表達式。一旦定義了一個正則表達式,我們可以嘗試匹配和查看符合條件的字符串。我們可以使用exec函數來匹配字符串。

re.exec("car"); // returns ["ar", index: 1, input: "car"]
re.exec("cab"); // returns null

很少特殊的字符類可以用來構建複雜的正則表達式。

正則表達式包含許多類型的元素。其中一些是:

  • 字符:\\w-查找單詞字符,\\d-查找數字,\\D-查找非數字字符
  • 字符類:[x-y]查找從x到y到字符,[^x]查找非x的任何字符
  • 量詞:+,?,*(查找多個或0個匹配字符)
  • 邊界:^(開頭),$(結尾)

對以上內容加以舉例說明,如下:

/* Character class */
var re1 = /[AEIOU]/;
re1.exec("Oval"); // returns ["O", index: 0, input: "Oval"]
re1.exec("2456"); // null
var re2 = /[1-9]/;
re2.exec('mp4'); // returns ["4", index: 2, input: "mp4"]
/* Characters */
var re4 = /\\d\\D\\w/;
re4.exec('1232W2sdf'); // returns ["2W2", index: 3, input: "1232W2sdf"]
re4.exec('W3q'); // returns null
/* Boundaries */
var re5 = /^\\d\\D\\w/;
re5.exec('2W34'); // returns ["2W3", index: 0, input: "2W34"]
re5.exec('W34567'); // returns null
var re6 = /^[0-9]{5}-[0-9]{5}-[0-9]{5}$/;
re6.exec('23451-45242-99078'); // returns ["23451-45242-99078", index: 0, input: "23451-45242-99078"]
re6.exec('23451-abcd-efgh-ijkl'); // returns null
/* Quantifiers */
var re7 = /\\d+\\D+$/;
re7.exec('2abcd'); // returns ["2abcd", index: 0, input: "2abcd"]
re7.exec('23'); // returns null
re7.exec('2abcd3'); // returns null

var re8 = /(.*?)/;
re8.exec('

Hello JS developer

'); //returns ["

Hello JS developer

", "p", "Hello JS developer", index: 0, input: "

Hello JS developer

"]

正則表達式的更多細節內容,請參考手冊

(http://www.rexegg.com/regex-qu

ickstart.html)。

除了 exec 函數,還有 match, search 和 replace 函數,它們可通過正則表達式找到某個字符串。但這些函數應該應用於字符串本身。

"2345-678r9".match(/[a-z A-Z]/); // returns ["r", index: 8, input: "2345-678r9"]
"2345-678r9".replace(/[a-z A-Z]/, ""); // returns 2345-6789

開發者應掌握正則表達式這一重要內容,以便輕鬆解決複雜的問題。


熟悉 Map, Reduce 和 Filter


函數式編程是當今的一個熱門話題。許多編程語言都將諸如 lambdas 之類的函數概念添加到它們的新版本中(例如:Java 7以上版本)。JavaScript 對函數式編程的支持由來已久。我們需要深入學習三個主要函數。數學函數傳進輸入並返回輸出。純函數對於給定的的輸入總是返回相同的輸出。我們現在討論的函數也滿足純度要求。

map

map 函數用在 JavaScript 數組中。map 函數通過將數組的每個元素傳遞給轉換函數,並返回一個新數組。JS 數組中 map 的一般語法是:

arr.map((elem){
process(elem)
return processedValue
}) // returns new array with each element processed

假設,我們最近正在處理串行鍵中少量不需要的字符。我們需要把它們移走。我們不是通過循環和查找來移除字符,而是使用map達到相同的效果並獲得結果數組。

var data = ["2345-34r", "2e345-211", "543-67i4", "346-598"];
var re = /[a-z A-Z]/;
var cleanedData = data.map((elem) => {return elem.replace(re, "")});
console.log(cleanedData); // ["2345-34", "2345-211", "543-674", "346-598"]

小注:JavaScript ES6 使用箭頭語法來定義函數。

map 攜帶一個函數參數。而該函數自身也帶有參數。這個參數是從數組中篩選的。這個方法應用於數組中的所有元素,並返回處理過的元素。

reduce

ruduce 函數將指定的列表縮減為一個最終值。當然,通過循環數組並將結果保存在變量中也能實現相同的效果。但在這裡,同樣是將一個數組縮減成一個值,reduce 更為簡潔。JS 中 reduce 的一般語法是:

arr.reduce((accumulator,
currentValue,
currentIndex) => {
process(accumulator, currentValue)
return intermediateValue/finalValue
}, initialAccumulatorValue) // returns reduced value

accumulator 保存中間值和最終值。currentIndex, currentValue 分別是當前數組元素的索引和值。initialAccumultorValue 是傳遞給函數的初始值。

reduce 的一個實際用途是合併數組中的數組元素。合併是將內部數組元素轉換成一個簡單數組。例如:

var arr = [[1, 2], [3, 4], [5, 6]];
var flattenedArray = [1, 2, 3, 4, 5, 6];

我們也可以通過常規迭代來實現這一點。但使用 reduce 一行代碼就搞定了。神奇吧!

var flattenedArray = arr.reduce((accumulator, currentValue) => {
return accumulator.concat(currentValue);
}, []); // returns [1, 2, 3, 4, 5, 6]

filter

這是第三種函數式編程概念。filter 與 map 用法相近,因為 filter 也是處理數組中的每個元素並最終返回另一個數組(而不像 reduce 返回一個值)。篩選後的數組長度可以小於或等於原始數組。因為相對於輸出數組,傳入的篩選條件不可能是極少/0。JS filter 的一般語法是:

arr.filter((elem) => {
return true/false
})

這裡的 elem 是數組的數據元素,而函數返回的 true/false 將表示包含/不包含被過濾元素。常見的例子是根據給定的開頭和結尾條件篩選單詞數組。假設要篩選一個以 t 開頭且以 r 結尾的單詞數組。

var words = ["tiger", "toast", "boat", "tumor", "track", "bridge"]
var newData = words.filter((elem) => {
return elem.startsWith('t') && elem.endsWith('r') ? true:false;
}); // returns ["tiger", "tumor"]

當有人問及 JavaScript 函數式編程方面的問題,這三個函數應該能脫口而出。如你所見,這三種用法既保證了函數的純度,又不改變原始數組。


理解錯誤處理模式


這是許多開發者最不關心的 JavaScript 內容。屈指可數的開發者會討論錯誤處理問題。一個好的開發方法就是,嚴謹的將 JS 代碼封裝在 try/catch 代碼塊中。

雅虎的 UI 工程師 Nicholas C.Zakas 早在 2008 年就說過“要時常假設代碼出錯,假設事件無法正常執行!並在服務器拋出報錯信息。”

在 JavaScript 中,只要編碼過程稍不留神,就可能出錯。例如:

$("button").click(function(){
$.ajax({url: "user.json", success: function(result){
updateUI(result["posts"]);
}});
});

在這裡,我們掉進了默認結果總是 JSON 對象的陷阱。這樣可能導致服務器崩潰並返回一個 null,而不是返回正確結果。在這種情況下,null 的[“posts”]將會拋出一個錯誤。正確的處理方法應該是這樣!

$("button").click(function(){
$.ajax({url: "user.json", success: function(result){
try {
updateUI(result["posts"]);
}
catch(e) {
// Custom functions
logError();
flashInfoMessage();
}
}});
});

logError 函數的作用是向服務器返回報錯信息。第二個函數flashInfoMessage 是為了展示像“服務器當前不可用”之類的用戶友好提示。

Nicholas 認為,當感覺會發生意料之外的事情時,就要手動拋出錯誤。還需區分致命錯誤和非致命錯誤。上面的錯誤與後端服務器宕機有關,屬於致命錯誤。這種情況下,應該告知顧客由於某種原因服務暫停了。在某些情況下,這可能又不是致命的,但最好給服務器一個提示。為構建這樣的代碼,首先要拋出一個錯誤,用 window 對象層級的錯誤事件捕捉它,然後調用 API 將該信息打出到服務器。

reportErrorToServer = function (error) {
$.ajax({type: "POST",
url: "http://api.xyz.com/report",
data: error,
success: function (result) {}
});
}
// Window error event
window.addEventListener('error', function (e) {
reportErrorToServer({message: e.message})
})}
function mainLogic() {
// Somewhere you feel like fishy
throw new Error("user feeds are having fewer fields than expected...");
}

這段代碼主要做三件事:

  1. 監聽window層級的錯誤
  2. 一旦出現問題就調用API
  3. 打出到服務器上!

執行代碼前,可以使用新的布爾函數(ES5,ES6)檢查變量是否有效,是否為 null 或 undefined。

if (Boolean(someVariable)) {
// use variable now
} else {
throw new Error("Custom message")
}

時常考慮錯誤處理問題,不依賴瀏覽器而是依靠自己。可能會出錯的!


其他要點(提升, 事件冒泡)


對於 JavaScript 開發者來說,以上所有的概念都是基礎知識。但瞭解少量的內部細節是非常有用的。比如瞭解 JavaScript 在瀏覽器中的工作機制。那什麼是提升和事件冒泡呢?

提升

提升是在運行程序時將聲明的變量提升到作用域的頂部的過程。

doSomething(foo); // used before
var foo; // declared later

將以上代碼在像 Python 這樣的腳本語言中運行時,它會拋出一個錯誤。變量需要先定義才能引用。即使 JS 是一種腳本語言,它也有提升機制。在這機制中,JavaScript VM 在運行程序時會做這兩件事:

  1. 首先掃描程序,收集所有的變量和函數聲明,併為其分配內存空間。
  2. 通過給指定的變量填充值來運行程序,如果沒有指定值,則填充undefined。

在上面的代碼片段中,控制檯日誌會輸出“undefined”。這是因為先收集了變量 foo。VM 再給變量 foo 尋找有無與之對應的賦值。這種提升會導致許多JavaScript 場景,一些代碼會在某些地方拋出錯誤,另一些則不知不覺引用了 undefined。你需要了解提升以消除這些模糊場景。

事件冒泡

現在來看看事件冒泡!根據高級軟件工程師 Arun P 的說法:

“事件的冒泡和捕獲是HTML DOM API事件傳播的兩種形式,當事件發生在一個元素內的另一個元素中,並且兩個元素都執行了該事件。事件的傳播模式決定元素接收事件的順序。”

通過冒泡,事件首先在最內層元素捕獲和處理,接著傳播到外層元素。而捕獲則相反。我們通常使用 addEventListener 函數來監聽事件的執行。

addEventListener("click", handler, useCapture=false)

第三個參數 useCapture 是關鍵。它的默認值是 false。因此,這是一個冒泡模型,事件從最內層元素開始執行,然後向外傳播直到到達父級元素。如果這個參數為 true,那麼它就是捕獲模型。

例如:冒泡模型







當點擊 li 元素,程序的執行順序與冒泡模型(默認情況)類似。

handler() => ulHandler() => divHandler()


如何在 JavaScript 面試中過五關斬六將?


如上圖所示,程序按順序依次向外觸發。類似地,捕獲模型則按順序依次向內觸發,即從父元素向內直到被點擊的元素。現在修改上面代碼中的這一行。

document.getElementById("foo").addEventListener("click", handler, true)

程序的執行順序將是:

divHandler => ulHandler() => handler()


如何在 JavaScript 面試中過五關斬六將?


我們應該正確理解事件冒泡(觸發方向是向內還是向外),這有助於實現用戶界面(UI)而避免任何不必要的行為。

以上就是 JavaScript 的基本概念。正如我一開始說的,除了掌握這些概念,工作經驗、知識和充分的準備都有助於破解 JavaScript 面試。請做好終身學習的準備。留意最新的技術進展(ES6)。深入瞭解 JavaScript 的方方面面,比如 V6 引擎,測試等。這裡有一些視頻資源供大家學習。最後,如果沒有掌握數據結構和算法,任何面試都是不會成功的。Oleksii Trekhleb 策劃了一個非常棒的 git repo 項目,他用 JS 編寫了關於面試準備的算法。瞭解下吧(https://github.com/trekhleb/javascript-algorithms)。


分享到:


相關文章: