代码输出题
前言
代码输出结果也是面试中常考的题目,一段代码中可能涉及到很多的知识点,这就考察到了应聘者的基础能力。在前端面试中,常考的代码输出问题主要涉及到以下知识点:异步编程、事件循环、this指向、作用域、变量提升、闭包、原型、继承等,这些知识点往往不是单独出现的,而是在同一段代码中包含多个知识点。所以,笔者将这些问题大致分为四类进行讨论。这里不会系统的阐述基础知识,而是通过面试例题的形式,来讲述每个题目的知识点以及代码的执行过程。如果会了这些例题,在前端面试中多数代码输出问题就可以轻而易举的解决了。
一、异步&事件循环
1. 代码输出结果
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
输出结果如下:
1
2
4
promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要promise内部的状态发生变化,因为这里内部没有发生变化,一直处于pending状态,所以不输出3。
2. 代码输出结果
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
输出结果如下:
promise1
1 Promise{<resolved>: resolve1}
2 Promise{<pending>}
resolve1
需要注意的是,直接打印promise1,会打印出它的状态值和参数。
代码执行过程如下:
- script是一个宏任务,按照顺序执行这些代码;
- 首先进入Promise,执行该构造函数中的代码,打印
promise1; - 碰到
resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来; - 碰到
promise1.then这个微任务,将它放入微任务队列; promise2是一个新的状态为pending的Promise;- 执行同步代码1, 同时打印出
promise1的状态是resolved; - 执行同步代码2,同时打印出
promise2的状态是pending; - 宏任务执行完毕,查找微任务 队列,发现
promise1.then这个微任务且状态为resolved,执行它。
3. 代码输出结果
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
输出结果如下:
1
2
4
timerStart
timerEnd
success
代码执行过程如下:
- 首先遇到Promise构造函数,会先执行里面的内容,打印 1;
- 遇到定时器
steTimeout,它是一个宏任务,放入宏任务队列; - 继续向下执行,打印出2;
- 由于
Promise的状态此时还是pending,所以promise.then先不执行; - 继续执行下面的同步任务,打印出4;
- 此时微任务队列没有任务,继续执行下一轮宏任务,执行
steTimeout; - 首先执行
timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列,再执行timerEnd; - 执行完这个宏任务,就去执行微任务
promise.then,打印出resolve的结果。
4. 代码输出结果
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
输出结果如下:
start
promise1
timer1
promise2
timer2
代码执行过程如下:
- 首先,
Promise.resolve().then是一个微任务,加入微任务队列 - 执行timer1,它是一个宏任务,加入宏任务队列
- 继续执行下面的同步代码,打印出
start - 这样第一轮宏任务就执行完了,开始执行微任务
Promise.resolve().then,打印出promise1 - 遇到
timer2,它是一个宏任务,将其加入宏任务队列,此时宏任务队列有两个任务,分别是timer1、timer2; - 这样第一轮微任务就执行完了,开始执行第二轮宏任务,首先执行定时器
timer1,打印timer1; - 遇到
Promise.resolve().then,它是一个微任务,加入微任务队列 - 开始执行微任务队列中的任务,打印
promise2; - 最后执行宏任务
timer2定时器,打印出timer2;
5. 代码输出结果
const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise.then((res) => {
console.log('then:', res);
}).catch((err) => {
console.log('catch:', err);
})
输出结果如下:
then:success1
这个题目考察的就是Promise的状态在发生变化之后,就不会再发生变化。开始状态由 pending变为 resolve,说明已经变为已完成状态,下面的两个状态的就不会再执行,同时下面的catch也不会捕获到错误。
6. 代码输出结果
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输出结果如下:
1
Promise {<fulfilled>: undefined}
Promise.resolve方法的参数如果是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved,Promise.resolve方法的参数,会同时传给回调函数。
then方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为then(null),这就会导致前一个Promise的结果会传递下面。
7. 代码输出结果
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
输出结果如下:
promise1 Promise {<pending>}
promise2 Promise {<pending>}
Uncaught (in promise) Error: error!!!
promise1 Promise {<fulfilled>: "success"}
promise2 Promise {<rejected>: Error: error!!}
8. 代码输出结果
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
输出结果如下:
1
2
Promise是可以链式调用的,由于每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般任务的链式调用一样return this。
上面的输出结果之所以依次打印出1和2,是因为 resolve(1)之后走的是第一个then方法,并没有进catch里,所以第二个then中的res得到的实际上是第一个then的返回值。并且return 2会被包装成 resolve(2),被最后的then打印输出2。
9. 代码输出结果
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})
输出结果如下:
"then: " "Error: error!!!"
返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的 return new Error('error!!!')也被包裹成了 return Promise.resolve(new Error('error!!!')),因此它会被then捕获而不是catch。
10. 代码输出结果
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch(console.err)
输出结果如下:
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
这里其实是一个坑,.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
11. 代码输出结果
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
输出结果如下:
1
看到这个题目,好多的then,实际上只需要记住一个原则:.then 或 .catch 的参数期望是函数,传入非函数则会发生值传透。
第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将 resolve(1) 的值直接传到最后一个then里,直接打印出1。
12. 代码输出结果
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
输出结果如下:
error err!!!
我们知道,.then函数中的两个参数:
- 第一个参数是用来处理Promise成功的函数
- 第二个则是处理失败的函数
也就是说 Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。
在这道题中,错误直接被 then的第二个参数捕获了,所以就不会被 catch捕获了,输出结果为:error err!!!'
但是,如果是像下面这样:
Promise.resolve()
.then(function success (res) {
throw new Error('error!!!')
}, function fail1 (err) {
console.log('fail1', err)
}).catch(function fail2 (err) {
console.log('fail2', err)
})
在 then的第一参数中抛出了错误,那么他就不会被第二个参数不活了,而是被后面的 catch捕获到。
13. 代码输出结果
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
输出结果如下:
1
finally2
finally
finally2后面的then函数 2
.finally()一般用的很少,只要记住以下几点就可以了:
.finally()方法不管Promise对象最后的状态如何都会执行.finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是无法知道Promise最终的状态是resolved还是rejected的- 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
- finally本质上是then方法的特例
.finally()的错误捕获:
Promise.resolve('1')
.finally(() => {
console.log('finally1')
throw new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
输出结果为:
'finally1'
'捕获错误' Error: 我是finally中抛出的异常
14. 代码输出结果
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
输出结果如下:
1
2
3
[1, 2, 3]
首先,定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,然后间隔一秒后打印出这个x。
之后再使用 Promise.all来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。
15. 代码输出结果
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
输出结果如下:
// 1s后输出
1
3
// 2s后输出
2
Error: 2
// 4s后输出
4
可以看到。catch捕获到了第一个错误,在这道题目中最先的错误就是 runReject(2)的结果。如果一组异步操作中有一个异常都不会进入 .then()的第一个回调函数参数中。会被 .then()的第二个回调函数捕获。
16. 代码输出结果
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log('result: ', res))
.catch(err => console.log(err))
输出结果如下:
1
'result: ' 1
2
3
then只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被then捕获了。
17. 代码输出结果
function runAsync(x) {
const p = new Promise(r =>
setTimeout(() => r(x, console.log(x)), 1000)
);
return p;
}
function runReject(x) {
const p = new Promise((res, rej) =>
setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
);
return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log("result: ", res),rej=>console.log("reject",rej))
.catch(err => console.log("catch",err));
输出结果如下:
0
Error: 0
1
2
3
可以看到在catch捕获到第一个 错误之后,后面的代码还不执行,不过不会再被捕获了。
注意:all和 race传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。
18. 代码输出结果
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
输出结果如下:
async1 start
async2
start
async1 end
代码的执行过程如下:
- 首先执行函数中的同步代码
async1 start,之后遇到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1; - 跳出
async1函数后,执行同步代码start; - 在一轮宏任务全部执行完之后,再来执行
await后面的内容async1 end。
这里可以理解为await后面的语句相当于放到了new Promise中, 下一行及之后的语句相当于放在Promise.then中。
19. 代码输出结果
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")
输出结果如下:
async1 start
async2
start
async1 end
timer2
timer3
timer1
代码的执行过程如下:
- 首先进入
async1,打印出async1 start; - 之后遇到
async2,进入async2,遇到定时器timer2,加入宏任务队列,之后打印async2; - 由于
async2阻塞了后面代码的执行,所以执行后面的定时器timer3,将其加入宏任务队列,之后打印start; - 然后执行
async2后面的代码,打印出async1 end,遇到定时器timer1,将其加入宏任务队列; - 最后,宏任务队列有三个任务,先后顺序为
timer2,timer3,timer1,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。
20. 代码输出结果
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
输出结果如下:
script start
async1 start
promise1
script end
这里需要注意的是在 async1中 await后面的Promise是没有返回值的,也就是它的状态始终是 pending状态,所以在 await之后的内容是不会执行的,包括 async1后面的 .then。
21. 代码输出结果
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise1 resolve')
}).then(res => console.log(res))
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
这里是对上面一题进行了改造,加上了resolve。
输出结果如下:
script start
async1 start
promise1
script end
promise1 resolve
async1 success
async1 end
22. 代码输出结果
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(resolve => {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')
输出结果如下:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
代码执行过程如下:
- 开头定义了async1和async2两个函数,但是并未执行,执行script中的代码,所以打印出script start;
- 遇到定时器Settimeout,它是一个宏任务,将其加入到宏任务队列;
- 之后执行函数async1,首先打印出async1 start;
- 遇到await,执行async2,打印出async2,并阻断后面代码的执行,将后面的代码加入到微任务队列;
- 然后跳出async1和async2,遇到Promise,打印出promise1;
- 遇到resolve,将其加入到微任务队列,然后执行后面的script代码,打印出script end;
- 之后就该执行微任务队列了,首先打印出async1 end,然后打印出promise2;
- 执行完微任务队列,就开始执行宏任务队列中的定时器,打印出setTimeout。
23. 代码输出结果
async function async1 () {
await async2();
console.log('async1');
return 'async1 success'
}
async function async2 () {
return new Promise((resolve, reject) => {
console.log('async2')
reject('error')
})
}
async1().then(res => console.log(res))
输出结果如下:
async2
Uncaught (in promise) error
可以看到,如果async函数中抛出了错误,就会终止错误结果,不会继续向下执行。
如果想要让错误不足之处后面的代码执行,可以使用catch来捕获:
async function async1 () {
await Promise.reject('error!!!').catch(e => console.log(e))
console.log('async1');
return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')
这样的输出结果就是:
script start
error!!!
async1
async1 success
24. 代码输出结果
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);
输出结果如下:
3
7
4
1
2
5
Promise{<resolved>: 1}
代码的执行过程如下:
- 首先会进入Promise,打印出3,之后进入下面的Promise,打印出7;
- 遇到了定时器,将其加入宏任务队列;
- 执行Promise p中的resolve,状态变为resolved,返回值为1;
- 执行Promise first中的resolve,状态变为resolved,返回值为2;
- 遇到p.then,将其加入微任务队列,遇到first().then,将其加入任务队列;
- 执行外面的代码,打印出4;
- 这样第一轮宏任务就执行完了,开始执行微任务队列中的任务,先后打印出1和2;
- 这样微任务就执行完了,开始执行下一轮宏任务,宏任务队列中有一个定时器,执行它,打印出5,由于执行已经变为resolved状态,所以
resolve(6)不会再执行; - 最后
console.log(p)打印出Promise{<resolved>: 1};
25. 代码输出结果
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
输出结果如下:
script start
async1
promise1
script end
1
timer2
timer1
代码的执行过程如下:
- 首先执行同步带吗,打印出script start;
- 遇到定时器timer1将其加入宏任务队列;
- 之后是执行Promise,打印出promise1,由于Promise没有返回值,所以后面的代码不会执行;
- 然后执行同步代码,打印出script end;
- 继续执行下面的Promise,.then和.catch期望参数是一个函数,这里传入的是一个数字,因此就会发生值渗透,将resolve(1)的值传到最后一个then,直接打印出1;
- 遇到第二个定时器,将其加入到微任务队列,执行微任务队列,按顺序依次执行两个定时器,但是由于定时器时间的原因,会在两秒后先打印出timer2,在四秒后打印出timer1。
26. 代码输出结果
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve('resolve3');
console.log('timer1')
}, 0)
resolve('resovle1');
resolve('resolve2');
}).then(res => {
console.log(res) // resolve1
setTimeout(() => {
console.log(p1)
}, 1000)
}).finally(res => {
console.log('finally', res)
})
执行结果为如下:
resolve1
finally undefined
timer1
Promise{<resolved>: undefined}
27. 代码输出结果
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
输出结果如下:
1
7
6
8
2
4
3
5
9
11
10
12
(1)第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到
console.log,输出1。 - 遇到
setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1。 - 遇到
process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1。 - 遇到
Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1。 - 又遇到了
setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout1 | process1 |
| setTimeout2 | then1 |
上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了 process1和 then1两个微任务:
- 执行
process1,输出6。 - 执行
then1,输出8。
第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。
(2)第二轮时间循环从 setTimeout1宏任务开始:
- 首先输出2。接下来遇到了
process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。 new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout2 | process2 |
| then2 |
第二轮事件循环宏任务结束,发现有 process2和 then2两个微任务可以执行:
- 输出3。
- 输出5。
第二轮事件循环结束,第二轮输出2,4,3,5。
(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将
process.nextTick()分发到微任务Event Queue中。记为process3。 - 直接执行
new Promise,输出11。 - 将
then分发到微任务Event Queue中,记为then3。
| 宏任务Event Queue |
|---|