3個經常被問到的 JavaScript 面試題

weixin_34253539發表於2017-03-03

原文連結

問題 #1: 事件委託

事件委託,也叫事件委派,事件代理。

當構建應用程式時,有時需要將事件監聽器繫結到頁面上的某些元素上,以便在使用者與元素互動時執行某些操作。

假設我們現在有一個無序列表:

<ul id="todo-app">
  <li class="item">Walk the dog</li>
  <li class="item">Pay bills</li>
  <li class="item">Make dinner</li>
  <li class="item">Code for one hour</li>
</ul>

我們需要在<li>上繫結點選事件,我們可能會這樣操作:

app = document.getElementById('todo-app');
let items = app.getElementsByClassName('item');

// 將事件偵聽器繫結到每個列表項
for (let item of items) {
  item.addEventListener('click', function() {
    alert('you clicked on item: ' + item.innerHTML);
  });
}

雖然這樣可以實現功能,但問題是要單獨將事件偵聽器繫結到每個列表項。這是4個元素,沒什麼大問題,但如果列表中有10,000個事項,怎麼辦?這個函式將會建立10,000個獨立的事件監聽器,並將每個事件監聽器繫結到 DOM 。這樣程式碼執行的效率非常低下

更高效的解決方案是將一個事件偵聽器實際繫結到父容器<ul>上,然後在實際單擊時可以訪問每個確切元素。這被稱為事件委託,並且它比每個元素單獨繫結事件的處理程式更高效。

那麼上面的程式碼可以改變為:

let app = document.getElementById('todo-app');
  
// 事件偵聽器繫結到整個容器上
app.addEventListener('click', function(e) {
  if (e.target && e.target.nodeName === 'LI') {
    let item = e.target;
    alert('you clicked on item: ' + item.innerHTML);
  }
});

問題 #2: 在迴圈內使用閉包(Closures)

閉包的本質是一個內部函式訪問其作用域之外的變數。閉包可以用於實現諸如 私有變數 和 建立工廠函式之類的東西。

在面試中我們可能會見到一段這樣的程式碼:

for (var i = 0; i < 4; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

執行上面的程式碼控制檯會在1秒後列印4個4,而不是0,1,2,3。

其原因是因為setTimeout函式建立了一個可以訪問其外部作用域的函式(也就是我們經常說的閉包),每個迴圈都包含了索引i

1秒後,該函式被執行並且列印出i的值,其在迴圈結束時為4,因為它的迴圈週期經歷了0,1,2,3,4,並且迴圈最終在4時停止。

下面列舉兩種方案解決這個問題:

for (var i = 0; i < 4; i++) {
  // 通過傳遞變數 i
  // 在每個函式中都可以獲取到正確的索引
  setTimeout(function(j) {
    return function() {
      console.log(j);
    }
  }(i), 1000);
}
for (let i = 0; i < 4; i++) {
  // 使用ES6的let語法,它會建立一個新的繫結
  // 每個方法都是被單獨呼叫的
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

問題 #3: 函式防抖(Debouncing)

有一些瀏覽器事件可以在很短的時間內快速啟動多次,例如頁面滾動事件。如果將事件偵聽器繫結到視窗滾動事件上,並且使用者快速滾動頁面,事件很可能會在短時間多次觸發。這可能會導致一些嚴重的效能問題。

因此,在偵聽滾動,視窗調整大小,或鍵盤按下的事件時,請務必使用函式防抖動(Debouncing)函式節流(Throttling)來提升頁面速度和效能。

函式防抖(Debouncing)是解決這個問題的一種方式,通過限制需要經過的時間,直到再次呼叫函式。一個實現函式防抖的方法是:把多個函式放在一個函式裡呼叫,隔一定時間執行一次。

這裡有一個使用原生JavaScript實現的例子,用到了作用域、閉包、this和定時事件:

function debounce(fn, delay) {
  // 持久化一個定時器 timer
  let timer = null;
  // 閉包函式可以訪問 timer
  return function() {
    // 通過 'this' 和 'arguments' 獲得函式的作用域和引數
    let self = this;
    let args = arguments;
    // 如果事件被觸發,清除 timer 並重新開始計時
    clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(self, args);
    }, delay);
  }
}

// 當使用者滾動時呼叫函式foo()
function foo() {
  console.log('You are scrolling!');
} 

// 在事件觸發的兩秒後,包裹在debounce()中的函式才會被觸發
window.addEventListener('scroll', debounce(foo, 2000));

函式節流是另一個類似函式防抖的技巧,除了使用等待一段時間再呼叫函式的方法,函式節流還限制固定時間內只能呼叫一次。所以,如果一個事件在100毫秒內發生10次,函式節流會每2秒呼叫一次函式,而不是100毫秒內全部呼叫。

(完)

相關文章