2022年我的面试万字总结(JS篇下)

语言: CN / TW / HK

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

前言

又到了金九银十季,最近我也是奔波于各种面试。我自己总结整理了很多方向的前端面试题。借着国庆这个假期,也把这些题目总结分享给大家,也祝正在面试的朋友们能够拿到满意的offer。

往期文章

(1) 2022年我的面试万字总结(浏览器网络篇)

(2) 2022年我的面试万字总结(CSS篇)

(3) 2022年我的面试万字总结(HTML篇)

(4) 2022年我的面试万字总结(JS篇上)

(6) 2022年我的面试万字总结(代码篇)

(7) 2022年我的面试万字总结(Vue上)

(8) 2022年我的面试万字总结(Vue下)

四、原型与继承

4.1说说面向对象的特性与特点

  • 封装性
  • 继承性
  • 多态性

面向对象编程具有灵活、代码可复用、容易维护和开发的有点、更适合多人合作的大型软件项目

4.2 说说你对工厂模式的理解

工厂模式是用来创建对象的一种最常用的设计模式,不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂

其就像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品

4.3 创建对象有哪几种方式?

  1. 字面量的形式直接创建对象

  2. 函数方法

    1. 工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。
    2. 构造函数模式
    3. 原型模式
    4. 构造函数模式+原型模式,这是创建自定义类型的最常见方式。
    5. 动态原型模式
    6. 寄生构造函数模式
  3. class创建

4.4 JS宿主对象和原生对象的区别

原生对象

独立于宿主环境的 ECMAScript 实现提供的对象

包含:Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError

内置对象

开发者不必明确实例化内置对象,它已被内部实例化了

同样是“独立于宿主环境”。而 ECMA-262 只定义了两个内置对象,即 Global 和 Math

宿主对象

BOM 和 DOM 都是宿主对象。因为其对于不同的“宿主”环境所展示的内容不同。其实说白了就是,ECMAScript 官方未定义的对象都属于宿主对象,因为其未定义的对象大多数是自己通过 ECMAScript 程序创建的对象

4.5 JavaScript 内置的常用对象有哪些?并列举该对象常用的方法?

Number 数值对象,数值常用方法

  • Number.toFixed( ) 采用定点计数法格式化数字
  • Number.toString( ) 将—个数字转换成字符串
  • Number.valueOf( ) 返回原始数值

String 字符串对象,字符串常用方法

  • Length 获取字符串的长度
  • split()将一个字符串切割数组
  • concat() 连接字符串
  • indexOf()返回一个子字符串在原始字符串中的索引值。如果没有找到,则返回固定值 -1
  • lastIndexOf() 从后向前检索一个字符串
  • slice() 抽取一个子串

Boolean 布尔对象,布尔常用方法

  • Boolean.toString() 将布尔值转换成字符串
  • Boolean.valueOf() Boolean 对象的原始值的布尔值

Array 数组对象,数组常用方法

  • join() 将一个数组转成字符串。返回一个字符串
  • reverse() 将数组中各元素颠倒顺序
  • delete 运算符只能删除数组元素的值,而所占空间还在,总长度没变(arr.length)
  • shift()删除数组中第一个元素,返回删除的那个值,并将长度减 1
  • pop()删除数组中最后一个元素,返回删除的那个值,并将长度减 1
  • unshift() 往数组前面添加一个或多个数组元素,长度会改变
  • push() 往数组结尾添加一个或多个数组元素,长度会改变
  • concat() 连接数组
  • slice() 切割数组,返回数组的一部分
  • splice()插入、删除或替换数组的元素
  • toLocaleString() 把数组转换成局部字符串
  • toString()将数组转换成一个字符串
  • forEach()遍历所有元素
  • every()判断所有元素是否都符合条件
  • sort()对数组元素进行排序
  • map()对元素重新组装,生成新数组
  • filter()过滤符合条件的元素
  • find() 查找 返回满足提供的测试函数的第一个元素的值。否则返回 undefined。
  • some() 判断是否有一个满足条件 ,返回布尔值
  • fill() 填充数组
  • flat() 数组扁平化

Function 函数对象,函数常用方法

  • Function.arguments 传递给函数的参数
  • Function.apply() 将函数作为一个对象的方法调用
  • Function.call() 将函数作为对象的方法调用
  • Function.caller 调用当前函数的函数
  • Function.length 已声明的参数的个数
  • Function.prototype 对象类的原型
  • Function.toString() 把函数转换成字符串

