Skip to content

经典输出题

作者:青见春山
发表于:2026-07-21
字数统计:6141 字
预计阅读21分钟

⭐1.setTimeout 循环输出问题

一.用闭包解决

JavaScript
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. 闭包是什么意思?

闭包的定义:

  • 闭包是在 函数 被定义的时候,函数内部 可以访问 外部函数的变量

  • 闭包可以 "记住" 外部函数的变量,即使外部函数已经执行完毕,闭包仍然能够访问并使用这些变量。

举个简单的例子:

JavaScript
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 outer

3. 为什么这个输出是正确的?

在你的代码中,闭包的作用是通过立即执行函数表达式(IIFE)来创建一个新的作用域,并将每次循环的 i 的值传递给 j。这个 j 就是 setTimeout 回调函数中的局部变量,它会被正确地保留,即使在 setTimeout 执行时,外部的 i 变量已经不再有效。

具体执行过程:

  1. for 循环依次将 i 从 1 到 5 传递给立即执行的匿名函数。

  2. 每次匿名函数执行时,传入的 i 被作为参数 j 保存下来,并且在闭包中被正确引用。

  3. setTimeoutj * 1000 毫秒后执行回调函数,这时回调函数中的 j 是一个已被捕获的固定值(通过闭包),而不是循环结束后 i 的值。

这样,即使 i 的值在循环结束后已经变为 6,每个 setTimeout 中的回调函数仍然能正确输出按顺序的数字(1, 2, 3, 4, 5)。

4. 闭包有什么用处?

  • 数据隐私和封装:闭包能够将私有变量和方法封装在函数内部,外部无法直接访问,提供数据保护。

  • 函数工厂:通过闭包可以创建动态生成的函数,这些函数可以根据传入的参数创建并返回新的函数。

  • 延迟执行:闭包使得你可以在异步操作中捕获函数执行时的上下文,如 setTimeoutsetInterval

  • 记住状态:闭包能记住外部函数的变量并在以后再次使用。例如,计数器函数。

5. 总结

在你的例子中,闭包确保了 setTimeout 回调中的 j 值是按预期捕获并保留的。因此,每次 setTimeout 执行时,都会打印正确的数字(1 到 5),而不是重复打印相同的值。

如果没有闭包机制,那么 setTimeout 中的回调函数会共享同一个 i 变量的值,在循环结束后,所有的回调函数都会输出 i = 6

二.用let解决

1. for 循环的执行

JavaScript
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) 在循环外

JavaScript
console.log(i);

这行代码在 for 循环外部执行,且它打印的是 i 的最终值。因为 i 是用 let 声明的,在 for 循环结束后,i 的值会是 5(退出循环时的值),所以这行代码会在控制台打印 5

总结:

  • 由于 let 具有块级作用域,每次循环的 i 是独立的,所以 setTimeout 中的回调函数能够正确地访问到每次循环时的 i

  • console.log(i) 在循环外打印的是 i 的最终值,即 5

三.用定时器第三个参数解决

JavaScript

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 传递过来的。我们来逐步分析一下:

JavaScript
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 值,而不再共享同一个变量。

例如:

  • 在第一次迭代时,i0setTimeout 会将 i 的值 0 传递给回调函数。

  • 在第二次迭代时,i1setTimeout 会将 i 的值 1 传递给回调函数。

  • 依此类推,直到 i4

因此,打印出来的 i 值是从 0 到 4,而不是最后的 5。

3. console.log(i) 在循环外

  • console.log(i) 会在 for 循环结束后立即执行。由于 i 是用 var 声明的,它的作用域是全局的(或者函数作用域),因此在循环结束时,i 的值是 5。所以,这行代码会打印出 5

为什么这个代码能正常工作:

  • 通过给 setTimeout 的回调函数传递参数 i,每次回调函数接收到的是当前 i 的值,而不是回调函数执行时的 ifor 循环中最后的 i)。

  • 这样做避免了使用 var 时,多个异步回调共享同一个变量 i 的问题。

