loader

Nerio News Magazine brings you trusted, timely and thought-provoking stories from around the globe.

Follow Us

JavaScript 面试硬题:事件循环、闭包、原型与 this 的 15 道追问

Share This Article:

JavaScript 面试四大名捕:事件循环、闭包、原型、this。本文用 15 道递进题目讲清每个概念的常见误解与追问陷阱,附手写 Promise.all 与浮点数精度等综合题。

JavaScript 面试硬题:事件循环、闭包、原型与 this 的 15 道追问
关键词事件循环、宏任务、微任务、闭包、原型链、this、Promise

事件循环、闭包、原型、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 一个函数时发生了什么?

  1. 创建一个新空对象;
  2. 把该对象的 __proto__ 指向构造函数的 prototype
  3. 以该对象为 this 执行构造函数;
  4. 若构造函数返回对象则用该对象,否则返回新创建的对象。

Q11:ES6 class 是语法糖吗?
本质上是基于原型的语法糖,但有实质差异:class 必须用 new 调用、方法不可枚举、内部默认严格模式、有 extendssuper 的语义。答”纯粹的语法糖”会被追问。

四、this:四种绑定规则的优先级

Q12:this 的绑定规则有哪些?优先级如何?

  1. new 绑定:this 指向新创建的对象;
  2. 显式绑定call / apply / bind
  3. 隐式绑定:作为对象方法调用时,指向该对象;
  4. 默认绑定:非严格模式指向全局对象,严格模式为 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#前端面试题#面试

Related Post

发表回复

Your email address will not be published.