JavaScript面試的完美指南(開發者視角)

Fundebug發表於2019-02-21

摘要: 面試季手冊。

Fundebug經授權轉載,版權歸原作者所有。

為了說明 JS 面試的複雜性,首先,請嘗試給出以下結果:

onsole.log(2.0 == “2” == new Boolean(true) == “1”)
複製程式碼

十有八九的會給出false, 其實執行結果是true,原因請看 這裡

1) 理解 JS 函式

函式是 JavaScript 的精華,是 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
複製程式碼

在 JavaScript 中,如果將函式定義為變數,變數名將被提升,是 JS 執行到它的定義才能被訪問。

你可能在一些程式碼中頻繁的見到如下程式碼。

var simpleLibrary = function() {
   var simpleLibrary = {
        a,
        b,
        add: function(a, b) {
            return a + b;
        },
        subtract: function(a, b) {
            return a - b;   
        }
   }
  return simpleLibrary;
}();
複製程式碼

為什麼會做這種奇怪的事情? 這是因為一個函式變數中變數和函式被分裝,可以避免全域性變數汙染。 JQueryLodash 的庫採用這種技術提供 $_

2) 理解 bind、apply 和 call

你可能在所有常用庫中看到過這三個函式。它們允許區域性套用, 我們可以把功能組合到不同的函式。一個優秀的js開發者可以隨時告訴你關於這三個函式。

基本上,這些是改變行為以實現某些功能的原型方法,根據 JS 開發人員 Chad 的說法,用法如下:

希望使用某個上下文呼叫該函式,請使用 .bind() ,這在事件中很有用。 如果要立即呼叫函式,請使用.call().apply(),並修改上下文。

舉例說明

讓我們看看上面的陳述是什麼意思! 假設你的數學老師要求你建立一個庫並提交。你寫了一個抽象的庫,它可以求出圓的面積和周長:

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 精確到小數點後 5 位數而你只精確到 2 位, 現在由於最後期限已過你沒有機會提交庫。 這裡 JS的 call 函式可以幫你, 只需要呼叫你的程式碼如下:

mathLib.area.call({pi: 3.1.159}, 2)
複製程式碼

它會動態地獲取新的 pi 值,結果如下:

12.56636
複製程式碼

這時,注意到 call 函式具有兩個引數:

  • Context
  • 函式引數

area 函式中, 上下文是物件被關鍵詞 this 代替,後面的引數作為函式引數被傳遞。 如下:

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

複製程式碼

呼叫方式如下:

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 注入到指定的函式上,改變 this 的指向, 使用 bind 時,函式不會像 callapply 立即執行。

var newVolume = cylinder.volume.bind({pi: 3.14159});
newVolume(2,6); // Now pi is 3.14159
複製程式碼

bind 用途是什麼?它允許我們將上下文注入一個函式,該函式返回一個具有更新上下文的新函式。這意味著這個變數將是使用者提供的變數,這在處理 JavaScript 事件時非常有用。

3) 理解 js 作用域(閉包)

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); // 列印 "12.56636" 不是 "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,因為 if 已執行。但 是ES6 銷燬了塊級變數的作用域,作用域進入全域性。

現在來到神奇的作用域,可以使用閉包來實現,JavaScript 閉包是一個返回另一個函式的函式。

如果有人問你這個問題,編寫一個輸入一個字串並逐次返回字元。 如果給出了新字串,則應該替換舊字串,類似簡單的一個生成器。

function generator(input) {
  var index = 0;
  return {
    next: function() {
      if (index < input.lenght) {
        return input[index -1];
      }
      return "";
    }
  }
}
複製程式碼

執行如下:

var mygenerator = generator("boomerang");
mygenerator.next(); // returns "b"
mygenerator.next() // returns "o"
mygenerator = generator("toon");
mygenerator.next(); // returns "t"
複製程式碼