总结:

  • 回调函数接收参数:通过将当前的 i 作为 setTimeout 的第三个参数传递给回调函数,确保了每个回调函数都能够访问到各自的 i 值,而不是在 for 循环结束时的 i(即 5)。

  • 打印的结果:最终打印出 0, 1, 2, 3, 4,并且最后打印 5,因为 i 在循环结束后被更新为 5。

⭐2.事件循环笔试题

1.代码分析:

JavaScript
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");
  1. 同步执行:

    • console.log("script start") 会立刻打印。

    • setTimeout() 是一个异步操作,但因为延时设置为 0,它会在当前执行栈清空后立即执行,所以它会稍后打印 "settimeout"

  2. Promise 执行:

    • let pro = new Promise(...) 会立即执行构造函数中的代码(即同步部分)。所以 "promise1" 会立刻打印出来。

    • res("promise2") 触发了 Promise 的成功状态,并把 "promise2" 传递给了 then() 中的回调函数。

  3. then** 方法:**

    • pro.then(res => {...}) 这个回调是异步的,会在当前执行栈清空后执行。虽然 Promise 已经解决,但它会在所有同步代码执行完后才会被调用,所以 "promise2" 会在 "script end" 之后打印。
  4. 最终输出顺序:

    • "script start"

    • "promise1"

    • "script end"

    • "promise2"

    • "settimeout"

总结:

new Promise() 是用来创建一个表示异步操作结果的 Promise 对象的构造函数。它的主要作用是处理异步代码,允许你使用 .then() 来处理异步成功的结果。

这段代码的输出顺序有点复杂,因为它涉及到 宏任务setTimeout)和 微任务Promisethen)的执行顺序。我们可以按执行栈和事件循环的规则一步步解析这个问题。

2.代码分析

JavaScript
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

执行流程

  1. 执行栈的同步任务:

    • console.log("script start") 被立即执行,打印 "script start"
  2. 遇到 **setTimeout**(宏任务)

    • setTimeout() 被调用,虽然延迟为 0,但它会被放入 宏任务队列,等到当前执行栈空闲时才会执行。
  3. 创建 **Promise** 对象 **pro**:

    • let pro = new Promise(...) 会立即执行其构造函数中的同步代码,打印 "promise1"

    • 执行 res("promise2") 后,pro 变成了已解决的状态,但 then() 里的回调会被放入 微任务队列,等当前任务完成后再执行。

  4. 创建 **Promise** 对象 **pro3**:

    • let pro3 = new Promise(...) 同样会立即执行其构造函数中的同步代码,打印 "promise3"

    • 执行 res("promise4") 后,pro3 变成了已解决的状态,但是它的 then() 调用被放到微任务队列中,等后面的 pro.then() 里回调执行完再执行。

  5. 同步执行结束

    • console.log("script end") 被立即执行,打印 "script end"
  6. 执行微任务:

    • 微任务队列中的任务会在当前执行栈清空后立刻执行。此时,微任务队列里有 pro.then() 的回调。

    • pro.then(res => {...}) 执行,打印 "promise2"pro 被解析为 "promise2")。

    • 然后,setTimeout() 被调用,并放入宏任务队列,等待下一个事件循环。

  7. 执行宏任务 **setTimeout**(第一个)

    • 宏任务 setTimeout(() => {...}, 0) 被执行,打印 "settimeout1"
  8. 执行微任务:

    • pro3.then() 中的回调被放入微任务队列,等待执行。它的回调函数会在当前执行栈空闲后执行。
  9. 执行宏任务 **setTimeout**(第二个)

    • 第二个 setTimeout(() => {...}, 0) 被执行,打印 "settimeout3"

    • settimeout3 内部,再次使用了 pro3.then(),因此,pro3 的微任务会加入到微任务队列中,打印 "promise4"

最终输出:

Plain
script start
promise1
promise3
script end
promise2
settimeout1
settimeout3
promise4

解释“异步宏任务套微任务”:

