题目
原题已不记得,根据记忆复原如下:
const list = [1,2,3];
const YB = async(v) => {
return new Promise(resolve => {
setTimeout(resolve, 1000, v**2)
})
};
list.forEach(async (v) => {
let res = await YB(v);
console.log(res);
});
复制代码
以下是笔者自己的思考:
首先res的值肯定是[1,2,3]
三个值的平方1,4,9。因为知道await
会把后面的同步代码包装在一个Promise.then()
里异步执行,所以回答说一秒后打印出1,再隔一秒打印出4,再隔一秒打印出9。
面试的时候,心里也没有底。下来后自己写demo试运行。发现并不是这个样子。会打印出1,4,9。不过是等一秒钟后,打印出1,然后4和9紧接着就被打印出来,并不会等待一秒。
分析
笔者一开始对这个结果并不是很明白,好在经过一定分析与实践,总算是搞明白了。
串行执行
抛开题目,我们先写一个1,4,9分别间隔1秒打印的例子
const list = [1,2,3];
const YB = async(v) => {
return new Promise(resolve => {
console.log(v, 'clog');
setTimeout(resolve, 1000, v**2)
})
};
async function LIST() {
for(let i = 1; i < 4; i++){
let res = await YB(i);
console.log(res);
}
}
LIST();
复制代码
打印的结果如下
1 "clog" //不用等待
1 //1秒后打印出
2 "clog" //1秒后打印出
4 //2秒后打印出
2 "clog" //2秒后打印出
9 //3秒后打印出
复制代码
这个例子并不难理解,按照高级程序设计上的说法,属于期约的串行执行,简单理解就是第一个期约执行完毕,再去执行下一个期约。(期约就是我们常说的Promise)。
并行执行
既然有串行执行,那么按照常识,应该会有并行执行,也确实有,高程上称为平行执行,理解为多个期约同时执行。
现在把上边的例子改成平行执行
const list = [1,2,3];
const YB = async(v) => {
return new Promise(resolve => {
console.log(v, 'clog');
setTimeout(resolve, 1000, v**2)
})
};
const promises = list.map(YB);
async function LIST() {
for(const p of promises) {
let res = await p;
console.log(res);
}
}
LIST();
复制代码
打印的结果如下
1 "clog" //不用等待
2 "clog" //不用等待
3 "clog" //不用等待
1 //1秒后打印出
4 //1秒后打印出
9 //1秒后打印出
复制代码
两种执行的区别
对比这两种方式,我们可以看出区别。
- 想要并行执行期约,那么
Promise
实例是需要有new
操作的。什么时候会有Promise
的new
操作呢,示例中可以看出,是在YB
函数中完成的。 - 串行的例子中,
YB
函数是在await
后边出现的,所以上一个期约执行(resolve
)完,才会new
下一个期约并执行,在进入new Promse的同步代码中,把setTimeout添加进事件队列。在串行中,3个Promise的new操作并不是同时进行的,所以setTimeout进入事件队列也不是同时的,开始计时的时刻也不一样。 - 并行的例子中,通过
list.map(YB)
完成了Promse
的实例化,并把这些Promise存入了LIST
数组里,在这个时候,Promse
里的setTimeout
已经放入了事件队列,开始计时了。因为list.map(YB)
里并没有await
进行阻塞,所以3个Promise
的实例化和3个setTimeout
的添加进事件队列,可以认为是同时的(虽然实际上会有逻辑上的先后顺序),计时也是同时开始的。
forEach和for循环的区别
根据上边的并行执行的例子,我们可以看出,并行的执行效果和题目的执行效果基本一致。所以我们可以猜测题目中的代码,也属于并行执行的一种实现。
在对这个思考进行验证前,我们先看下forEach和for循环的区别。
for循环的特点
- 通过
break;
终止整个循环; - 通过
continue;
跳过本次循环,继续执行后边的循环; - 直接在for循环里写return;会报错,因为return不能脱离函数作用域,return是对函数执行结果的返回;
forEach的特点
- 不能执行
break;
和continue;
,因为这两个是对块作用域(不仅仅是for产生的块作用域,switch case也有类似的操作); return
是对最直接一级的函数执行结果进行返回,forEach
的参数就是一个函数,所以return
的行为,会类似for循环里的continue;
;forEach一定会迭代完整个可迭代对象(含有迭代器的对象),不可中断迭代过程。forEach
对于数组中的empty
值,是不会进行处理的,不会进入对应的循环体,所以循环次数不会因为empty值而增加。而for循环,一般是根据数组的length值来确定循环次数,所以会进入empty值对应的循环。这一点在后边可以明确
一般forEach不可中止迭代,元素有n个,一定会循环n次。但可以通过try catch实现中止,如
try{
[1,2,3,4,6].forEach((v, i) => {
if(i === 2){
throw new Error('跳出')
}
console.log(v)
});
}catch(e){console.log(e)}
复制代码
数组的empty值
有以下方法可以产生empty值:
const a = Array(5);//(5)[empty × 5]
const b = [1,,3,,,];//(5)[1, empty, 3, empty × 2]
复制代码
在控制台的打印值分别如下
这两个数组的length属性都为5,但empty值对应的位置是没有相应的数字属性的。如果访问a[0]
会得到undefined
。
但Array(5)
和[undefined,undefined,undefined,undefined,undefined]
是不一样的,后者的控制台打印是
他们的属性数量是不一样的。
forEach的手写实现
这是自己的用for循环实现的forEach
Array.prototype.myForEach = function(fn) {
const len = this.length;
const tempArr = Object.keys(this);//可枚举的常规属性集合
for(let i =0;i<len;i++) {
if(tempArr.includes(i+'')){
//不是empty的值,才执行到
fn(this[i], i, this)
}
}
}
复制代码
其中Object.keys(this)获取数组的可枚举的常规属性,includes方法检查对应的索引值,是否在这个集合里。关于对象属性遍历的方法,可参考遍历对象方法总结及思考
另外也可以通过Object.entries
和for of
得到更简单的实现
Array.prototype.myForEach = function(fn) {
const tempArr = Object.entries(this);
for(const v of tempArr){
fn(v[1], v[0], this)
}
}
复制代码
因为Object.entries()
方法返回一个给定对象自身可枚举属性的键值对数组,而empty是不存在的,所以不会被包含在内。这样的实现和上边的实现区别在于,上边的会执行规定的this.length次的循环,而Object.entries()得到的值只包含可枚举的项,这个长度会小于等于this.length。
分解结果
从前边的手写forEach可以看出,forEach的参数(一个匿名函数),题目中是如下函数
async (v) => {
let res = await YB(v);
console.log(res);
}
复制代码
这个函数中异步代码的执行完毕与否,不会影响for循环的执行,如下,fn即是要执行的函数
for(let i =0;i<len;i++) {
if(tempArr.includes(i+'')){
//不是empty的值,才执行到
fn(this[i], i, this)
}
}
复制代码
这个循环过程都是在同步代码中执行的,虽然fn中包含异步执行的代码,但这个循环的结束和fn的中的异步代码执行完毕并没有关系,只要fn中的同步代码执行完毕即可(因为同步代码都在主线程里执行)。这个for循环执行的时间很短,可以认为是同时完成。new Promise和setTimeout的进入事件队列,都是在同步代码中完成的,和for循环一样都是在js的主线程中顺序执行。
到这里,我们最开始遇到的题目,便可看出,也是一种并行执行的实现,结果也就容易理解了。
笔者之所以没有答对,还是因为基础知识没复习好。并行执行和串行执行,在高程里都有详细的解答。
面试中不懂的问题记录
美团面试中其他的问题如下,
自己认为回答的好的问题:
- webpack打包对文件进行hash值处理的原因
- 介绍下浏览器缓存
其他的记得不是很清楚了。
最后列举自己回答的不好的几个问题,也是后面学习要逐个弄明白的知识点,如果读者感兴趣,也可以帮忙解答下
- flex: 1;的真实值是啥?
- vue3对diff算法的优化有哪些?
- vue中key的作用是啥,有什么弊端吗?
- 手写一个柯里化函数实现累加。(自己只说了思路,面试时不想去写)
- 如何对页面性能进行分析,首字节时间?
- vue-router路由切换的原理,onhashchange做了什么?
- app和h5交互中,app如何知道h5最内的变化?
- 对微前端的理解?
- webpack5的了解