在這裡,作用域扮演著重要的角色。閉包是返回另一個函式並攜帶資料的函式。上面的字串生成器適用於閉包。index 在多個函式呼叫之間保留,定義的內部函式可以訪問在父函式中定義的變數。這是一個不同的作用域。如果在第二級函式中再定義一個函式,它可以訪問所有父級變數。

4) 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); // 列印 {}?
}
複製程式碼

因為這是一個全域性物件,記住,無論父作用域是什麼,它都將由子作用域繼承。列印出來是 window 物件。上面討論的三個方法實際上用於設定這個物件。

現在,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 實際上是指物件本身。正如我們前面提到的,它在不同的地方有不同的表現。

5) 理解物件 (Object.freeze, Object.seal)

通常物件的格式如下:

var marks = {physics: 98, maths:95, chemistry: 91};
複製程式碼

它是一個儲存鍵、值對的對映。 javascript 物件有一個特殊的屬性,可以將任何東西儲存為一個值。這意味著我們可以將一個列表、另一個物件、一個函式等儲存為一個值。

可以用如下方式來建立物件:

var marks = {};
var marks = new Object();
複製程式碼

可以使用 JSON.stringify() 將一個物件轉製成字串,也可以用 JSON.parse 在將其轉成物件。

// 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 highScere = 0;

for (i of Object.keys(marks)) {
  if (marks[i] > highScore)
    highScore = marks[i];
}
複製程式碼

Object.values 以陣列的方式返回物件的值。

物件上的其他重要函式有:

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

Object.prototype 上提供了許多應用上相關的函式,如下:

Object.prototype.hasOwnProperty 用於檢查給定的屬性/鍵是否存在於物件中。

marks.hasOwnProperty("physics"); // returns true
marks.hasOwnProperty("greek"); // returns false
複製程式碼

Object.prototype.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 來判斷,給定物件是否被凍結:

Object.isFrozen(finalizedMarks); // returns true
複製程式碼

Object.sealObject.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 判斷物件是否被密封。

Object.isSealed(marks); // returns true
複製程式碼

在全域性物件函式上還有許多其他重要的函式/方法,在這裡找到他們。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

6) 理解原型繼承

在傳統 JavaScript 中,有一種偽裝的繼承概念,它是通過使用原型技術來實現的。在ES5、ES6中看到使用 new 的語法只是底層原型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 屬性。是的,它打算由繼承了上述類的子類傳遞。

JavaScript中, 如下實現繼承:

function Dog(name, type) {
Animal.call(this, name, type);
this.sound = 'bow';
}
複製程式碼

我定義了一個更具體的函式,叫做 Dog。在這裡,為了繼承 Animal 類,我需要call傳遞this和其他引數。使用如下方式來例項化一隻德國牧羊犬

var pet = Dog("德國牧羊犬", animalGroups.MAMMAL);
console.log(pet); // returns Dog {name: "德國牧羊犬", type: 1, sound: "bow"}
複製程式碼

我們沒有在子函式中分配 nametype 屬性,我們呼叫的是超級函式 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(); // 德國牧羊犬 bowing...
複製程式碼

現在可以使用 shout 方法。 我們可以使用 object.constructor 函式檢查 JavaScript 中給定物件的類 來看看 pet 是什麼類:

pet.constructor; // returns Animal
複製程式碼

這是模糊的,Animal 是一個父類。但是 pet 到底是什麼型別的呢? pet 應該是 Dog 的型別。之所以是 Animal 型別,是因為 Dog 類的建構函式:

Dog.prototype.constructor; // returns Animal
複製程式碼

它是 Animal 型別的。我們應該將它設定為 Dog 本身,這樣類的所有例項(物件)才能給出正確的類名。

Dog.prototype.constructor = Dog;
複製程式碼

關於原型繼承, 我們應該記住以下幾條:

  • 類屬性使用 this 繫結
  • 類方法使用 prototype 物件來繫結
  • 為了繼承屬性, 使用 call 函式來傳遞 this
  • 為了繼承方法, 使用 Object.create 連線父和子的原型
  • 始終將子類建構函式設定為自身,以獲得其物件的正確型別

