本文目录:
- 1.说一说函数中的私有方法、公共方法以及静态方法
- 2.说一说this的指向
- 3.说一说call()、apply()、bind()的联系和区别
- 4.说一下对js中同步和异步的理解
- 5.事件循环机制(Event Loop)
- 6.JS原始数据类型有哪些?引用数据类型有哪些?
- 7.说说JSON.parse和eval()的区别
- 8.说一说js中的强制类型转换和自动类型转换(隐式转换)
- 9.执行上下文
- 10.作用域和作用域链
- 11.什么是函数式编程,说下你对函数式编程的理解。
- 12.defer 和 async 异同点?
- 13.原型和原型链
- 14.继承
- 15.闭包
- 16.es6的class类的es5的构造函数有什么区别
- 17.addEventListener第三个参数是什么
- 18.setTimeout 的执行原理
- 19.浅拷贝与深拷贝
- 20.typeof与 instanceOf 、Object.prototype.toString.call() 的区别
- 21.使用 bind 和箭头函数的区别
- 22.说一下事件委托(事件代理机制)
- 23.什么是 IIFE
- 24.说一下 ES5 和ES6的主要区别
- 25.避免使用箭头函数的情况以及使用箭头函数的好处是什么
- 26.展开(spread)语法的好处以及它与剩余(rest)语法的不同
- 27.解释一下 Object.freeze() 和 const 的区别
- 28.类数组转为真正数组的必备条件
- 29.什么是回调函数?回调函数有什么缺点
- 30.Promise是什么,可以手写实现一下吗
- 31.async await 和 promise 的区别与联系
1.说一说函数中的私有方法、公共方法以及静态方法
- 在函数中直接进行定义的变量和方法是私有方法,只能在函数内部进行使用。
- 通过this.的形式和定义在prototype上的是公共属性和公共方法,在实例化对象的时候会自动挂载到实例化对象上面,并且也只能通过实例化对象去访问和使用。在prototype中也可以通过this直接去使用函数中通过this添加的公共属性。另外在函数内部减少使用this去添加方法(函数),这样会形成闭包,并且在每次实例化对象的时候都执行一次,造成更大的性能消耗(公共属性和方法直接添加到prototype)。
- 通过构造函数.的形式添加的属性和方法是静态属性和静态方法,只能通过构造函数.的形式去访问和使用,如Array.isArray()
2.说一说this的指向
- 1.在方法中,this 指的是拥有者
- 2.单独使用的时候,this 指的是全局对象
- 3.在函数中,this 指的是全局对象,严格模式下,this 是 undefined
- 4.在事件中,this 指的是接收事件的元素
- 箭头函数里面没有自己的this
- 构造函数的this默认为实例对象
- 在函数中的this:谁去调用它,this就指向谁
3.说一说call()、apply()、bind()的联系和区别
共同点
- 这三者的作用都是:通过强硬改变函数中this的指向。让函数在某个指定的对象下执行。
- 三者的第一个参数都是函数中this的指向对象
区别: - bind返回的是一个新的函数,你必须调用它才会被执行,而call和apply会直接自动执行。
- call和bind的第二及第n个参数是用都逗号分割的,而apply总共就两个参数,第二个参数就是一个数组。
4.说一下对js中同步和异步的理解
js的任务分为同步任务和异步任务,js是单线程语言,所以直接进行主线程执行的任务是同步任务,被挂起到任务队列等待执行的是异步任务,常见的异步任务有绑定事件、定时器以及多数情况下的ajax请求和promise.then,需要特别注意的一点是:promise.then的执行顺序高于其他几种常见的异步任务(微任务高于宏任务)。
5.事件循环机制(Event Loop)
异步任务可以分为宏任务和微任务
宏任务主要有setTimeout、setInterval、setImmediate、I/O、UI rendering,微任务主要有promise.then、process.nextTick
执行顺序
- 1、先执行主线程
- 2、遇到宏队列(macrotask)放到宏队列(macrotask)
- 3、遇到微队列(microtask)放到微队列(microtask)
- 4、主线程执行完毕
- 5、执行微队列(microtask),微队列(microtask)执行完毕
- 6、执行一次宏队列(macrotask)中的一个任务,执行完毕
- 7、依次循环。。。
6.JS原始数据类型有哪些?引用数据类型有哪些?
在 JS 中,存在着 7 种原始值,分别是:
- boolean
- null
- undefined
- number
- string
- symbol(es6新增,表示独一无二的值)
- bigint(es10新增)
引用数据类型: 对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math,函数对象-Function)
JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。
原始数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
7.说说JSON.parse和eval()的区别
两者都可以对字符串进行解析,JSON.parse要解析的字符串必须完全符合JSON格式,否则会报错,而eval()则看上去强大的多,可解析不规范的JSON字符串,但是也会对字符串进行计算和执行,尤其是对于未知数据,你根本不知道eval()之后会干什么。所以在实际开发中,我们很少用到eval()。
8.说一说js中的强制类型转换和自动类型转换(隐式转换)
强制类型转换:
Number()带有其他任何字符,结果都会变成NaN
String()
parseInt()保留整数,parseFloat转换成小数
Boolean(),数字零和空字符串和null和undefined会被转换成false,其他会转换成true,注意空格会被转换成true
自动类型转换(隐式转换)
隐式转换体现了js这门语言的宽容性,但是也是js的一个大坑,比较常见的就是运算符的隐式转换,加号会优先进行字符串拼接,而减号乘号除号会优先进行数字运算,如:
3+‘2’,结果是‘32’,加法会优先进行字符串转换,并进行拼接
3-‘2’,结果是1,减法会优先自动转换成数字,并进行计算,另外乘法和除法也都会自动进行数字转换。
3‘2’,结果是6
3/‘2’,结果是1.5
8null,结果是0
==更是有数之不尽的坑,如null == undefined 结果为true,NaN !== NaN,[] == false ,0 == false,我们在编程中应该注重变量的类型定义,避免不同类型的数据进行比较和运算,用===去代替==。
9.执行上下文
当代码运行时,会产生一个对应的执行环境,代码从上往下开始执行,就叫做执行上下文。特点:
- 1.单线程,在主进程上运行
- 2.同步执行,从上往下按顺序执行
- 3.全局上下文只有一个,浏览器关闭时会被弹出栈
- 4.函数的执行上下文没有数目限制
- 5.函数每被调用一次,都会产生一个新的执行上下文环境
10.作用域和作用域链
作用域
在 Javascript 中,作用域分为 全局作用域 和 函数作用域。拥有全局作用于的代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。函数作用域在固定的代码中才能被访问。
作用域有上下级关系,上下级关系的确定就看函数是在哪个作用域下创建的。如在fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
作用域链
一般情况下,变量取值到 创建 这个变量 的函数的作用域中取值,但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
11.什么是函数式编程,说下你对函数式编程的理解。
函数式编程(经常缩写为FP)是通过组合纯函数,避免共享状态、可变数据、和副作用来构建软件的过程。函数式编程是声明性的而不是命令式的,应用状态流经纯函数中。相比于面向对象编程,其中的应用状态经常是共享的,并且和方法一起定义在一些对象中。
函数式编程是一种编程模式。意味着它是一种基于一些基本原理和定义原则来思考软件构造的方式。其它的编程模式还包括面向对象编程和过程式编程。
相比于命令式的和面向对象式的代码,函数式的代码趋向于更简洁、更加可预言的、更容易测试。
12.defer 和 async 异同点?
相同点:
async和defer都是script标签的属性,都代表异步加载script引入的js文件,使用方法是直接加在标签中就可以了
<script type="text/javascript" src="js/1.js" defer></script>
<script type="text/javascript" src="js/1.js" async></script>
不同点:
defer
文档解析时,遇到设置了defer的脚本,就会在后台进行下载,但是并不会阻止文档的渲染,当页面解析&渲染完毕后。会等到所有的defer脚本加载完毕并按照顺序执行,执行完毕后会触发DOMContentLoaded事件。如果你的脚本代码依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖,请使用defer
async
async脚本会在加载完毕后立即执行,会根据加载完毕的时间打破正常的执行顺序,如果你的脚本并不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据,请使用async,如果不太确定各个JS文件的用途,慎用async。
13.原型和原型链
原型是一个对象,也称为原型对象。原型的作用,就是共享方法。我们通过在原型上添加可共享方法,实例化对象的时候,就不会反复开辟空间存储方法。原型中this的指向是实例。
原型与原型层层相链接的过程即为原型链。对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有proto原型的存在。
14.继承
ES6之前并没有给我们提供extends继承,我们可以通过构造函数+原型对象模拟实现继承。
继承属性,利用call改变this指向。但该方法只可以继承属性,实例不可以使用父类的方法。继承父类方法的方式有很多,当前最成熟的方式是寄生方式。
15.闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的最常见的方式,就是在一个函数内部创建另一个函数。闭包这个概念涉及到作用域链、执行上下文和垃圾回收机制,需通过全面的分析才能有更好的理解。
16.es6的class类的es5的构造函数有什么区别
本质上都是函数,class只是构造函数的另一种写法,让对象原型的写法更加清晰(语法糖)。构造函数和class创建的实例,里面都有proto指向prototype原型对象。
// ES5 Function Constructor
function Person(name) {
this.name = name;
}
// ES6 Class
class Person {
constructor(name) {
this.name = name;
}
}
区别:
ES6的类和构造函数的主要区别在于继承。如果咱们创建一个继承Person类的Student子类并添加一个studentId字段,以下是两种方式的使用:
function Student(name, studentID) {
// 调用你类的构造函数以初始化你类派生的成员。
Person.call(this, name)
// 初始化子类的成员。
this.studentID= studentID
}
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
// ES6 Class
class Student extends Person {
constructor(name, studentId) {
super(name)
this.studentId = studentId
}
}
主要区别说明:
- 通过构造函数创建实例,是可以变量提升的。es6中的类,必须先有类,才可以实例化。
- 类的所有实例都共享一个原型对象,同时类的所有方法都定义在类的prototype属性上面。
- constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法,一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加
- 类必须使用new调用,负责会报错,这是它跟普通构造函数的一大区别,后者不用new也可以调用。
- 类的内部,默认就是严格模式,所以不需要使用use strict指定运行模式
17.addEventListener第三个参数是什么
element.addEventListener(event, function, useCapture)
第一个参数:event,必须,字符串,指定事件名
第二个参数,必须,指定要事件触发时执行的函数
第三个参数,可选,布尔值,true - 事件句柄在捕获阶段执行,false- 默认值,事件句柄在冒泡阶段执行
18.setTimeout 的执行原理
setTimeout方法包含两个参数,第一个参数为一个函数或者一个会作为eval()方法参数的js代码字符串,第二个参数为以毫秒为单位的时间。该方法的实际作用即:在一定时间之后,把一个函数加入消息队列末尾。如果这个时间点消息队列中还存在其他消息,那么该函数会在排在他之前的消息都执行完之后再开始执行。
19.浅拷贝与深拷贝
如果拷贝的对象里的元素只有值,没有引用,那浅拷贝和深拷贝没有差别,都会将原有对象复制一份,产生一个新对象,对新对象里的值进行修改不会影响原有对象,新对象和原对象完全分离开。
如果拷贝的对象里的元素包含引用(像一个列表里储存着另一个列表,存的就是另一个列表的引用),那浅拷贝和深拷贝是不同的,浅拷贝虽然将原有对象复制一份,但是依然保存的是引用,所以对新对象里的引用里的值进行修改,依然会改变原对象里的列表的值,新对象和原对象并没有完全分离开。而深拷贝则不同,它会将原对象里的引用也新创建一个,即新建一个列表,然后放的是新列表的引用,这样就可以将新对象和原对象完全分离开。
20.typeof与 instanceOf 、Object.prototype.toString.call() 的区别
联系:typeof和instanceof的目的都是检测变量的类型,两个区别在于typeof只能用于检测基本数据类型,instanceof可以检测基本数据类型,也可以检测某些引用数据类型,但是instanceof只能通过true或者false来判断,不能直接看出来是什么类型。
由此可见,无论是typeof还是instanceof都不能准确判断出正确的类型。所以需要另外的更直观的方法:
通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。
21.使用 bind 和箭头函数的区别
箭头函数生成一个JavaScript函数对象,其this上下文绑定到与this创建它们的位置相同的值。
显然,在一般意义上,Function.prototype.bind它比箭头函数更灵活:它可以将函数中的this绑定到本地以外值,并且它可以让this在任何时间绑定指定的对象。
22.说一下事件委托(事件代理机制)
事件委托 本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到 目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
23.什么是 IIFE
IIFE是一个立即调用的函数表达式,它在创建后立即执行
(function IIFE(){
console.log( "Hello!" );
})();
// "Hello!"
常常使用此模式来避免污染全局命名空间,因为在IIFE中使用的所有变量(与任何其他普通函数一样)在其作用域之外都是不可见的。
24.说一下 ES5 和ES6的主要区别
ECMAScript 5 (ES5):ECMAScript 的第五版,于2009年标准化,该标准已在所有现代浏览器中完全支持。
ECMAScript 6 (ES6)/ ECMAScript 2015 (ES2015):ECMAscript 第 6 版,2015 年标准化。这个标准已经在大多数现代浏览器中部分实现。
1.箭头函数和字符串插值
const greetings = (name) => {
return `hello ${name}`;
}
也可以这样写:
const greetings = name => `hello ${name}`;
2.const
const 表示无法修改变量的原始值。
3.块作用域
ES6 中 let, const 会创建块级作用域,不会像 var 声明变量一样会被提升。
4.默认参数
默认参数使咱们可以使用默认值初始化函数。当参数省略或 undefined 时使用默认参数值。
function multiply (a, b = 2) {
return a * b;
}
multiply(5); // 10
5.类定义与继承
ES6 引入了对类(class关键字)、构造函数(constructor关键字)和 extend 关键字(用于继承)的语言支持。
6.for-of 运算符
for...of 语句创建一个遍历可迭代对象的循环。
7.展开操作符
const obj1 = { a: 1, b: 2 }
const obj2 = { a: 2, c: 3, d: 4}
const obj3 = {...obj1, ...obj2}
8.Promise
Promise 提供了一种机制来处理异步操作的结果和错误。可以使用回调来完成相同的事情,但是Promises 通过方法链接和简洁的错误处理来提高可读性。
const isGreater = (a, b) => {
return new Promise ((resolve, reject) => {
if(a > b) {
resolve(true)
} else {
reject(false)
}
})
}
isGreater(1, 2)
.then(result => {
console.log('greater')
})
.catch(result => {
console.log('smaller')
})
**9.模块导出和导入
const myModule = { x: 1, y: () => { console.log('This is ES5') }}
export default myModule;
import myModule from './myModule';
25.避免使用箭头函数的情况以及使用箭头函数的好处是什么
避免使用箭头函数的情况:
1.当想要函数被提升时(箭头函数是匿名的)
2.要在函数中使用this/arguments时,由于箭头函数本身不具有this/arguments,因此它们取决于外部上下文
3.使用命名函数(箭头函数是匿名的)
4.使用函数作为构造函数时(箭头函数没有构造函数)
5.当想在对象字面是以将函数作为属性添加并在其中使用对象时,因为咱们无法访问 this 即对象本身。
为啥大多数情况都建议使用箭头函数?
1.作用域安全性:当箭头函数被一致使用时,所有东西都保证使用与根对象相同的thisObject。如果一个标准函数回调与一堆箭头函数混合在一起,那么作用域就有可能变得混乱。
2.紧凑性:箭头函数更容易读写。
3.清晰度:使用箭头函数可明确知道当前 this 指向。
26.展开(spread)语法的好处以及它与剩余(rest)语法的不同
ES6 的展开语法在以函数形式进行编码时非常有用,因为咱们可以轻松地创建数组或对象的副本,而无需求助于Object.create,slice或库函数。Redux 和rx.js项目中经常使用此特性。
function putDookieInAnyArray(arr) {
return [...arr, 'dookie'];
}
const result = putDookieInAnyArray(['I', 'really', "don't", 'like']);
// ["I", "really", "don't", "like", "dookie"]
const person = {
name: 'Todd',
age: 29,
};
const copyOfTodd = { ...person };
ES6 的 rest 语法提供了一种捷径,其中包括要传递给函数的任意数量的参数。
就像展开语法的逆过程一样,它将数据放入并填充到数组中而不是展开数组,并且它在函数变量以及数组和对象解构分中也经常用到。
function addFiveToABunchOfNumbers(...numbers) {
return numbers.map(x => x + 5);
}
const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10);
// [9, 10, 11, 12, 13, 14, 15]
const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]
const { e, f, ...others } = {
e: 1,
f: 2,
g: 3,
h: 4,
};
// e: 1, f: 2, others: { g: 3, h: 4 }
对于三个点号,三点放在形参或者等号左边为rest运算符; 放在实参或者等号右边为spread运算符,或者说,放在被赋值一方为rest运算符,放在赋值一方为扩展运算符。
如果是做为rest运算符的时候,...不能出现了中间,spread运算符则位置随意。
27.解释一下 Object.freeze() 和 const 的区别
const和Object.freeze是两个完全不同的概念。
const 声明一个只读的变量,一旦声明,常量的值就不可改变:
const person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
person = animal; // ERROR "person" is read-only
Object.freeze适用于值,更具体地说,适用于对象值,它使对象不可变,即不能更改其属性。
let person = {
name: "Leonardo"
};
let animal = {
species: "snake"
};
Object.freeze(person);
person.name = "Lima"; //TypeError: Cannot assign to read only property 'name' of object
console.log(person);
28.类数组转为真正数组的必备条件
要将一个类数组对象转换为一个真正的数组,必须具备以下条件:
- 1、该类数组对象必须具有length属性,用于指定数组的长度。如果没有length属性,那么转换后的数组是一个空数组。
- 2、该类数组对象的属性名必须为数值型或字符串型的数字
29.什么是回调函数?回调函数有什么缺点
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中,我们等待id为btnAdd的元素中的click事件,如果它被单击,则执行clickCallback函数。回调函数向某些数据或事件添加一些功能。
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个事件存在依赖性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
这就是典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来就更是乱糟糟,所以在es6中提出了Promise和async/await来解决回调地狱的问题。当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。
30.Promise是什么,可以手写实现一下吗
Promise,翻译过来是承诺,承诺它过一段时间会给你一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在MDN的相关说明:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:
pending: 初始状态,既不是成功,也不是失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。可能光看概念大家不理解Promise,我们举个简单的栗子;
假如我有个女朋友,下周一是她生日,我答应她生日给她一个惊喜,那么从现在开始这个承诺就进入等待状态,等待下周一的到来,然后状态改变。如果下周一我如约给了女朋友惊喜,那么这个承诺的状态就会由pending切换为fulfilled,表示承诺成功兑现,一旦是这个结果了,就不会再有其他结果,即状态不会在发生改变;反之如果当天我因为工作太忙加班,把这事给忘了,说好的惊喜没有兑现,状态就会由pending切换为rejected,时间不可倒流,所以状态也不能再发生变化。
上一条我们说过Promise可以解决回调地狱的问题,没错,pending 状态的 Promise 对象会触发 fulfilled/rejected 状态,一旦状态改变,Promise 对象的 then 方法就会被调用;否则就会触发 catch。我们将上一条回调地狱的代码改写一下:
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
}).then((res) => {
setTimeout(() => {
console.log(2)
},2000)
}).then((res) => {
setTimeout(() => {
console.log(3)
},3000)
}).catch((err) => {
console.log(err)
})
其实Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
promise手写实现,面试够用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
if(self.status==="resolved"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
if(self.status==="rejected"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
31.async await 和 promise 的区别与联系
async await 和 promise 都是异步编程解决方案。
Promise的写法只是回调函数的改进,使用then方法,只是让异步任务的两段执行更清楚而已。Promise的最大问题是代码冗余,请求任务多时,一堆的then,也使得原来的语义变得很不清楚。
async搭配await是ES7提出的,它的实现是基于Promise,通过同步方式的写法,使得代码更容易阅读。
async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
网友评论