JavaScript 面试四大名捕:事件循环、闭包、原型、this。本文用 15 道递进题目讲清每个概念的常见误解与追问陷阱,附手写 Promise.all 与浮点数精度等综合题。
事件循环、闭包、原型、this——JavaScript 面试的”四大名捕”。它们之所以常考,不是因为偏,而是因为这四块直接决定了你写的异步代码会不会出诡异 bug。
本文用 15 道递进式题目,把每个概念的常见误解与追问陷阱一次讲透。
一、事件循环:先分清两个队列

Q1:宏任务和微任务的执行顺序是怎样的?
一次事件循环:执行一个宏任务 → 清空所有微任务 → (可能)渲染 → 取下一个宏任务。关键是微任务会被一次性清空,而不是每次只取一个。
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
.then(() => console.log('4'))
console.log('5')
// 输出:1 5 3 4 2
解释:同步代码输出 1、5;然后清空微任务队列,链式 then 产生的 3 和 4 都在同一轮清空;最后执行宏任务 setTimeout,输出 2。
Q2:微任务里再产生微任务会怎样?
会在本轮继续被执行,直到队列为空。这意味着递归产生微任务会饿死宏任务,页面卡死。这是面试里区分”背过”和”懂了”的关键追问。
Q3:async/await 在事件循环里怎么算?
await 后面的代码相当于放进 .then() 里,属于微任务。但要注意:多个 await 串联会产生多轮微任务,顺序题里最容易错在这里。
Q4:Node.js 和浏览器的事件循环一样吗?
不一样。Node 有更细的阶段划分(timers、poll、check 等),且有 process.nextTick,其优先级高于普通微任务。答”完全一样”是常见失分点。
二、闭包:不止是”函数套函数”
Q5:闭包到底是什么?
闭包是函数与其定义时所在词法环境的组合。只要内部函数持有对外部变量的引用,且该函数在其词法作用域之外被调用,闭包就产生了。
Q6:经典陷阱题——循环里的闭包。
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出:3 3 3(var 是函数作用域,共享同一个 i)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出:0 1 2(let 为每次迭代创建新的绑定)
追问:“不用 let 怎么修?” → 用 IIFE 创建新的函数作用域,或用 setTimeout 的第三个参数传值。
Q7:闭包会导致内存泄漏吗?
闭包本身不会”泄漏”,它只是让变量无法被回收。真正的问题是被闭包持有的大对象(如整个 DOM 子树、缓存数组)一直不被释放。解法是解除引用或缩小捕获范围,而不是不用闭包。
三、原型与继承:把三张关系图记牢
Q8:__proto__ 和 prototype 的区别?
prototype是函数才有的属性,指向该函数作为构造函数时创建实例的原型对象;__proto__是对象都有的内部属性(标准写法是Object.getPrototypeOf()),指向其原型;- 关系:
obj.__proto__ === Constructor.prototype。
Q9:属性查找的完整链路?
先在对象自身找 → 找不到沿 __proto__ 到原型对象 → 继续沿原型链向上 → 直到 null 返回 undefined。
Q10:new 一个函数时发生了什么?
- 创建一个新空对象;
- 把该对象的
__proto__指向构造函数的prototype; - 以该对象为
this执行构造函数; - 若构造函数返回对象则用该对象,否则返回新创建的对象。
Q11:ES6 class 是语法糖吗?
本质上是基于原型的语法糖,但有实质差异:class 必须用 new 调用、方法不可枚举、内部默认严格模式、有 extends 与 super 的语义。答”纯粹的语法糖”会被追问。
四、this:四种绑定规则的优先级
Q12:this 的绑定规则有哪些?优先级如何?
- new 绑定:this 指向新创建的对象;
- 显式绑定:
call/apply/bind; - 隐式绑定:作为对象方法调用时,指向该对象;
- 默认绑定:非严格模式指向全局对象,严格模式为
undefined。
Q13:箭头函数的 this 有什么特殊?
箭头函数没有自己的 this,它在定义时捕获外层作用域的 this,且无法通过 call/apply/bind 改变,也不能用作构造函数。
const obj = {
name: 'obj',
normal() { console.log(this.name) },
arrow: () => console.log(this?.name)
}
obj.normal() // 'obj'
obj.arrow() // undefined(this 来自模块/全局作用域)
追问:“为什么类方法用箭头函数就能稳定 this?” → 因为箭头函数捕获了构造函数作用域中的 this(即实例),不随调用方式改变。代价是该方法成为实例属性而非原型方法,每个实例一份,内存略增。
五、综合题:把四个知识点串起来
Q14:手写 Promise.all 要注意什么?
三个要点:并发发起、结果按输入顺序返回(而非完成顺序)、任一失败立即 reject。还要处理传入非 Promise 值的情况(用 Promise.resolve 包装),以及空数组应 resolve 空数组。
Promise.myAll = function (list) {
return new Promise((resolve, reject) => {
const res = new Array(list.length)
let count = 0
if (list.length === 0) return resolve(res)
list.forEach((p, i) => {
Promise.resolve(p).then(
v => { res[i] = v; if (++count === list.length) resolve(res) },
reject
)
})
})
}
Q15:为什么 0.1 + 0.2 !== 0.3?
因为 JS 用 IEEE 754 双精度浮点数表示数字,部分十进制小数无法被二进制精确表示,相加后产生微小误差。工程解法:比较时用容差(Math.abs(a-b) < Number.EPSILON * k),金额场景用整数分或专用十进制库。
六、小结
这四块知识点有一个共同特点:都能用一段短代码验证。面试准备时不要只背结论,把每道题的代码在控制台跑一遍、改动一下再跑,理解会比背十遍更牢。
答题时记住一个原则:先给结论,再给机制,最后补一句工程上的注意事项。这样的回答结构清晰,也最容易拿到加分。
参考:MDN · JavaScript 参考文档。语言行为以 ECMAScript 最新规范与 MDN 为准。
#JavaScript#前端面试题#面试