7)理解 callback 和 promise

回撥是在 I/O 操作完成後執行的函式。一個耗時的I/O操作會阻塞程式碼, 因此在Python/Ruby不被允許。但是在 JavaScript中,由於允許非同步執行,我們可以提供對非同步函式的回撥。這個例子是由瀏覽器到伺服器的AJAX(XMLHettpRequest)呼叫,由滑鼠、鍵盤事件生成。如下:

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 請求時將執行該回撥函式。

Promise 是回撥函式的優雅的封裝, 使得我們優雅的實現非同步程式碼。在以下給出的這篇文章中討論了很多 promise,這也是在 JS 中應該知道的重要部分。

Writing neat asynchronous Node JS code with Promises

8)理解正則表達

正規表示式有許多應用地方,處理文字、對使用者輸入執行規則等。JavaScript 開發人員應該知道如何執行基本正規表示式並解決問題。Regex 是一個通用概念,來看看如何從 JS 中做到這一點。

建立正規表示式,有如下兩種方式:

var re = /ar/;
var re = new RegExp('ar'); 
複製程式碼

上面的正規表示式是與給定字串集匹配的表示式。定義正規表示式之後,我們可以嘗試匹配並檢視匹配的字串。可以使用 exec 函式匹配字串:

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

有一些特殊的字元類允許我們編寫複雜的正規表示式。RegEx 中有許多型別的元素,其中一些如下:

  • 字元正則:\w-字母數字, \d- 數字, \D- 沒有數字
  • 字元類正則:[x-y] x-y區間, [^x] 沒有x
  • 數量正則:+ 至少一個、? 沒或多個、* 多個
  • 邊界正則,^ 開始、$ 結尾

例子如下:

