前端JS面試中經(jīng)常會(huì)被問(wèn)到的幾個(gè)問(wèn)題
本文不是討論新的 JavaScript 庫(kù)、常見(jiàn)的開(kāi)發(fā)實(shí)踐或任何新的 ES6 函數(shù),只是聊聊在面試中出現(xiàn)頻率比較高的的幾道面試題。
問(wèn)題1、事件的節(jié)流(throttle)與防抖(debounce)
有些瀏覽器事件可以在短時(shí)間內(nèi)快速觸發(fā)多次,比如調(diào)整窗口大小或向下滾動(dòng)頁(yè)面。例如,監(jiān)聽(tīng)頁(yè)面窗口滾動(dòng)事件,并且用戶持續(xù)快速地向下滾動(dòng)頁(yè)面,那么滾動(dòng)事件可能在 3 秒內(nèi)觸發(fā)數(shù)千次,這可能會(huì)導(dǎo)致一些嚴(yán)重的性能問(wèn)題。那我們應(yīng)該如何去避免這樣的問(wèn)題,下面有兩種方法:
1、節(jié)流(throttle)
節(jié)流的主要思想在于:在某段時(shí)間內(nèi),不管你觸發(fā)了多少次回調(diào),都只認(rèn)第一次,并在計(jì)時(shí)結(jié)束時(shí)給予響應(yīng)。
代碼示例:
- // fn是我們需要包裝的事件回調(diào), interval是時(shí)間間隔的閾值
- function throttle(fn, interval) {
- // last為上一次觸發(fā)回調(diào)的時(shí)間
- let last = 0
- // 將throttle處理結(jié)果當(dāng)作函數(shù)返回
- return function () {
- // 保留調(diào)用時(shí)的this上下文
- let context = this
- // 保留調(diào)用時(shí)傳入的參數(shù)
- let args = arguments
- // 記錄本次觸發(fā)回調(diào)的時(shí)間
- let now = +new Date()
- // 判斷上次觸發(fā)的時(shí)間和本次觸發(fā)的時(shí)間差是否小于時(shí)間間隔的閾值
- if (now - last >= interval) {
- // 如果時(shí)間間隔大于我們?cè)O(shè)定的時(shí)間間隔閾值,則執(zhí)行回調(diào)
- last = now;
- fn.apply(context, args);
- }
- }
- }
- // 用throttle來(lái)包裝scroll的回調(diào)
- const better_scroll = throttle(() => console.log('觸發(fā)了滾動(dòng)事件'), 1000)
- document.addEventListener('scroll', better_scroll)
2、防抖(Debounce)
防抖的主要思想在于:我會(huì)等你到底。在某段時(shí)間內(nèi),不管你觸發(fā)了多少次回調(diào),我都只認(rèn)最后一次。
代碼示例:
- // fn是我們需要包裝的事件回調(diào), delay是每次推遲執(zhí)行的等待時(shí)間
- function debounce(fn, delay) {
- // 定時(shí)器
- let timer = null
- // 將debounce處理結(jié)果當(dāng)作函數(shù)返回
- return function () {
- // 保留調(diào)用時(shí)的this上下文
- let context = this
- // 保留調(diào)用時(shí)傳入的參數(shù)
- let args = arguments
- // 每次事件被觸發(fā)時(shí),都去清除之前的舊定時(shí)器
- if(timer) {
- clearTimeout(timer)
- }
- // 設(shè)立新定時(shí)器
- timer = setTimeout(function () {
- fn.apply(context, args)
- }, delay)
- }
- }
- // 用debounce來(lái)包裝scroll的回調(diào)
- const better_scroll = debounce(() => console.log('觸發(fā)了滾動(dòng)事件'), 1000)
- document.addEventListener('scroll', better_scroll)
3、節(jié)流和防抖的實(shí)際應(yīng)用
在我們的應(yīng)用中,單一的應(yīng)用節(jié)流或者防抖都不是一個(gè)好主意。
場(chǎng)景:如果用戶的操作十分頻繁——他每次都不等 debounce 設(shè)置的 delay 時(shí)間結(jié)束就進(jìn)行下一次操作,于是每次 debounce 都為該用戶重新生成定時(shí)器,回調(diào)函數(shù)被延遲了不計(jì)其數(shù)次。頻繁的延遲會(huì)導(dǎo)致用戶遲遲得不到響應(yīng),用戶同樣會(huì)產(chǎn)生“這個(gè)頁(yè)面卡死了”的觀感。
為了避免弄巧成拙,我們需要借力 throttle 的思想,打造一個(gè)“有底線”的 debounce——等你可以,但我有我的原則:delay 時(shí)間內(nèi),我可以為你重新生成定時(shí)器;但只要delay的時(shí)間到了,我必須要給用戶一個(gè)響應(yīng)。這個(gè)節(jié)流與防抖結(jié)合的思路。
代碼示例:
- // fn是我們需要包裝的事件回調(diào), delay是時(shí)間間隔的閾值
- function throttle(fn, delay) {
- // last為上一次觸發(fā)回調(diào)的時(shí)間, timer是定時(shí)器
- let last = 0, timer = null
- // 將throttle處理結(jié)果當(dāng)作函數(shù)返回
- return function () {
- // 保留調(diào)用時(shí)的this上下文
- let context = this
- // 保留調(diào)用時(shí)傳入的參數(shù)
- let args = arguments
- // 記錄本次觸發(fā)回調(diào)的時(shí)間
- let now = +new Date()
- // 判斷上次觸發(fā)的時(shí)間和本次觸發(fā)的時(shí)間差是否小于時(shí)間間隔的閾值
- if (now - last < delay) {
- // 如果時(shí)間間隔小于我們?cè)O(shè)定的時(shí)間間隔閾值,則為本次觸發(fā)操作設(shè)立一個(gè)新的定時(shí)器
- clearTimeout(timer)
- timer = setTimeout(function () {
- last = now
- fn.apply(context, args)
- }, delay)
- } else {
- // 如果時(shí)間間隔超出了我們?cè)O(shè)定的時(shí)間間隔閾值,那就不等了,無(wú)論如何要反饋給用戶一次響應(yīng)
- last = now
- fn.apply(context, args)
- }
- }
- }
- // 用新的throttle包裝scroll的回調(diào)
- const better_scroll = throttle(() => console.log('觸發(fā)了滾動(dòng)事件'), 1000)
- document.addEventListener('scroll', better_scroll)
問(wèn)題2、事件委托代理
傳統(tǒng)的事件綁定方式:
- <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 itimes = app.getElementsByClassName('item');
- for (let item of items) {
- item.addEventListener('click', function(){
- alert('you clicked on item: ' + item.innerHTML);
- })
- }
- })
雖然這在技術(shù)上是沒(méi)什么問(wèn)題的,但問(wèn)題是要將事件分別綁定到每個(gè)項(xiàng)。這對(duì)于目前 4 個(gè)元素來(lái)說(shuō),沒(méi)什么大問(wèn)題,但是如果在待辦事項(xiàng)列表中添加了 10,000 項(xiàng)(他們可能有很多事情要做)怎么辦?然后,函數(shù)將創(chuàng)建 10,000 個(gè)獨(dú)立的事件偵聽(tīng)器,并將每個(gè)事件監(jiān)聽(tīng)器綁定到 DOM ,這樣代碼執(zhí)行的效率非常低下。如何高效的進(jìn)行事件綁定:
基于事件委托的代碼示例:
- document.addEventListener('DOMContentLoaded', function() {
- 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)
- }
- })
- })
問(wèn)題3、閉包的使用
閉包常常出現(xiàn)在面試中,以便面試官衡量你對(duì) JS 的熟悉程度,以及你是否知道何時(shí)使用閉包。
閉包基本上是內(nèi)部函數(shù)可以訪問(wèn)其范圍之外的變量。 閉包可用于實(shí)現(xiàn)隱藏變量和創(chuàng)建函數(shù)工廠。
不正確的閉包應(yīng)用:
- 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);
- }
如果運(yùn)行上面代碼,3 秒延遲后你會(huì)看到,實(shí)際上每次打印輸出是 4,而不是期望的 0,1,2,3 。
原因是因?yàn)?setTimeout 函數(shù)創(chuàng)建了一個(gè)可以訪問(wèn)其外部作用域的函數(shù)(閉包),該作用域是包含索引 i 的循環(huán)。 經(jīng)過(guò) 3 秒后,執(zhí)行該函數(shù)并打印出 i 的值,該值在循環(huán)結(jié)束時(shí)為 4,因?yàn)樗h(huán)經(jīng)過(guò)0,1,2,3,4并且循環(huán)最終停止在 4。
正確的閉包使用:
- //方法一:
- const arr = [10, 12, 15, 21];
- for (var i = 0; i < arr.length; i++) {
- 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++) {
- setTimeout(function() {
- console.log('The index of this number is: ' + i);
- }, 3000);
- }