经典输出题
⭐1.setTimeout 循环输出问题
一.用闭包解决
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(i);
}这个代码使用了 闭包 来正确输出按顺序递增的数字,下面我们来逐步分析:
1. 为什么这是闭包?
闭包是指 函数内部定义的函数 能够访问其外部函数的作用域。即使外部函数已经执行完毕,内部函数依然可以访问外部函数的变量。
在你给出的代码中,(function(j) {...})(i) 是一个立即执行的函数表达式(IIFE)。它创建了一个 新的作用域,并将 i 的值传递给了内部函数的参数 j。然后 setTimeout 内部的回调函数在延时后执行时,能够访问到 j 的值,因为它是通过闭包机制保存的。
2. 闭包是什么意思?
闭包的定义:
闭包是在 函数 被定义的时候,函数内部 可以访问 外部函数的变量。
闭包可以 "记住" 外部函数的变量,即使外部函数已经执行完毕,闭包仍然能够访问并使用这些变量。
举个简单的例子:
function outer() {
var outerVar = "I am outer";
function inner() {
console.log(outerVar); // inner函数可以访问outer函数的变量outerVar
}
return inner;
}
const closureExample = outer(); // outer函数执行完毕后,返回的inner函数仍然可以访问outerVar
closureExample(); // 输出: I am outer3. 为什么这个输出是正确的?
在你的代码中,闭包的作用是通过立即执行函数表达式(IIFE)来创建一个新的作用域,并将每次循环的 i 的值传递给 j。这个 j 就是 setTimeout 回调函数中的局部变量,它会被正确地保留,即使在 setTimeout 执行时,外部的 i 变量已经不再有效。
具体执行过程:
for循环依次将i从 1 到 5 传递给立即执行的匿名函数。每次匿名函数执行时,传入的
i被作为参数j保存下来,并且在闭包中被正确引用。setTimeout在j * 1000毫秒后执行回调函数,这时回调函数中的j是一个已被捕获的固定值(通过闭包),而不是循环结束后i的值。
这样,即使 i 的值在循环结束后已经变为 6,每个 setTimeout 中的回调函数仍然能正确输出按顺序的数字(1, 2, 3, 4, 5)。
4. 闭包有什么用处?
数据隐私和封装:闭包能够将私有变量和方法封装在函数内部,外部无法直接访问,提供数据保护。
函数工厂:通过闭包可以创建动态生成的函数,这些函数可以根据传入的参数创建并返回新的函数。
延迟执行:闭包使得你可以在异步操作中捕获函数执行时的上下文,如
setTimeout或setInterval。记住状态:闭包能记住外部函数的变量并在以后再次使用。例如,计数器函数。
5. 总结
在你的例子中,闭包确保了 setTimeout 回调中的 j 值是按预期捕获并保留的。因此,每次 setTimeout 执行时,都会打印正确的数字(1 到 5),而不是重复打印相同的值。
如果没有闭包机制,那么 setTimeout 中的回调函数会共享同一个 i 变量的值,在循环结束后,所有的回调函数都会输出 i = 6。
二.用let解决
1. for 循环的执行
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(i)这里的
let i = 0是使用了块级作用域,也就是说i的作用域仅限于for循环内部的每次迭代。每次循环,i都是一个新的局部变量,保证了每次setTimeout中使用的i是不同的。循环会立刻进行,而不需要等待
setTimeout执行。
2. setTimeout 异步执行
setTimeout 会在延迟时间(这里是 1000 毫秒)后异步执行回调函数。回调函数是在 1000 毫秒后执行的,但是 for 循环会立刻继续执行,而不会被阻塞。
3. console.log(i) 在 setTimeout 中
在每次迭代时,
setTimeout的回调函数会捕获并使用当前i的值,并且因为let使得每次循环的i是一个新的局部变量,因此在每个回调函数中,i的值是正确的。1000 毫秒后,每个回调函数按照它们的顺序依次打印出
0, 1, 2, 3, 4。
4. console.log(i) 在循环外
console.log(i);这行代码在 for 循环外部执行,且它打印的是 i 的最终值。因为 i 是用 let 声明的,在 for 循环结束后,i 的值会是 5(退出循环时的值),所以这行代码会在控制台打印 5。
总结:
由于
let具有块级作用域,每次循环的i是独立的,所以setTimeout中的回调函数能够正确地访问到每次循环时的i。console.log(i)在循环外打印的是i的最终值,即5。
三.用定时器第三个参数解决
for (var i = 0; i < 5; i++) {
setTimeout(function(i) {
console.log(i);
}, 1000,i);
}
console.log(i);这个代码之所以能正确打印出 0, 1, 2, 3, 4,是因为 setTimeout 的回调函数现在接收了传入的参数 i,而这个 i 是在每次调用 setTimeout 时从 for 循环的当前 i 传递过来的。我们来逐步分析一下:
for (var i = 0; i < 5; i++) {
setTimeout(function(i) {
console.log(i);
}, 1000, i);
}
console.log(i);1. setTimeout 的第三个参数
setTimeout的第三个参数i被传递给了回调函数。这个参数是立即传递给回调函数的,而不是在回调函数内部访问外部的i变量。由于每次循环时,
setTimeout被调用时都将当前的i作为参数传入回调函数,因此每个回调函数内的i是循环中当时的值。
2. 回调函数的执行
- 当
setTimeout被调用时,它会在 1000 毫秒后才执行回调函数,但因为传递给回调的i是当前迭代中的i,所以每个setTimeout的回调函数会正确地使用当时的i值,而不再共享同一个变量。
例如:
在第一次迭代时,
i为0,setTimeout会将i的值0传递给回调函数。在第二次迭代时,
i为1,setTimeout会将i的值1传递给回调函数。依此类推,直到
i为4。
因此,打印出来的 i 值是从 0 到 4,而不是最后的 5。
3. console.log(i) 在循环外
console.log(i)会在for循环结束后立即执行。由于i是用var声明的,它的作用域是全局的(或者函数作用域),因此在循环结束时,i的值是5。所以,这行代码会打印出5。
为什么这个代码能正常工作:
通过给
setTimeout的回调函数传递参数i,每次回调函数接收到的是当前i的值,而不是回调函数执行时的i(for循环中最后的i)。这样做避免了使用
var时,多个异步回调共享同一个变量i的问题。
总结:
回调函数接收参数:通过将当前的
i作为setTimeout的第三个参数传递给回调函数,确保了每个回调函数都能够访问到各自的i值,而不是在for循环结束时的i(即 5)。打印的结果:最终打印出
0, 1, 2, 3, 4,并且最后打印5,因为i在循环结束后被更新为 5。
⭐2.事件循环笔试题
1.代码分析:
console.log("script start");
setTimeout(() => {
console.log("settimeout");
}, 0);
let pro = new Promise(res => {
console.log("promise1");
res("promise2");
});
pro.then(res => {
console.log(res);
});
console.log("script end");同步执行:
console.log("script start")会立刻打印。setTimeout()是一个异步操作,但因为延时设置为0,它会在当前执行栈清空后立即执行,所以它会稍后打印"settimeout"。
Promise 执行:
let pro = new Promise(...)会立即执行构造函数中的代码(即同步部分)。所以"promise1"会立刻打印出来。res("promise2")触发了Promise的成功状态,并把"promise2"传递给了then()中的回调函数。
then** 方法:**pro.then(res => {...})这个回调是异步的,会在当前执行栈清空后执行。虽然Promise已经解决,但它会在所有同步代码执行完后才会被调用,所以"promise2"会在"script end"之后打印。
最终输出顺序:
"script start""promise1""script end""promise2""settimeout"
总结:
new Promise() 是用来创建一个表示异步操作结果的 Promise 对象的构造函数。它的主要作用是处理异步代码,允许你使用 .then() 来处理异步成功的结果。
这段代码的输出顺序有点复杂,因为它涉及到 宏任务(setTimeout)和 微任务(Promise 和 then)的执行顺序。我们可以按执行栈和事件循环的规则一步步解析这个问题。
2.代码分析
console.log("script start");
setTimeout(() => {
console.log("settimeout1");
}, 0);
let pro = new Promise(res => {
console.log("promise1");
res("promise2");
});
let pro3 = new Promise(res => {
console.log("promise3");
res("promise4");
});
pro.then(res => {
console.log(res);
setTimeout(() => {
console.log("settimeout3");
pro3.then(res => {
console.log(res);
});
}, 0);
});
console.log("script end");
script start ,promise1, promise3,script end, promise2,settimeout1,settiout3,promise4执行流程
执行栈的同步任务:
console.log("script start")被立即执行,打印"script start"。
遇到 **
setTimeout**(宏任务):setTimeout()被调用,虽然延迟为0,但它会被放入 宏任务队列,等到当前执行栈空闲时才会执行。
创建 **
Promise** 对象 **pro**:let pro = new Promise(...)会立即执行其构造函数中的同步代码,打印"promise1"。执行
res("promise2")后,pro变成了已解决的状态,但then()里的回调会被放入 微任务队列,等当前任务完成后再执行。
创建 **
Promise** 对象 **pro3**:let pro3 = new Promise(...)同样会立即执行其构造函数中的同步代码,打印"promise3"。执行
res("promise4")后,pro3变成了已解决的状态,但是它的then()调用被放到微任务队列中,等后面的pro.then()里回调执行完再执行。
同步执行结束:
console.log("script end")被立即执行,打印"script end"。
执行微任务:
微任务队列中的任务会在当前执行栈清空后立刻执行。此时,微任务队列里有
pro.then()的回调。pro.then(res => {...})执行,打印"promise2"(pro被解析为"promise2")。然后,
setTimeout()被调用,并放入宏任务队列,等待下一个事件循环。
执行宏任务 **
setTimeout**(第一个):- 宏任务
setTimeout(() => {...}, 0)被执行,打印"settimeout1"。
- 宏任务
执行微任务:
- 在
pro3.then()中的回调被放入微任务队列,等待执行。它的回调函数会在当前执行栈空闲后执行。
- 在
执行宏任务 **
setTimeout**(第二个):第二个
setTimeout(() => {...}, 0)被执行,打印"settimeout3"。在
settimeout3内部,再次使用了pro3.then(),因此,pro3的微任务会加入到微任务队列中,打印"promise4"。
最终输出:
script start
promise1
promise3
script end
promise2
settimeout1
settimeout3
promise4解释“异步宏任务套微任务”:
在 pro.then() 回调中,你使用了 setTimeout。这个 setTimeout 被放入 宏任务队列。但是,宏任务内部包含的 pro3.then() 还是一个微任务,它会被放入 微任务队列,而微任务队列的任务会在当前栈执行完毕后立即执行。
微任务(**
pro.then()**) 被执行完,触发了第二个setTimeout(宏任务)。在宏任务内部,再次创建了微任务(**
pro3.then()**),并且这部分微任务将在本轮宏任务后执行。
这种情况体现了 宏任务中套微任务 的执行方式,先执行宏任务中的代码,随后将微任务加入队列,等宏任务完成后,微任务会立即执行。
总结:
宏任务(如
setTimeout)和 微任务(如Promise)的执行顺序是:宏任务队列中的任务在微任务队列清空后才会执行。微任务(
then)会在宏任务(setTimeout)之前执行,确保所有的微任务都执行完毕后,才会执行下一个宏任务。
3.问题分析
let pro = ()=>{
new Promise(res=>{
res(5)
})
pro().then(res1=>{
console.log(res1)
})
}
pro().then(res2=>{
console.log(res2)
})1. 变量作用域问题
let pro = () => {
new Promise(res => {
res(5)
})
pro().then(res1 => { // ❌ 这里递归调用,但 pro 尚未完全定义
console.log(res1)
})
}pro 是一个箭头函数,但在函数体内又调用了 pro(),这会导致:
无限递归:每次调用
pro()都会创建新的 Promise,并再次调用pro()栈溢出:没有终止条件
2. 缺少返回值
let pro = () => {
new Promise(res => {
res(5)
}) // ❌ 这个 Promise 没有被返回
// ...
}箭头函数 pro 没有 return 语句,所以 pro() 返回 undefined
3. 最后的调用
pro().then(res2 => { // ❌ pro() 返回 undefined,无法调用 .then()
console.log(res2)
})修正后的代码
// 正确版本1:返回 Promise
let pro = () => {
return new Promise(res => {
res(5)
})
}
pro().then(res1 => {
console.log(res1) // 输出: 5
})
pro().then(res2 => {
console.log(res2) // 输出: 5
})或者如果要在内部链式调用:
// 正确版本2:内部链式调用
let pro = () => {
return new Promise(res => {
res(5)
}).then(res1 => {
console.log('内部:', res1) // 输出: 内部: 5
return res1
})
}
pro().then(res2 => {
console.log('外部:', res2) // 输出: 外部: 5
})疑问
let pro = () => {
return new Promise((res) => { // ← 执行到这里就返回了
res(5);
});
// 下面的代码永远不会执行,因为上面已经 return 了
pro().then((res1) => {
console.log(res1);
});
};同步调用和异步调用
// 场景A:同步递归(会栈溢出)
let pro = () => {
pro() // ← 直接调用自己,立即执行
}
pro() // 💥 栈溢出
// 场景B:异步递归(不会溢出)
let pro = () => {
Promise.resolve().then(() => {
pro() // ← 在回调中调用自己,延迟执行
})
}
pro() // ✅ 正常运行4.执行顺序分析
关键点:定时器的触发时机
虽然所有定时器都设置了 0ms 延迟,但它们的开始计时的时间点不同:
async function async1() {
console.log("async1 start"); // 1
await async2(); // 调用 async2
console.log("async1 end"); // 4 (微任务)
setTimeout(() => {
console.log('timer1') // 7 (最后一个宏任务)
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2') // 5 (第一个宏任务)
}, 0)
console.log("async2"); // 2
}
async1();
setTimeout(() => {
console.log('timer3') // 6 (第二个宏任务)
}, 0)
console.log("start") // 3
async1 start, async2,start,1执行时间线
同步代码执行阶段(时刻 0ms):
async1()开始执行输出
"async1 start"调用
async2():设置 timer2 定时器(此时开始计时 0ms)
输出
"async2"
await将后续代码(console.log("async1 end")和设置 timer1)加入微任务队列设置 timer3 定时器(此时开始计时 0ms)
输出
"start"
微任务执行阶段(约 0ms 后):
执行
console.log("async1 end"),输出"async1 end"设置 timer1 定时器(此时才开始计时 0ms)
宏任务执行阶段(根据定时器完成计时的顺序):
timer2:在同步代码阶段就设置了,计时完成最早 ✅
timer3:也在同步代码阶段设置,与 timer2 几乎同时完成,但浏览器可能按设置顺序执行
timer1:在微任务阶段才设置,计时完成最晚
正确输出
async1 start
async2
start
async1 end
timer2
timer3
timer1为什么会这样?
timer2 和 timer3 都在同步代码阶段设置,理论上它们的计时起点相同
浏览器在处理多个同时到期的定时器时,通常按照设置顺序执行
但在这个例子中,由于 async2 先执行,所以 timer2 比 timer3 更早被添加到任务队列
timer1 因为是在微任务阶段才设置,所以即使也是 0ms 延迟,实际上已经晚了几个事件循环周期
注意:async包裹的函数可以当作同步代码执行,直到遇到await跳出,执行函数外部的同步代码async2和外部的console.log,再跳回执行await后的同步代码,最后根据压入任务队列的顺序执行宏任务
额外注意的是:
async包裹的函数fn(),使用await接收时不一定需要fn()具有return。
如果函数定义return则返回return的输出,不然async语法会默认返回一个undefined,也就是async+await的函数一定会有返回值,其promise的状态只有fulfilled和rejected两种,不存在pending。
为什么需要注意这一点是因为,await接收的函数只有存在返回值的情况下才会执行后续的代码,不然会进行截断,代码会在await处结束。具体见下例
没有resolve带来的结果
async function async1() {
console.log('async1 start'); // ① 同步
await new Promise(resolve => {
console.log('promise1'); // ② 同步(Promise 执行器立即执行)
}); // 这个 Promise 永远不会 resolve
console.log('async1 success'); // 永远不会执行
return 'async1 end';
}
console.log('script start'); // ③ 同步
async1().then(res => console.log(res)); // ④ 调用 async1
console.log('script end'); // ⑤ 同步逐步执行
1. 同步代码阶段
执行
console.log('script start')→ 输出script start调用
async1():输出
async1 start遇到
await new Promise(...)Promise 执行器立即执行:输出
promise1这个 Promise 没有调用 **
resolve()**,所以状态永远为 pending
await会让 async1 函数暂停,并返回一个 pending 状态的 Promise(给async1().then(...)的那个 Promise)await后面的代码(console.log('async1 success')和return)永远不会被放入微任务队列(因为 Promise 永远不会 resolved)
async1().then(res => console.log(res)):这里的
then是挂在async1()返回的那个 Promise 上的由于 async1 内部 Promise 永远不会 resolve,这个 then 的回调也永远不会执行
输出
script end
此时同步代码执行完毕,输出:
script start
async1 start
promise1
script end2. 微任务阶段
当前微任务队列里 没有任务(因为 await 的那个 Promise 没有 resolve,所以没有微任务被加入)
事件循环继续,但没有任何 pending 的微任务
3. 后续
程序没有其他宏任务,也没有微任务
进程空闲,但不会再有输出
最终输出
script start
async1 start
promise1
script end注意:async1 success 和 async1 end 都不会输出,因为 Promise 永远不会被 resolve,await 后面的代码永远不会被执行。
修正
async function async1() {
console.log('async1 start'); // ① 同步
await new Promise(resolve => {
console.log('promise1'); // ② 同步(Promise 执行器立即执行)
resolve() //这加个resolve
});
console.log('async1 success');
return 'async1 end';
}
console.log('script start'); // ③ 同步
async1().then(res => console.log(res)); // ④ 调用 async1
console.log('script end'); // ⑤ 同步第一阶段:同步代码
输出
"script start"调用
async1():输出
"async1 start"进入
await new Promise(...):Promise 执行器立即执行,输出
"promise1"立即调用
resolve()
由于 Promise 被 resolve,
await后面的代码(console.log('async1 success')和return)被包装成微任务,加入微任务队列
async1().then(...)注册回调,这个回调也会被加入微任务队列(等待 async1 返回的 Promise 被解决)输出
"script end"
此时输出:
script start
async1 start
promise1
script end第二阶段:微任务执行
微任务队列中有两个任务(按加入顺序):
第一个微任务:执行
console.log('async1 success'),然后return 'async1 end',这会导致 async1 返回的 Promise 被解决- 输出
"async1 success"
- 输出
第二个微任务:执行
async1().then(res => console.log(res))的回调,参数是'async1 end'- 输出
"async1 end"
- 输出
最终输出:
async1 success
async1 end完整输出顺序
script start
async1 start
promise1
script end
async1 success
async1 end关键点
resolve/reject函数由 Promise 构造器传给 executor;可以在 executor 内调用,也可以被闭包捕获后由后续回调调用。关键是最终要调用其中之一或采用另一个 Promise 的状态,而不是要求调用语句必须位于 executor 的同步代码中await 后面的代码(包括 return)会被放入微任务队列
async1().then() 的回调也会被放入微任务队列,在 async1 返回的 Promise 解决后执行
两个微任务的执行顺序取决于它们被加入队列的顺序
代码输出题(字节)
window.name = 'window'
function Person(name) {
this.name = name;
}
Person.prototype.print = function() {
return this.name;
};
Person('abc');
const a = new Person('123').print.call({});
console.log(a);
const fn = () => {
this.name = 'z';
};
const b = {name: 'y'};
fn.call(b);
console.log(b.name);
const c = new Person('123').print
console.log(c())- 全局环境初始化
window.name = 'window'在浏览器中,全局对象是 window,所以 window.name 初始化为 'window'。
- Person 函数定义
function Person(name) {
this.name = name;
}
Person.prototype.print = function() {
return this.name;
};- 第一次调用 Person('abc')
Person('abc');没有 new,相当于普通函数调用。
在非严格模式下,this 指向全局对象 window。
因此 window.name 被赋值为 'abc'。
这一步后:window.name === 'abc'。
- 执行 new Person('123').print.call({})
const a = new Person('123').print.call({});· new Person('123') 创建一个对象,name = '123',并且有 print 方法(在原型上)。
· 取 print 方法,并调用 .call({}),传入一个空对象 {} 作为 this。
· print 方法内部 return this.name,这里 this 是 {},{} 没有 name 属性,所以返回 undefined。
· a 的值是 undefined。
输出:
undefined- 箭头函数中的 this
const fn = () => {
this.name = 'z';
};
const b = {name: 'y'};
fn.call(b);
console.log(b.name);· 箭头函数 fn 在定义时捕获 this,这里的 this 是外层作用域的 this。
· 在全局作用域下定义 fn,所以箭头函数内的 this 就是全局 this(浏览器中是 window)。
· .call(b) 无法改变箭头函数的 this,因为箭头函数忽略 call、apply、bind 的第一个参数。
· 所以 fn() 执行时,this.name = 'z' 实际上设置的是 window.name,而不是 b.name。
· 因此 b.name 仍为 'y',打印 'y'。
输出:
y- 最后一段
const c = new Person('123').print;
console.log(c());· new Person('123') 创建对象,name = '123',print 是它的原型方法。
· c 就是 print 函数本身,没有被绑定到那个 Person 实例。
· 调用 c():这是普通函数调用,this 指向全局 window(非严格模式)。
· 全局 window.name 在第 3 步被改为 'abc'。
· 所以 c() 返回 window.name,即 'abc'。
输出:
abc最终控制台输出顺序
undefined
y
abc