/* 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 = /<([\w]+).*>(.*?)<\/\1>/;
re8.exec('<p>Hello JS developer</p>'); //returns  ["<p>Hello JS developer</p>", "p", "Hello JS developer", index: 0, input: "<p>Hello JS developer</p>"]
複製程式碼

有關 regex 的詳細資訊,可以看 這裡

除了 exec 之外,還有其他函式,即 matchsearchreplace,可以使用正規表示式在另一個字串中查詢字串,但是這些函式在字串本身上使用。

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

Regex 是一個重要的主題,開發人員應該理解它,以便輕鬆解決複雜的問題。

9)理解 map、reduce 和 filter

函數語言程式設計是當今的一個熱門討論話題。許多程式語言都在新版本中包含了函式概念,比如 lambdas(例如:Java >7)。在 JavaScrip t中,函數語言程式設計結構的支援已經存在很長時間了。我們需要深入學習三個主要函式。數學函式接受一些輸入和返回輸出。純函式都是給定的輸入返回相同的輸出。我們現在討論的函式也滿足純度。

map

map 函式在 JavaScript 陣列中可用,使用這個函式,我們可以通過對陣列中的每個元素應用一個轉換函式來獲得一個新的陣列。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"]
複製程式碼

map 接受一個作為引數的函式, 此函式接受一個來自陣列的引數。我們需要返回一個處理過的元素, 並應用於陣列中的所有元素。

reduce

reduce 函式將一個給定的列表整理成一個最終的結果。通過迭代陣列執行相同的操作, 並儲存中間結果到一個變數中。這裡是一個更簡潔的方式進行處理。js 的 reduce 一般使用語法如下:

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

accumulator 儲存中間值和最終值。currentIndexcurrentValue分別是陣列中元素的 index 和 value。initialAccumulatorValueaccumulator 初始值。

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

filtermap 更為接近, 對陣列的每個元素進行操作並返回另外一個陣列(不同於 reduce 返回的值)。過濾後的陣列可能比原陣列長度更短,因為通過過濾條件,排除了一些我們不需要的。

filter 語法如下:

arr.filter((elem) => {
   return true/false
})
複製程式碼

elem 是陣列中的元素, 通過 true/false 表示過濾元素儲存/排除。假設, 我們過濾出以 t 開始以 r 結束的元素:

var words = ["tiger", "toast", "boat", "tumor", "track", "bridge"]
var newData = words.filter((str) => {
    return str.startsWith('t') && str.endsWith('r');
})
newData // (2) ["tiger", "tumor"]
複製程式碼

當有人問起JavaScript的函式程式設計方面時,這三個函式應該信手拈來。 如你所見,原始陣列在所有三種情況下都沒有改變,這證明了這些函式的純度。

10) 理解錯誤處理模式

這是許多開發人員最不關心的 JavaScript。 我看到很少有開發人員談論錯誤處理, 一個好的開發方法總是謹慎地將 JS 程式碼封裝裝在 try/catch 塊周圍。

在 JavaScript中,只要我們隨意編寫程式碼,就可能會失敗,如果所示:

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

這裡,我們陷入了一個陷阱,我們說 result 總是 JSON 物件。但有時伺服器會崩潰,返回的是 null 而不是 result。在這種情況下,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...");
}
複製程式碼

這段程式碼主要做三件事:

  • 監聽window層級錯誤
  • 無論何時發生錯誤,都要呼叫 API
  • 在伺服器中記錄

你也可以使用新的 Boolean 函式(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 在執行程式時做兩件事:

  • 首先掃描程式,收集所有的變數和函式宣告,併為其分配記憶體空間
  • 通過填充分配的變數來執行程式, 沒有分配則填充 undefined

在上面的程式碼片段中,console.log 列印 “undefined”。 這是因為在第一次傳遞變數 foo 被收集。 JS 虛擬機器 查詢為變數 foo 定義的任何值。 這種提升可能導致許多JavaScript 在某些地方丟擲錯誤,和另外地方使用 undefined

學習一些 例子 來搞清楚提升。

事件冒泡

現在事件開始冒泡了! 根據高階軟體工程師 Arun P的說法:

“當事件發生在另一個元素內的元素中時,事件冒泡和捕獲是 HTML DOM API 中事件傳播的兩種方式,並且這兩個元素都已為該事件註冊了處理程式,事件傳播模式確定元素接收事件的順序。“

通過冒泡,事件首先由最內部的元素捕獲和處理,然後傳播到外部元素。對於捕獲,過程是相反的。我們通常使用addEventListener 函式將事件附加到處理程式。

addEventListener("click", handler, useCapture=false)
複製程式碼

useCapture 是第三個引數的關鍵詞, 預設為 false。因此, 冒泡模式是事件由底部向上傳遞。 反之, 這是捕獲模式。

冒泡模式:

<div onClick="divHandler()">
    <ul onClick="ulHandler">
        <li id="foo"></li>
    </ul>
</div>
<script>
function handler() {
 // do something here
}
function divHandler(){}
function ulHandler(){}
document.getElementById("foo").addEventListener("click", handler)
</script>
複製程式碼

點選li元素, 事件順序:

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

JavaScript面試的完美指南(開發者視角)

在圖中,處理程式按順序向外觸發。類似地,捕獲模型試圖將事件從父元素向內觸發到單擊的元素。現在更改上面程式碼中的這一行。

document.getElementById("foo").addEventListener("click", handler, true)
複製程式碼

事件順序:

divHandler => ulHandler() => handler()
複製程式碼

JavaScript面試的完美指南(開發者視角)

你應該正確地理解事件冒泡(無論方向是指向父節點還是子節點),以實現使用者介面(UI),以避免任何不需要的行為。

這些是 JavaScrip t中的基本概念。正如我最初提到的,除了工作經驗和知識之外,準備有助理於你通過 JavaScript 面試。始終保持學習。留意最新的發展(第六章)。深入瞭解JavaScript的各個方面,如 V6 引擎、測試等。最後,沒有掌握資料結構和演算法的面試是不成功的。Oleksii Trekhleb 策劃了一個很棒的 git repo,它包含了所有使用 JS 程式碼的面試準備演算法。

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用

JavaScript面試的完美指南(開發者視角)

相關文章