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 / reject 函数由 Promise 构造器传给 executor;可以在 executor 内调用,也可以被闭包捕获后由后续回调调用。关键是最终要调用其中之一或采用另一个 Promise 的状态,而不是要求调用语句必须位于 executor 的同步代码中

  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