Javascript 面試中常常被問到的三個問題!

本文不是討論最新的 JavaScript 庫、常見的開發實踐或任何新的 ES6 函數。相反,在討論 JavaScript 時,面試中一般會提到三件事。我本身也被問到這些問題,個人朋友們告訴我他們也被問到這些問題。javascript

然而,這些並非你在面試以前應該學習的惟一三件事 - 你能夠經過多種方式更好地爲即將到來的面試作準備 - 但面試官可能會問到下面是三個問題,來判斷你對 JavaScript 語言的理解和 DOM 的掌握程度。css

讓咱們開始吧!注意,咱們將在下面的示例中使用原生的 JavaScript,由於面試官一般但願瞭解你在沒有 jQuery 等庫的幫助下對JavaScript 和 DOM 的理解程度。前端

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!java

問題 1: 事件委託代理

在構建應用程序時,有時須要將事件綁定到頁面上的按鈕、文本或圖像,以便在用戶與元素交互時執行某些操做。node

若是咱們以一個簡單的待辦事項列表爲例,面試官可能會告訴你,當用戶點擊列表中的一個列表項時執行某些操做。他們但願你用 JavaScript 實現這個功能,假設有以下 HTML 代碼:git

<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>
複製代碼

你可能想要作以下操做來將事件綁定到元素:github

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);
    })
  }
})
複製代碼

雖然這在技術上是可行的,但問題是要將事件分別綁定到每一個項。這對於目前 4 個元素來講,沒什麼大問題,可是若是在待辦事項列表中添加了 10,000 項(他們可能有不少事情要作)怎麼辦?而後,函數將建立 10,000 個獨立的事件偵聽器,並將每一個事件監聽器綁定到 DOM ,這樣代碼執行的效率很是低下。面試

在面試中,最好先問面試官用戶能夠輸入的最大元素數量是多少。例如,若是它不超過 10,那麼上面的代碼就能夠很好地工做。可是若是用戶能夠輸入的條目數量沒有限制,那麼你應該使用一個更高效的解決方案。瀏覽器

若是你的應用程序最終可能有數百個事件偵聽器,那麼更有效的解決方案是將一個事件偵聽器實際綁定到整個容器,而後在單擊它時可以訪問每一個列表項, 這稱爲 事件委託,它比附加單獨的事件處理程序更有效。緩存

下面是事件委託的代碼:

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)
    }
  })
})
複製代碼

問題 2: 在循環中使用閉包

閉包經常出如今面試中,以便面試官衡量你對 JS 的熟悉程度,以及你是否知道什麼時候使用閉包。

閉包基本上是內部函數能夠訪問其範圍以外的變量。 閉包可用於實現隱私和建立函數工廠, 閉包常見的面試題以下:

編寫一個函數,該函數將遍歷整數列表,並在延遲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);
}
複製代碼

若是運行上面代碼,3 秒延遲後你會看到,實際上每次打印輸出是 4,而不是指望的 0,1,2,3

爲了正確理解爲何會發生這種狀況,瞭解爲何會在 JavaScript 中發生這種狀況將很是有用,這正是面試官試圖測試的內容。

緣由是由於 setTimeout 函數建立了一個能夠訪問其外部做用域的函數(閉包),該做用域是包含索引 i 的循環。 通過 3 秒後,執行該函數並打印出 i 的值,該值在循環結束時爲 4,由於它循環通過0,1,2,3,4而且循環最終中止在 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);
}
複製代碼

問題 3:事件的節流(throttle)與防抖(debounce)

有些瀏覽器事件能夠在短期內快速觸發屢次,好比調整窗口大小或向下滾動頁面。例如,監聽頁面窗口滾動事件,而且用戶持續快速地向下滾動頁面,那麼滾動事件可能在 3 秒內觸發數千次,這可能會致使一些嚴重的性能問題。

若是在面試中討論構建應用程序,出現滾動、窗口大小調整或按下鍵等事件請務必說起 防抖(Debouncing)函數節流(Throttling)來提高頁面速度和性能。這兩兄弟的本質都是以閉包的形式存在。經過對事件對應的回調函數進行包裹、以自由變量的形式緩存時間信息,最後用 setTimeout 來控制事件的觸發頻率。

####Throttle: 第一我的說了算

throttle 的主要思想在於:在某段時間內,無論你觸發了多少次回調,都只認第一次,並在計時結束時給予響應。

這個故事裏,‘裁判’ 就是咱們的節流閥, 他控制參賽者吃東西的時機, 「參賽者吃東西」就是咱們頻繁操做事件而不斷涌入的回調任務,它受 「裁判」 的控制,而計時器,就是上文提到的以自由變量形式存在的時間信息,它是 「裁判」 決定是否中止比賽的依據,最後,等待比賽結果就對應到回調函數的執行。

總結下來,所謂的「節流」,是經過在一段時間內無視後來產生的回調請求來實現的。只要 裁判宣佈比賽開始,裁判就會開啓計時器,在這段時間內,參賽者就儘管不斷的吃,誰也沒法知道最終結果。

