欢迎光临
我们一直在努力

前端面试中 JavaScript 3个经常被问到的问题

问题 #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毫秒内全部调用。

赞(0)
版权归原作者所有,如有侵权请告知。达维营-前端网 » 前端面试中 JavaScript 3个经常被问到的问题

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址