簡評:在這篇文章中,作者總結了 3 個在 JavaScript 面試問題中問得最多的問題(不清楚國內是不是)。這三個問題不是關于任何庫的用法或 ES6 的新功能,而主要是對原生 JavaScript 的理解。
Question #1: 事件代理
當創建一個應用時,不可避免的會遇到監聽事件觸發的需求。這里有一個小的簡單的待辦列表要完成,想要在用戶點擊其中一個列表項時觸發一個動作。下面是一段 HTML 代碼:
<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>
你可能會想像下面這樣來寫:
document.addEventListener('DOMContentLoaded', function() {
let app = document.getElementById('todo-app');
let items = app.getElementsByClassName('item');
// attach event listener to each item
for (let item of items) {
item.addEventListener('click', function() {
alert('you clicked on item: ' + item.innerHTML);
});
}
});
當然這樣寫在技術上是完全可以的,唯一的問題就是當列表項過多的時候(比如 10,000 個),你的這段函數就將會同時創建 10,000 個監聽函數。
下面是一種更有效率的寫法:
document.addEventListener('DOMContentLoaded', function() {
let app = document.getElementById('todo-app');
// attach event listener to whole container
app.addEventListener('click', function(e) {
if (e.target && e.target.nodeName === 'LI') {
let item = e.target;
alert('you clicked on item: ' + item.innerHTML);
}
});
});
Question #2: 在循環中使用閉包
閉包是 JavaScript 的一個重要特性,開發者可以用來模擬私有方法。在這里有個簡單的問題:
實現一個函數,循環遍歷整數列表,并在 3 秒后打印每個元素的索引。
一個常見的錯誤實現:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('The index of this number is: ' + i);
}, 3000);
}
如果你執行這段代碼,會發現每次輸出的是 4 而不是按順序的 0,1,2,3。
原因在于 setTimeout 創建了一個匿名函數并訪問處于外部的變量 i,都處于同一環境中。當 console.log 被調用的時候,匿名函數保持對外部變量 i 的引用,此時 for 循環已經結束, i 的值被修改成了 4。為了得到想要的結果,需要在每次循環中創建變量 i 的拷貝。
事實上正確的寫法有好幾種,這里列舉用得最多的兩種:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
// pass in the variable i so that each function
// has access to the correct index
setTimeout(function(i_local) {
return function() {
console.log('The index of this number is: ' + i_local);
}
}(i), 3000);
}
const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
// using the ES6 let syntax, it creates a new binding
// every single time the function is called
// read more here: http://exploringjs.com/es6/ch_variables.html#sec_let-const-loop-heads
setTimeout(function() {
console.log('The index of this number is: ' + i);
}, 3000);
}
Question #3: Debouncing
有一些瀏覽器事件可以在很短的時間內快速啟動多次,例如調整窗口大小或滾動頁面。如果你在窗口滾動上綁定了事件,那么可能在用戶滾動頁面的幾秒鐘里,你的事件方法就執行了數千次,這就會導致很嚴重的性能問題。
一個真實的案例就是 2011 年 Twitter,在你滾動 Twitter feed 時,其會變得非常慢甚至未響應。這里有一篇 blog 就詳細講了當時的這個 bug,也就是在 scroll 事件上綁定一個復雜函數是多糟的主意。
Debouncing 就是解決這個問題的一種方法,簡單來說就是限制函數調用的間隔時間。如果在時間間隔內再次觸發事件,就重啟定時器并忽略掉這次事件。
// debounce function that will wrap our event
function debounce(fn, delay) {
// maintain a timer
let timer = null;
// closure function that has access to timer
return function() {
// get the scope and parameters of the function
// via 'this' and 'arguments'
let context = this;
let args = arguments;
// if event is called, clear the timer and start over
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
}
使用:
// function to be called when user scrolls
function foo() {
console.log('You are scrolling!');
}
// wrap our function in a debounce to fire once 2 seconds have gone by
let elem = document.getElementById('container');
elem.addEventListener('scroll', debounce(foo, 2000));
與 Debouncing 類似的技術是 Throttling,同樣也是使用計時器來控制事件的觸發,不同之處在于 Throttling 沒有忽略掉事件,而是延遲觸發。如果想了解更多,可以進一步閱讀下面的這幾篇文章。
擴展閱讀:
- Throttling and Debouncing in JavaScript
- The Difference Between Throttling and Debouncing
- Examples of Throttling and Debouncing
- Remy Sharp’s blog post on Throttling function calls
原文:3 JavaScript questions to watch out for during coding interviews
歡迎關注知乎專欄「極光日報」,每天為 Makers 導讀三篇優質英文文章。