pro.then() 回调中,你使用了 setTimeout。这个 setTimeout 被放入 宏任务队列。但是,宏任务内部包含的 pro3.then() 还是一个微任务,它会被放入 微任务队列,而微任务队列的任务会在当前栈执行完毕后立即执行。

  1. 微任务(**pro.then()**) 被执行完,触发了第二个 setTimeout(宏任务)。

  2. 在宏任务内部,再次创建了微任务(**pro3.then()**),并且这部分微任务将在本轮宏任务后执行。

这种情况体现了 宏任务中套微任务 的执行方式,先执行宏任务中的代码,随后将微任务加入队列,等宏任务完成后,微任务会立即执行。

总结:

  • 宏任务(如 setTimeout)和 微任务(如 Promise)的执行顺序是:宏任务队列中的任务在微任务队列清空后才会执行。

  • 微任务(then)会在宏任务(setTimeout)之前执行,确保所有的微任务都执行完毕后,才会执行下一个宏任务。

3.问题分析

JavaScript
let pro = ()=>{
  new Promise(res=>{
       res(5)
     })
       pro().then(res1=>{
     console.log(res1)
    })
    }
pro().then(res2=>{
 console.log(res2)
 })

1. 变量作用域问题

JavaScript
let pro = () => {
    new Promise(res => {
        res(5)
    })
    pro().then(res1 => {  // ❌ 这里递归调用,但 pro 尚未完全定义
        console.log(res1)
    })
}

pro 是一个箭头函数,但在函数体内又调用了 pro(),这会导致:

  • 无限递归:每次调用 pro() 都会创建新的 Promise,并再次调用 pro()

  • 栈溢出:没有终止条件

2. 缺少返回值

JavaScript
let pro = () => {
    new Promise(res => {
        res(5)
    })  // ❌ 这个 Promise 没有被返回
    // ...
}

箭头函数 pro 没有 return 语句,所以 pro() 返回 undefined

3. 最后的调用

JavaScript
pro().then(res2 => {  // ❌ pro() 返回 undefined,无法调用 .then()
    console.log(res2)
})

修正后的代码

JavaScript
// 正确版本1:返回 Promise
let pro = () => {
    return new Promise(res => {
        res(5)
    })
}

pro().then(res1 => {
    console.log(res1)  // 输出: 5
})

pro().then(res2 => {
    console.log(res2)  // 输出: 5
})

或者如果要在内部链式调用:

JavaScript
// 正确版本2:内部链式调用
let pro = () => {
    return new Promise(res => {
        res(5)
    }).then(res1 => {
        console.log('内部:', res1)  // 输出: 内部: 5
        return res1
    })
}

pro().then(res2 => {
    console.log('外部:', res2)  // 输出: 外部: 5
})

疑问

JavaScript
let pro = () => {
  return new Promise((res) => {  // ← 执行到这里就返回了
    res(5);
  });
  // 下面的代码永远不会执行,因为上面已经 return 了
  pro().then((res1) => {
    console.log(res1);
  });
};

同步调用和异步调用

JavaScript
// 场景A:同步递归(会栈溢出)
let pro = () => {
    pro()  // ← 直接调用自己,立即执行
}
pro()  // 💥 栈溢出

// 场景B:异步递归(不会溢出)
let pro = () => {
    Promise.resolve().then(() => {
        pro()  // ← 在回调中调用自己,延迟执行
    })
}
pro()  // ✅ 正常运行

4.执行顺序分析

关键点:定时器的触发时机

虽然所有定时器都设置了 0ms 延迟,但它们的开始计时的时间点不同:

JavaScript
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