對應到實際的交互上是同樣同樣的:每當用戶觸發了一次 scroll 事件,咱們就爲這個觸發操做開啓計時器。一段時間內,後續全部的 scroll 事件都會被看成「參賽者吃東西——它們沒法觸發新的 scroll 回調。直到「一段時間」到了,第一次觸發的 scroll 事件對應的回調纔會執行,而「一段時間內」觸發的後續的 scroll 回調都會被節流閥無視掉。

如今一塊兒實現一個 throttle:

// fn是咱們須要包裝的事件回調, interval是時間間隔的閾值
function throttle(fn, interval) {
  // last爲上一次觸發回調的時間
  let last = 0
  
  // 將throttle處理結果看成函數返回
  return function () {
      // 保留調用時的this上下文
      let context = this
      // 保留調用時傳入的參數
      let args = arguments
      // 記錄本次觸發回調的時間
      let now = +new Date()
      
      // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值
      if (now - last >= interval) {
      // 若是時間間隔大於咱們設定的時間間隔閾值,則執行回調
          last = now;
          fn.apply(context, args);
      }
    }
}

// 用throttle來包裝scroll的回調
const better_scroll = throttle(() => console.log('觸發了滾動事件'), 1000)

document.addEventListener('scroll', better_scroll)
複製代碼

Debounce: 最後一個參賽者說了算

防抖的主要思想在於:我會等你到底。在某段時間內,無論你觸發了多少次回調,我都只認最後一次。

繼續大胃王比賽故事,此次換了一種比賽方式,時間不限,參賽者吃到不能吃爲止,當每一個參賽都吃不下的時候,後面10分鐘若是沒有人在吃,比賽結束,若是有人在10分鐘內還能吃,則比賽繼續,直到下一次10分鐘內無人在吃時爲止。

對比 throttle 來理解 debounce: 在 throttle 的邏輯裏, ‘裁判’ 說了算,當比賽時間到時,就執行回調函數。而 debounce 認爲最後一個參賽者說了算,只要還能吃的,就從新設定新的定時器。

如今一塊兒實現一個 debounce:

// fn是咱們須要包裝的事件回調, delay是每次推遲執行的等待時間
function debounce(fn, delay) {
  // 定時器
  let timer = null
  
  // 將debounce處理結果看成函數返回
  return function () {
    // 保留調用時的this上下文
    let context = this
    // 保留調用時傳入的參數
    let args = arguments

    // 每次事件被觸發時,都去清除以前的舊定時器
    if(timer) {
        clearTimeout(timer)
    }
    // 設立新定時器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

// 用debounce來包裝scroll的回調
const better_scroll = debounce(() => console.log('觸發了滾動事件'), 1000)

document.addEventListener('scroll', better_scroll)
複製代碼

用 Throttle 來優化 Debounce

debounce 的問題在於它「太有耐心了」。試想,若是用戶的操做十分頻繁——他每次都不等 debounce 設置的 delay 時間結束就進行下一次操做,因而每次 debounce 都爲該用戶從新生成定時器,回調函數被延遲了不可勝數次。頻繁的延遲會致使用戶遲遲得不到響應,用戶一樣會產生「這個頁面卡死了」的觀感。

爲了不弄巧成拙,咱們須要借力 throttle 的思想,打造一個「有底線」的 debounce——等你能夠,但我有個人原則:delay 時間內,我能夠爲你從新生成定時器;但只要delay的時間到了,我必需要給用戶一個響應。這個 throttle 與 debounce 「合體」思路,已經被不少成熟的前端庫應用到了它們的增強版 throttle 函數的實現中:

// fn是咱們須要包裝的事件回調, delay是時間間隔的閾值
function throttle(fn, delay) {
  // last爲上一次觸發回調的時間, timer是定時器
  let last = 0, timer = null
  // 將throttle處理結果看成函數返回
  
  return function () { 
    // 保留調用時的this上下文
    let context = this
    // 保留調用時傳入的參數
    let args = arguments
    // 記錄本次觸發回調的時間
    let now = +new Date()
    
    // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值
    if (now - last < delay) {
    // 若是時間間隔小於咱們設定的時間間隔閾值,則爲本次觸發操做設立一個新的定時器
       clearTimeout(timer)
       timer = setTimeout(function () {
          last = now
          fn.apply(context, args)
        }, delay)
    } else {
        // 若是時間間隔超出了咱們設定的時間間隔閾值,那就不等了,不管如何要反饋給用戶一次響應
        last = now
        fn.apply(context, args)
    }
  }
}

// 用新的throttle包裝scroll的回調
const better_scroll = throttle(() => console.log('觸發了滾動事件'), 1000)

document.addEventListener('scroll', better_scroll)
複製代碼

參考:

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 前端性能優化原理與實踐

你的點贊是我持續分享好東西的動力,歡迎點贊!

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索