Object 基础对象,对象常用方法

  • Object 含有所有 JavaScript 对象的特性的超类
  • Object.constructor 对象的构造函数
  • Object.hasOwnProperty( ) 检查属性是否被继承
  • Object.isPrototypeOf( ) 一个对象是否是另一个对象的原型
  • Object.propertyIsEnumerable( ) 是否可以通过 for/in 循环看到属性
  • Object.toLocaleString( ) 返回对象的本地字符串表示
  • Object.toString( ) 定义一个对象的字符串表示
  • Object.valueOf( ) 指定对象的原始值

Date 日期时间对象,日期常用方法

  • Date.getFullYear() 返回 Date 对象的年份字段
  • Date.getMonth() 返回 Date 对象的月份字段
  • Date.getDate() 返回一个月中的某一天
  • Date.getDay() 返回一周中的某一天
  • Date.getHours() 返回 Date 对象的小时字段
  • Date.getMinutes() 返回 Date 对象的分钟字段
  • Date.getSeconds() 返回 Date 对象的秒字段
  • Date.getMilliseconds() 返回 Date 对象的毫秒字段
  • Date.getTime() 返回 Date 对象的毫秒表示

Math 数学对象,数学常用方法

  • Math 对象是一个静态对象
  • Math.PI 圆周率
  • Math.abs() 绝对值
  • Math.ceil() 向上取整(整数加 1,小数去掉)
  • Math.floor() 向下取整(直接去掉小数)
  • Math.round() 四舍五入
  • Math.pow(x,y) 求 x 的 y 次方
  • Math.sqrt() 求平方根

RegExp 正则表达式对象,正则常用方法

  • RegExp.exec() 检索字符串中指定的值。返回找到的值,并确定其位置。
  • RegExp.test( ) 检索字符串中指定的值。返回 true 或 false。
  • RegExp.toString( ) 把正则表达式转换成字符串
  • RegExp.globa 判断是否设置了 "g" 修饰符
  • RegExp.ignoreCase 判断是否设置了 "i" 修饰符
  • RegExp.lastIndex 用于规定下次匹配的起始位置
  • RegExp.source 返回正则表达式的匹配模式

Error 异常对象

  • Error.message 设置或返回一个错误信息(字符串)
  • Error.name 设置或返回一个错误名
  • Error.toString( ) 把 Error 对象转换成字符串

4.6 说一下hasOwnProperty、instanceof方法

hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

4.7 什么是原型对象,说说对它的理解

构造函数的内部的 prototype 属性指向的对象,就是构造函数的原型对象。

原型对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个实例对象后,在这个对象的内部将包含一个指针(*proto*),这个指针指向构造函数的 原型对象,在 ES5 中这个指针被称为对象的原型。

4.8 什么是原型链

原型链是一种查找规则

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,这种链式查找过程称之为原型链

4.9 原型链的终点是什么?

原型链的尽头是null。也就是Object.prototype.proto****

4.10 Js实现继承的方法

1.原型链继承

关键:子类构造函数的原型为父类构造函数的实例对象

缺点:1、子类构造函数无法向父类构造函数传参。

2、所有的子类实例共享着一个原型对象,一旦原型对象的属性发生改变,所有子类的实例对象都会收影响

3、如果要给子类的原型上添加方法,必须放在Son.prototype = new Father()语句后面

function Father(name) {      this.name = name   }    Father.prototype.showName = function () {      console.log(this.name);   }    function Son(age) {      this.age = 20   }    // 原型链继承,将子函数的原型绑定到父函数的实例上,子函数可以通过原型链查找到复函数的原型,实现继承    Son.prototype = new Father()    // 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father    Son.prototype.constructor = Son    Son.prototype.showAge = function () {      console.log(this.age);   }    let son = new Son(20, '刘逍') // 无法向父构造函数里传参    // 子类构造函数的实例继承了父类构造函数原型的属性,所以可以访问到父类构造函数原型里的showName方法    // 子类构造函数的实例继承了父类构造函数的属性,但是无法传参赋值,所以是this.name是undefined    son.showName() // undefined    son.showAge()  // 20

2.借用构造函数继承

关键:用 .call() 和 .apply()方法,在子类构造函数中,调用父类构造函数

缺点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。

2、无法实现函数复用,如果父类构造函数里面有一个方法,会导致每一个子类实例上面都有相同的方法。

