JavaScript 面试题:执行机制、原型、异步与手写题
JavaScript 面试的核心不是背 API,而是把“代码为什么这样执行”讲清楚。高频追问通常围绕执行上下文、闭包、this、原型链、异步调度和手写实现展开。
执行机制
数据类型与类型转换
知识点讲解
JS 有 7 种原始类型:string、number、bigint、boolean、undefined、symbol、null,以及引用类型 object。typeof null 返回 object 是历史遗留问题,不代表 null 是对象。
类型转换重点看三类:转布尔、转数字、转字符串。对象参与运算时会先走 ToPrimitive,优先调用 valueOf 或 toString,不同运算符的偏好不同。
面试常问
问:== 和 === 的区别?
答:=== 不做隐式类型转换,类型和值都相同才返回 true。== 会做隐式转换,规则复杂,容易出现 [] == false、'' == 0 这类反直觉结果。工程里默认用 ===,只有明确需要兼容宽松输入时才考虑 ==。
作用域、变量提升与执行上下文
知识点讲解
执行上下文包含变量环境、词法环境、作用域链和 this 绑定。函数执行前会先创建上下文,所以 var 声明会提升并初始化为 undefined,let 和 const 也会提升,但在初始化前处于暂时性死区。
面试常问
问:为什么 let 也存在提升,却不能在声明前访问?
答:因为 let 在词法环境里已经创建了绑定,但没有初始化。声明前访问会触发暂时性死区,抛出 ReferenceError。
函数与对象模型
闭包
知识点讲解
闭包是函数和它能访问的外层词法环境的组合。它让函数在外层函数执行结束后,仍然可以访问外层变量。常见场景包括私有变量、函数柯里化、防抖节流、模块封装。
面试常问
问:闭包一定会造成内存泄漏吗?
答:不会。闭包只是延长了变量生命周期。只有当不再需要的引用被长期持有,导致垃圾回收无法释放,才会形成泄漏。工程上要注意移除事件监听、清理定时器、释放缓存引用。
this 绑定
知识点讲解
this 在普通函数里由调用方式决定:默认绑定、隐式绑定、显式绑定、new 绑定。箭头函数没有自己的 this,它捕获定义时外层作用域的 this。
面试常问
问:箭头函数为什么不能作为构造函数?
答:箭头函数没有自己的 this,也没有 prototype,不能通过 new 创建实例,所以不能作为构造函数。
原型链与继承
知识点讲解
每个对象都有内部原型 [[Prototype]],可以通过 Object.getPrototypeOf 访问。函数有 prototype 属性,构造函数创建实例时,实例的原型会指向构造函数的 prototype。访问属性时,先查对象自身,再沿原型链向上查找。
面试常问
问:__proto__ 和 prototype 区别?
答:prototype 是函数对象上的属性,用来给实例设置原型;__proto__ 是对象访问其内部原型的历史访问器。面试回答里最好用 Object.getPrototypeOf 代替 __proto__,更规范。
异步与 ES6+
Event Loop
知识点讲解
浏览器事件循环里,同步代码先执行;遇到异步任务后,回调进入任务队列。每轮宏任务结束后,会清空当前所有微任务,然后再进入渲染和下一轮宏任务。常见微任务有 Promise.then、queueMicrotask;宏任务有 setTimeout、setInterval、I/O、UI 事件。
面试常问
问:Promise 和 setTimeout 谁先执行?
答:同步代码结束后,微任务先于下一轮宏任务执行。所以同一轮里 Promise.then 通常早于 setTimeout(..., 0)。
Promise 与 async/await
知识点讲解
Promise 有 pending、fulfilled、rejected 三种状态,状态一旦改变不可逆。async/await 是 Promise 的语法糖,await 后面的代码会被放入微任务继续执行。
面试常问
问:try/catch 能捕获 Promise 错误吗?
答:能捕获 await 的 Promise rejection,也能捕获同步错误;但如果只是创建 Promise 而没有 await 或 return,外层 try/catch 捕获不到。
高频手写题
防抖与节流
知识点讲解
防抖是“等用户停下来再执行”,适合搜索联想、表单校验。节流是“固定时间内最多执行一次”,适合滚动、拖拽、窗口 resize。
面试常问
问:防抖和节流怎么选?
答:如果关心最后一次结果,用防抖;如果关心过程中的稳定频率,用节流。
call、apply、bind
知识点讲解
call 和 apply 都是立即执行并改变 this,区别是参数传递方式。bind 返回一个新函数,可以预置 this 和部分参数。手写时要注意临时挂载函数、Symbol 避免属性冲突、构造函数调用时 this 绑定规则。
面试常问
问:手写 bind 最容易漏什么?
答:容易漏 new 调用场景。被 bind 的函数如果作为构造函数调用,this 应该指向新实例,而不是绑定对象。
深拷贝
知识点讲解
简单对象可以用递归复制,但真实深拷贝要处理循环引用、Date、RegExp、Map、Set、函数、Symbol、原型等。面试里可以先写基础版本,再说明工程里优先用成熟库或 structuredClone。
面试常问
问:JSON.parse(JSON.stringify(obj)) 有什么问题?
答:会丢失 undefined、函数、Symbol,不能处理循环引用,Date 会变字符串,RegExp 会变空对象,也会丢失原型信息。
底层追问与代码示例
执行上下文与闭包到底保存了什么
知识点讲解
闭包保存的不是某个变量的“值拷贝”,而是对词法环境中变量绑定的引用。外层函数执行结束后,只要内部函数仍然可达,相关词法环境就不会被 GC 回收。
1 | function createCounter() { |
面试中可以强调:count 不在全局,也不在返回对象上,而是在 createCounter 的词法环境里;inc 和 get 都引用了同一个绑定。
面试常问
问:循环里使用 var 为什么会拿到同一个 i?
答:var 是函数作用域,循环不会为每次迭代创建新的词法环境,所以所有回调共享同一个 i。let 是块级作用域,并且在循环每次迭代会创建新的绑定。
1 | for (var i = 0; i < 3; i++) { |
手写 Promise:核心状态机
知识点讲解
Promise 的底层重点是状态不可逆、回调队列、then 返回新 Promise、异步调度。面试不一定要求写完整 A+,但要能写出主干。
1 | class MyPromise { |
这版还没有完整处理“then 回调返回 Promise 并递归解析”的情况。专业回答要主动说明:完整 Promise Resolution Procedure 要处理 thenable、循环引用、返回自身等边界。
手写 bind:兼容 new 调用
知识点讲解
bind 的难点不是改变 this,而是处理预置参数、原型链和构造调用。
1 | Function.prototype.myBind = function (context, ...presetArgs) { |
面试常问
问:为什么 new boundFn() 时不能使用绑定的 context?
答:因为构造调用的优先级高于显式绑定。new 会创建新对象并把 this 指向新实例,如果仍然使用绑定对象,就破坏了构造函数语义。
Event Loop 经典输出题
知识点讲解
输出题要先分同步、微任务、宏任务,不要凭感觉。
1 | console.log('script start') |
输出顺序:
1 | script start |
解释:同步代码先执行;第一轮微任务队列里先有 promise1,再有 queueMicrotask;promise1 执行后追加后续 then,排到已有微任务之后。