0%

JavaScript面试题汇总

JavaScript面试题汇总

js中的同步和异步

同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。

异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。异步模式:每一个任务都有一个或多个回调函数,前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务择时不等前一个任务结束就执行,所以程序的执行顺序与任务排列顺序是不一致的、异步的。当我们打开网站时,像图片的加载,音乐的加载,其实都是一个异步任务。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax请求。

JavaScript的异步机制:

  • 所有的同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个任务队列,只要异步任务有了结果,就会在任务队列中放置一个事件
  • 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面还有哪些事件,哪些对应的异步任务,然后结束等待状态,进入执行栈,开始执行
  • 主线程不断的重复上面三步

异步编程的实现方式

  • 回调函数:
    这是异步编程最基本的方法,当一个函数作为参数传入另一个参数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就成为回调函数。优点是简单。容易理解和部署,缺点是多个回调函数嵌套的时候就会掉造成回调地狱,上下两层的回调函数之间的代码耦合度太高,不利于代码的可维护性。

  • 事件监听:
    事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

  • 发布/订阅:
    嘉定存在一个“信号中心”,某个任务执行完成,就向中心发布一个信号,其他任务可以向中心订阅这个信号,从而知道什么时候自己可以开始执行,又称“观察者模式”

  • Promise
    字面上的意思就是承诺。可以将嵌套的回调函数用于链式调用。每一个异步任务返回一个Promise对象,该对象有一个then方法,允许置顶回调函数,但有多个then的链式调用时,同样会造成回调地狱。Promise总共有三种状态:Panding(初始)Fulfilled(成功)Rejected(失败)

  • Generator
    ES6提供的一种异步编程解决方案,当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕再将执行权给转移回来。

  • async/await异步终极解决方案)
    generator和promise实现的一个自动执行的语法糖。相对于Promise,优势在于处理then的调用链,更信息准确的写出代码,并且也能优雅的解决毁掉地狱问题,缺点,await将异步代码改造成了同步代码,如果多个异步没有依赖性却使用了await会导致性能上的降低,代码没有依赖性的话,完全可以使用Promise.all的方式。async/await与Promise一样,是非阻塞的。async/await是基于Promise实现的,是改良版的Promise,不能用于普通的回调函数。

闭包closure

JavaScript函数内部可以读取外部的变量,但是在函数外部无法直接读取函数内部的局部变量

闭包就是能够读取其他函数内部变量的函数。

由于在JavaScript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单的理解成定义在一个函数内的函数。闭包可以让你在一个内层函数中访问到其他外层函数的作用域,从而使这个外层函数的变量可以被外部访问到。闭包最大的作用就是延续函数内部某变量的生命周期。

闭包的特点:

  • 让外部访问函数内部变量
  • 让这些变量的值始终保持在内存中,不会再外部函数调用后被自动清楚
  • 避免使用全局变量,防止变量名污染
  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,导致内存泄露。解决办法是函数提供清除变量的方法,在适当的时机调用它用以删除变量。

防抖(debounce)

防止抖动,单位时间内事件触发会被重置,避免事件被误伤触发多次。

1
2
3
4
5
6
7
8
9
10
11
12
<button onclick="click(() => console.log(Date.now()), 1000)">按钮</button>
<script>
function click(fn, delay) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(function() {
fn.apply(this);
}, delay)
}
}
</script>

防抖应用场景:

  • 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  • 表单验证
  • 按钮提交事件
  • 浏览器窗口缩放,resize时间(如窗口停止改变大小以后重新计算布局)

节流(throttle)

控制流量,单位时间内事件只能触发一次,一个函数执行一次之后,只有大于设定的执行周期后才会执行第二次,有个需要频繁触发的函数,出于优化性能角度,在规定时间内,只让函数生效一次,后面不生效。所以节流会稀释函数的执行频率。代码实现种在开锁关锁机制。

1
2
3
4
5
6
7
8
9
10
function throttle(fn, wait) {
let lasttime = 0
return function() {
let now = Date.now()
if(now - lasttime > wait) {
fn.call(this);
lasttime = now
}
}
}

节流应用场景:

  • 按钮点击事件
  • 拖拽事件
  • 滚动事件Scroll
  • 计算鼠标移动的距离

宏任务与微任务(事件循环机制)

宏任务: 当前调用栈中执行的代码成为宏任务。(主代码块、定时器等)

I/O setTimeout setInterval setImmediate requestAnimationFrame

微任务: 当前(此次事件循环中)宏任务执行完,下一个宏任务开始之前需要执行的任务,可以理解为回调函数。

process.nextTick MutationObserver Promise.then catch finally

宏任务中的事件放在callback quene(有多个)中,由事件触发现成维护;微任务的时间放在微任务对列中(只有一个)中,由js引擎线程维护。

运行机制:

  • 在执行栈中执行第一个宏任务(只有一个任务:执行主线程的JS代码)
  • 执行过程中遇到微任务,将微任务添加到微任务对列中(微任务只有一个)
  • 当前宏任务执行完毕,立即执行微任务队列中的所有任务
  • 当前微任务队列中的任务执行完毕,检查渲染、GUI线程接管渲染
  • 渲染完毕后,JS线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取出)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    console.log('start')

    setTimeout(() => {
    console.log('setTimeout')
    }, 0)

    new Promise((resolve) => {
    console.log('promise')
    resolve()
    })
    .then(() => {
    console.log('then1')
    })
    .then(() => {
    console.log('then2')
    })

    console.log('end')
    //输出:
    start
    promise
    end
    then1
    then2
    setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async  function  async1 ()  {//函数创建未执行,所以不输出
console.log('async1 start');
await async2();//此处输出async2后在微队列等待执行栈完成任务后执行。
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start');
setTimeout(function () {//定时器,将任务放在宏任务中
console.log('setTimeout')//微队列执行完成后执行宏队列,输出setTimeout。
}, 0);
async1();//调用时才会执行
new Promise(function (resolve) {
console.log('promise1');//输出promise1完成后,Promise.then推送至微队列排队
resolve()
}).then(function () {
console.log('promise2')
});
console.log('script end')//输出script end,此时执行栈清空,开始执行微队列输出async1 end和promise2

//输出:
script start
async1 start
async2
promise1
script end
async1 end
promise2
undefined
setTimeout

总结:

  • 先执行同步和理解执行任务,比如console.log(),new Promise()
  • 再依次执行微任务,比如then函数catch函数
  • 当微任务执行完后再开始执行宏任务,比如定时器,回调函数等