function Father(name) {      this.name = name   }    Father.prototype.showName = function () {      console.log(this.name);   }    function Son(name, age) {      Father.call(this, name) // 在Son中借用了Father函数,只继承了父类构造函数的属性,没有继承父类原型的属性。      // 相当于 this.name = name      this.age = age   }    let s = new Son('刘逍', 20) // 可以给父构造函数传参    console.log(s.name); // '刘逍'    console.log(s.showName); // undefined

3.组合继承

关键:原型链继承+借用构造函数继承

缺点:1、使用组合继承时,父类构造函数会被调用两次,子类实例对象与子类的原型上会有相同的方法与属性,浪费内存。

function Father(name) {      this.name = name      this.say = function () {        console.log('hello,world');     }   }    Father.prototype.showName = function () {      console.log(this.name);   }    function Son(name, age) {      Father.call(this, name) //借用构造函数继承      this.age = age   }    // 原型链继承    Son.prototype = new Father()  // Son实例的原型上,会有同样的属性,父类构造函数相当于调用了两次    // 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father    Son.prototype.constructor = Son    Son.prototype.showAge = function () {      console.log(this.age);   }    let p = new Son('刘逍', 20) // 可以向父构造函数里传参    // 也继承了父函数原型上的方法    console.log(p);    p.showName() // '刘逍'    p.showAge()  // 20

4.原型式继承

关键:创建一个函数,将要继承的对象通过参数传递给这个函数,最终返回一个对象,它的隐式原型指向传入的对象。 (Object.create()方法的底层就是原型式继承)

缺点:只能继承父类函数原型对象上的属性和方法,无法给父类构造函数传参

function createObj(obj) {      function F() { }   // 声明一个构造函数      F.prototype = obj   //将这个构造函数的原型指向传入的对象      F.prototype.construct = F   // construct属性指回子类构造函数      return new F        // 返回子类构造函数的实例   }    function Father() {      this.name = '刘逍'   }    Father.prototype.showName = function () {      console.log(this.name);   }    const son = createObj(Father.prototype)    son.showName() // undefined 继承了原型上的方法,但是没有继承构造函数里的name属性

5.寄生式继承

关键:在原型式继承的函数里,给继承的对象上添加属性和方法,增强这个对象

缺点:只能继承父类函数原型对象上的属性和方法,无法给父类构造函数传参

function createObj(obj) {      function F() { }      F.prototype = obj      F.prototype.construct = F      F.prototype.age = 20  // 给F函数的原型添加属性和方法,增强对象      F.prototype.showAge = function () {        console.log(this.age);     }      return new F   }    function Father() {      this.name = '刘逍'   }    Father.prototype.showName = function () {      console.log(this.name);   }    const son = createObj(Father.prototype)    son.showName() // undefined    son.showAge()  // 20

6.寄生组合继承

关键:原型式继承 + 构造函数继承

Js最佳的继承方式,只调用了一次父类构造函数

function Father(name) {      this.name = name      this.say = function () {        console.log('hello,world');     }   }    Father.prototype.showName = function () {      console.log(this.name);   }    function Son(name, age) {      Father.call(this, name)      this.age = age   }    Son.prototype = Object.create(Father.prototype) // Object.create方法返回一个对象,它的隐式原型指向传入的对象。    Son.prototype.constructor = Son    const son = new Son('刘逍', 20)    console.log(son.prototype.name); // 原型上已经没有name属性了,所以这里会报错

7.混入继承

关键:利用Object.assign的方法多个父类函数的原型拷贝给子类原型

function Father(name) {      this.name = name   }    Father.prototype.showName = function () {      console.log(this.name);   } ​    function Mather(color) {      this.color = color   }    Mather.prototype.showColor = function () {      console.log(this.color);   } ​    function Son(name, color, age) {      // 调用两个父类函数      Father.call(this, name)      Mather.call(this, color)      this.age = age   }    Son.prototype = Object.create(Father.prototype)    Object.assign(Son.prototype, Mather.prototype)  // 将Mather父类函数的原型拷贝给子类函数    const son = new Son('刘逍', 'red', 20)    son.showColor()  // red

8. class继承

关键:class里的extends和super关键字,继承效果与寄生组合继承一样

class Father {      constructor(name) {        this.name = name     }      showName() {        console.log(this.name);     }   }    class Son extends Father {  // 子类通过extends继承父类      constructor(name, age) {        super(name)    // 调用父类里的constructor函数,等同于Father.call(this,name)        this.age = age     }      showAge() {        console.log(this.age);     }   }    const son = new Son('刘逍', 20)    son.showName()  // '刘逍'    son.showAge()   // 20

五、异步与事件循环

5.1. 异步编程的实现方式?

JavaScript中的异步机制可以分为以下几种:

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。

5.2 并发与并行的区别?

  • 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

5.3 setTimeout、setInterval、requestAnimationFrame的区别

  • setTimeout

执行该语句时,是立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值时将传入的函数加入任务队列,之后的执行就交给任务队列负责。但是如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间。

返回值timeoutID是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout()来取消该定时器。

  • setInterval

重复调用一个函数或执行一个代码片段,每次都精确的隔一段时间推入一个事件(但是,事件的执行时间不一定就不准确,还有可能是这个事件还没执行完毕,下一个事件就来了)。它返回一个 interval ID,该 ID 唯一地标识时间间隔,因此你可以稍后通过调用 clearInterval() 来移除定时器。

技术上,clearTimeout()clearInterval()可以互换。但是,为了避免混淆,不要混用取消定时函数。

  • requestAnimationFrame

是JS实现动画的一种方式,它告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

5.4. 什么是回调地狱?回调地狱会带来什么问题?

回调函数的层层嵌套,就叫做回调地狱。回调地狱会造成代码可复用性不强,可阅读性差,可维护性(迭代性差),扩展性差等等问题。

Promise语法

5.5. Promise是什么

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

promise本身只是一个容器,真正异步的是它的两个回调resolve()和reject()

promise本质 不是控制 异步代码的执行顺序(无法控制) , 而是控制异步代码结果处理的顺序

5.6 promise实例有哪些状态,怎么改变状态

(1)Promise的实例有三个状态:

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

如何改变 promise 的状态

  • resolve(value): 如果当前是 pending 就会变为 resolved
  • reject(error): 如果当前是 pending 就会变为 rejected
  • 抛出异常: 如果当前是 pending 就会变为 rejected

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

5.7 创建Promise实例有哪些方法

  • new Promise((resolve,reject)=>{ ... })

一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolvepromise.reject这两个方法:

  • Promise.resolve

Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:

Promise.resolve(11).then(function(value){ console.log(value); // 打印出11 });

  • Promise.reject

Promise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:

Promise.reject(new Error(“出错了!!”));

5.8 Promise有哪些实例方法

then

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

catch

该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

finally

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port) .then(function () {    // ... }) .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

5.9 Promise有哪些静态方法

all

all方法可以完成并发任务, 它接收一个数组,数组的每一项都是一个promise对象,返回一个Promise实例。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected

race

race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

any

它接收一个数组,数组的每一项都是一个promise对象,该方法会返回一个新的 promise,数组内的任意一个 promise 变成了resolved状态,那么由该方法所返回的 promise 就会变成resolved状态。如果数组内的 promise 状态都是rejected,那么该方法所返回的 promise 就会变成rejected状态,

resolve、reject

用来生成对应状态的Promise实例

5.10 Promise.all、Promise.race、Promise.any的区别

all: 成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

race: 哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

any: 返回最快的成功结果,如果全部失败就返回失败结果。

5.11 一个promise指定多个回调函数, 都会调用吗?

都会调用,成功状态放在then的第一个参数里调用

let p2 = new Promise((resolve, reject) => {    resolve(1) }) p2.then(value => {    console.log('第一个', value) }) p2.then(value => {    console.log('第二个', value) })

失败状态放在then的第二个参数里调用

let p3 = new Promise((resolve, reject) => {    reject(2) }) p3.then(       ()=>{},        value => {console.log('第一个', value)}   ) p3.then(       ()=>{},        value => {console.log('第二个', value)}   )

5.12 改变 promise 状态和指定回调函数谁先谁后?

  1. 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调

  2. 如何先改状态再指定回调?

    • 在执行器中直接调用 resolve()/reject()
    • 延迟更长时间才调用 then()
  3. 什么时候才能得到数据?

    • 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
    • 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据

5.13 promise.then()返回的新 promise 的结果状态由什么决定?

  1. 简单表达: 由 then()指定的回调函数执行的结果决定

  2. 详细表达:

    • 如果抛出异常, 新 promise 变为 rejected, 参数为抛出的异常
    • 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值
    • 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果

5.14 promise 如何串连多个操作任务?

  • promise 的 then()返回一个新的 promise, 可以开成 then()的链式调用
  • 通过 then 的链式调用串连多个同步/异步任务

5.15 promise 异常传透是什么?

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

  • 当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调,
  • 前面任何操作出了异常, 都会传到最后失败的回调中处理

5.16 如何中断 promise 链?

  • 当使用 promise 的 then 链式调用时, 在中间中断, 不再调用后面的回调函数。 在回调函数中返回一个 pendding 状态的 promise 对象

5.17 promise有什么缺点

代码层面

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

语法层面

  • Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦
  • Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

async/await语法

5.18 async 函数是什么

  • 一句话概括: 它就是 Generator 函数的语法糖,也就是处理异步操作的另一种高级写法

5.19 async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {  // ... } ​ // 等同于 ​ function fn(args) {  return spawn(function* () {  // spawn函数就是自动执行器    // ... }); }

5.20 async函数的返回值

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数。

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

5.21 await 到底在等待什么?

await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

5.22 什么是顶层await?

从 ES2022 开始,允许在模块的顶层独立使用await命令,使得上面那行代码不会报错了。它的主要目的是使用await解决模块异步加载的问题。

import { AsyncFun } from 'module' await AsyncFun() console.log(123)

5.23 如何用await让程序停顿指定的时间(休眠效果)

JavaScript 一直没有休眠的语法,但是借助await命令就可以让程序停顿指定的时间

function sleep(interval) {  return new Promise(resolve => {    setTimeout(resolve, interval); }) } ​ // 用法 async function one2FiveInAsync() {  for(let i = 1; i <= 5; i++) {    console.log(i);    await sleep(1000); } } ​ one2FiveInAsync();

5.24 await的使用注意点

  1. await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
  2. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
  3. await命令只能用在async函数之中,如果用在普通函数,就会报错。
  4. async 函数可以保留运行堆栈。

5.25 async语法怎么捕获异常

async函数内部的异常可以通过 .catch()或者 try/catch来捕获,区别是

  • try/catch 能捕获所有异常,try语句抛出错误后会执行catch语句,try语句内后面的内容不会执行
  • catch()只能捕获异步方法中reject错误,并且catch语句之后的语句会继续执行

async函数错误捕获,以登录功能为例      async function getCatch () {        await new Promise(function (resolve, reject) {          reject(new Error('登录失败'))       }).catch(error => {          console.log(error)  // .catch()能捕获到错误信息       })        console.log('登录成功') // 但是成功信息也会执行     }           async function getCatch () {        try {          await new Promise(function (resolve, reject) {            reject(new Error('登录失败'))         })          console.log('登录成功')  // try抛出错误之后,就不会执行这条语句       } catch (error) {          console.log(error)  // catch语句能捕获到错误信息       }     }

5.26 async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

事件循环Event Loop

5.27 JS的执行机制(同步任务、异步任务)

JS是一门单线程语言,单线程就意味着,所有的任务需要排队,前一个任务结束,才会执行下一个任务。这样所导致的问题是:如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的觉。为了解决这个问题,JS中出现了同步和异步。

同步任务:即主线程上的任务,按照顺序由上⾄下依次执⾏,当前⼀个任务执⾏完毕后,才能执⾏下⼀个任务。

异步任务:不进⼊主线程,⽽是进⼊任务队列的任务,执行完毕之后会产生一个回调函数,并且通知主线程。当主线程上的任务执行完后,就会调取最早通知自己的回调函数,使其进入主线程中执行。

5.28 什么是Event Loop

  • 事件循环Event Loop又叫事件队列,两者是一个概念

事件循环指的是js代码所在运行环境(浏览器、nodejs)编译器的一种解析执行规则。事件循环不属于js代码本身的范畴,而是属于js编译器的范畴,在js中讨论事件循环是没有意义的。换句话说,js代码可以理解为是一个人在公司中具体做的事情, 而 事件循环 相当于是公司的一种规章制度。 两者不是一个层面的概念。

5.29 宏任务与微任务的概念与区别

为了协调任务有条不紊地在主线程上执行,页面进程引入了 消息队列事件循环机制,渲染进程内部也会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。这些消息队列中的任务就称为 宏任务

微任务是一个需要异步执行的回调函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。当 JS 执行一段脚本(一个宏任务)的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个 微任务队列。也就是说 每个宏任务都关联了一个微任务队列

5.30 常见的宏任务与微任务分别有哪些

| 任务(代码) | 宏/微 任务 | 环境 | | ------------------ | ------ | -------- | |