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…

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

相关文章
相关标签/搜索