执行时间线

  1. 同步代码执行阶段(时刻 0ms):

    • async1() 开始执行

    • 输出 "async1 start"

    • 调用 async2()

      • 设置 timer2 定时器(此时开始计时 0ms)

      • 输出 "async2"

    • await 将后续代码(console.log("async1 end") 和设置 timer1)加入微任务队列

    • 设置 timer3 定时器(此时开始计时 0ms)

    • 输出 "start"

  2. 微任务执行阶段(约 0ms 后):

    • 执行 console.log("async1 end"),输出 "async1 end"

    • 设置 timer1 定时器(此时才开始计时 0ms)

  3. 宏任务执行阶段(根据定时器完成计时的顺序):

    • timer2:在同步代码阶段就设置了,计时完成最早 ✅

    • timer3:也在同步代码阶段设置,与 timer2 几乎同时完成,但浏览器可能按设置顺序执行

    • timer1:在微任务阶段才设置,计时完成最晚

正确输出

Plain
async1 start
async2
start
async1 end
timer2
timer3
timer1

为什么会这样?

  • timer2timer3 都在同步代码阶段设置,理论上它们的计时起点相同

  • 浏览器在处理多个同时到期的定时器时,通常按照设置顺序执行

  • 但在这个例子中,由于 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带来的结果

JavaScript
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

此时同步代码执行完毕,输出:

Plain
script start
async1 start
promise1
script end
2. 微任务阶段
  • 当前微任务队列里 没有任务(因为 await 的那个 Promise 没有 resolve,所以没有微任务被加入)

  • 事件循环继续,但没有任何 pending 的微任务

3. 后续
  • 程序没有其他宏任务,也没有微任务

  • 进程空闲,但不会再有输出

最终输出
Plain
script start
async1 start
promise1
script end

注意async1 successasync1 end 都不会输出,因为 Promise 永远不会被 resolve,await 后面的代码永远不会被执行。

修正

JavaScript
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');                           // ⑤ 同步
第一阶段:同步代码
  1. 输出 "script start"

  2. 调用 async1()

    • 输出 "async1 start"

    • 进入 await new Promise(...)

      • Promise 执行器立即执行,输出 "promise1"

      • 立即调用 resolve()

    • 由于 Promise 被 resolve,await 后面的代码(console.log('async1 success')return)被包装成微任务,加入微任务队列

  3. async1().then(...) 注册回调,这个回调也会被加入微任务队列(等待 async1 返回的 Promise 被解决)

  4. 输出 "script end"

此时输出:

Plain
script start
async1 start
promise1
script end
第二阶段:微任务执行

微任务队列中有两个任务(按加入顺序):

  1. 第一个微任务:执行 console.log('async1 success'),然后 return 'async1 end',这会导致 async1 返回的 Promise 被解决

    • 输出 "async1 success"
  2. 第二个微任务:执行 async1().then(res => console.log(res)) 的回调,参数是 'async1 end'

    • 输出 "async1 end"

最终输出:

Plain
async1 success
async1 end
完整输出顺序
Plain
script start
async1 start
promise1
script end
async1 success
async1 end
关键点
  1. resolve() 必须在 Promise 执行器中调用,否则 Promise 永远不会解决

  2. await 后面的代码(包括 return)会被放入微任务队列

  3. async1().then() 的回调也会被放入微任务队列,在 async1 返回的 Promise 解决后执行

  4. 两个微任务的执行顺序取决于它们被加入队列的顺序

代码输出题(字节)

JavaScript
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())
  1. 全局环境初始化
JavaScript
window.name = 'window'

在浏览器中,全局对象是 window,所以 window.name 初始化为 'window'。


  1. Person 函数定义
JavaScript
function Person(name) {
    this.name = name;
}
Person.prototype.print = function() {
    return this.name;
};

  1. 第一次调用 Person('abc')
JavaScript
Person('abc');

没有 new,相当于普通函数调用。

在非严格模式下,this 指向全局对象 window。

因此 window.name 被赋值为 'abc'。

这一步后:window.name === 'abc'。


  1. 执行 new Person('123').print.call({})
JavaScript
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。

输出:

Plain
undefined

  1. 箭头函数中的 this
JavaScript
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'。

输出:

Plain
y

  1. 最后一段
JavaScript
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'。

输出:

Plain
abc

最终控制台输出顺序

Plain
undefined
y
abc