ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端面试必问12个JS核心考点:执行上下文、闭包与事件循环

前端面试必问12个JS核心考点:执行上下文、闭包与事件循环 讲个有意思的事。我这两年帮团队做前端招聘面了差不多两百个候选人从初级到高级都有。你猜怎么着不管简历上写着会 Vue 还是 React不管项目经验是三年前端还是刚转行面试题绕来绕去最后基本都会落到 JavaScript 本身。很多候选人挂在第一轮不是因为算法题没刷也不是因为项目讲不清楚就是 JS 基础这块被问到底裤都没了。你问他和的区别他能背出来。你问他0.1 0.2为什么不等于0.3他也能说浮点数。但你让他现场说说执行上下文、事件循环、闭包在项目里到底怎么用他就开始支支吾吾。所以我想把这 12 个面试必问的 JS 核心点系统地整理一遍。这篇不是八股文背诵手册而是从面试官视角出发告诉你每个考点背后到底想考察什么以及你该怎么回答才能让面试官觉得“这人真的懂”。不管你是准备跳槽还是刚入行想补基础这篇文章都值得你花两小时细读一遍最好边看边敲代码。1. 为什么是这12个点面试官到底在考察什么先说明白一个逻辑面试官问 JS 基础不是为了刁难你而是想通过几个问题快速判断你的代码水平。假如你是个面试官面前坐着一个号称三年经验的候选人你总不能让他现场写个业务系统吧。但你可以通过几个基础问题判断他写代码的时候脑子里有没有完整的模型。1.1 前端面试的考察逻辑前端面试考察的东西其实就三层语言本身、运行环境、工程实践。语言本身指的是 ECMAScript 规范比如类型系统、作用域、闭包、原型链、异步机制。运行环境指的是浏览器或者 Node.js 给 JS 提供的宿主能力比如事件循环、DOM API、BOM API。工程实践则是指你在真实项目中怎么组织代码比如模块化、组件通信、状态管理。这里有个很多候选人容易忽略的点面试官问同一道题对不同级别的人期待值是不一样的。同样问闭包对初级候选人他背出“函数内部可以访问外部变量”就够了。但对高级候选人面试官期待你能讲清楚闭包和内存泄漏的关系、闭包在模块化设计中的作用、闭包与柯里化的联动。那有没有一套问题能在有限时间内覆盖这三层考察逻辑有就是我要讲的这 12 个点。它们正好横跨语言基础、运行机制和工程实践三个维度每个点都能由浅入深从概念问到原理再到手写实现。这也是为什么“90%前端面试必问”这个说法虽然夸张但背后确实有现实依据。1.2 12个核心考点全景整体上我把这 12 个点分成四组第一组基础功执行上下文、作用域链、闭包、this 指向。这组问题考察的是你对 JS 代码执行过程的理解。很多人写 JS 写了好几年其实并不知道一段代码在被引擎执行前经历了什么。变量提升为什么存在var和let的差异根源在哪闭包是怎么形成的this到底由什么决定这些问题全部指向同一个底层模型执行上下文。第二组数据与运算类型判断、类型转换、深浅拷贝、数组方法。这一组考察你对数据操作的基本功。typeof和instanceof的区别是什么null为什么被typeof判断成object深浅拷贝怎么实现数组的map、filter、reduce各自最适合什么场景字符串的几个高频场景判断是否包含、忽略大小写比较、验证 URL 有效性这些在项目里天天用到但你有没有踩过坑第三组异步与事件循环Promise、事件循环、setTimeout、async/await。这是面试的重灾区也是区分候选人水平的分水岭。事件循环的宏任务和微任务顺序能写对吗Promise的链式调用和错误捕获有什么陷阱async/await和 Promise 的关系是什么setTimeout(0)真的一定在 DOM 渲染后执行吗第四组工程实践防抖节流、柯里化、模块化与组件通信。这一组偏向应用。防抖节流怎么实现柯里化到底解决什么问题组件通信有哪些模式这些题目的背后是面试官在考察你是否具备工程化思维能不能把语言特性转化为实际问题的解决方案。把上面这些点串起来就构成了我在后面几个章节里详细展开的内容。注意这篇文章的编码风格偏“面试实战”代码片段我都会给出可以直接跑的示例建议你准备一个在线编辑器边看遍敲。2. 基础功执行上下文、闭包与this指向基础功这组问题是每次面试的开场白。我面过的候选人里十有八九能在“什么是闭包”上聊两句但能把执行上下文讲明白的可能不到两成。而实际上执行上下文才是理解闭包和 this 的钥匙。2.1 执行上下文与调用栈JS 代码在执行前引擎会做很多准备工作其中最重要的一步就是创建执行上下文。你可以把执行上下文理解成一块“内存区域”里面记录了这个执行环境中变量、函数声明、作用域链和三者的关系。JS 代码被运行时会维护一个执行上下文栈也常叫调用栈它是一个后进先出的结构。每次进入一个函数调用就有一个新的函数执行上下文被压入栈顶。函数执行完这个上下文就被弹出。全局代码运行在全局执行上下文里它是栈底的那个。这里有个面试高频考点变量提升hoisting为什么存在。在创建执行上下文时引擎会做“声明提升”——var声明的变量被初始化为undefinedfunction声明被完整赋值let和const声明被放到暂时性死区。这个过程是编译阶段的产物规范层面的原因是为了支持函数声明提前使用。来看一段代码console.log(a); // undefined var a 10; console.log(b); // ReferenceError: Cannot access b before initialization let b 20; sayHi(); // 可以正常执行 function sayHi() { console.log(hi); }为什么var a输出undefined而不是报错因为在全局执行上下文创建时a已经被登记了只是还没赋值。而let b虽然也被登记了但被放入暂时性死区。在引擎看来代码执行到console.log(b)时b所在的字节还没有被“初始化”。这个机制是 ES6 为了纠正var的乱象而引入的。面试中如果被问到这里你可以主动补充一句let和const的暂时性死区让 JS 的变量生命周期更可预测这也是为什么现代项目全面采用let/const替代var的根本原因。这句话能体现出你不只是会背规范而是思考过设计动机。2.2 闭包的形成机制与典型应用我之前总听候选人说“闭包就是函数套函数内部函数能用外部函数的变量。”这句话只对了三分之一。闭包的本质是函数和它被声明时所处词法作用域的引用组合。当你从外部函数的内部返回一个内部函数时这个内部函数会记住外部函数的活动对象外部函数执行完被弹出栈后它的变量并不会被垃圾回收因为内部函数还握着对它们的引用。经典代码几乎所有人都会背function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2但面试官很少满足于这个。他会接着问闭包有什么实际应用场景这里我建议你答三个角度每个都附带一个例子。第一个角度数据私有化。上面这个计数器就是一个简单例子。闭包让count成为外部不可直接访问的私有状态只有通过返回的函数才能操作。这种模式在前端组件封装中很常见。第二个角度函数式编程的工具。比如柯里化、部分应用、防抖节流底层都依赖闭包来保存状态。以防抖为例为什么setTimeout的返回值能被记住就是因为闭包机制。第三个角度模块化封装。IIFE立即执行函数配合闭包可以在 ES6 模块出现前模拟模块化开发实现私有变量和公开方法的分离。面试官接着很可能追问闭包会造成内存泄漏吗回答这个问题要有分寸。在 ES6 之前闭包确实容易导致内存泄漏因为 IE 的垃圾回收机制有 bug循环引用无法被回收。在现代引擎里只要你的闭包引用了一个 DOM 元素而这个闭包本身还被变量持有着那这个 DOM 元素确实不会被回收。举个例子function setup() { const bigData new Array(1000000).fill(x); document.getElementById(btn).onclick function () { console.log(clicked); }; }这段代码里事件回调函数通过闭包捕获了bigData只要元素不被移除、回调不被释放这 100 万个元素的数组就一直滞留在内存里。正确的做法是业务侧在不再需要时把onclick置为null。面试时把这个例子讲出来能立刻让面试官对你另眼相看。2.3 this绑定的四种规则this是前端面试的绝对高频点有段时间几乎每个面我的候选人都能把规则背得滚瓜烂熟但实际让其看一段代码写出输出结果时经常全军覆没。this的指向是一套运行时的绑定规则它不关心函数在哪定义只关心函数怎么被调用。这里我总结为四条规则按优先级从低到高排列。默认绑定独立函数调用this指向全局对象浏览器里是windowNode.js 里是global。如果开了严格模式指向undefined。隐式绑定函数作为对象的方法被调用this指向那个对象。这里有个最经典的坑——引用赋值丢失const obj { name: 前端面试, getName: function () { return this.name; }, }; const fn obj.getName; console.log(fn()); // undefinedthis 指向 window/全局而不是 obj为什么因为fn是一个独立的变量引用调用时它不再是obj.getName()这种形式引擎只能走默认绑定。类似的坑还有把方法作为回调传给setTimeoutsetTimeout(obj.getName, 100); // this 丢失显式绑定call、apply、bind。三个函数的区别要非常清楚——call和apply都会立即执行函数只是参数形式不同bind返回一个新函数不立即执行而且新函数的this被永久绑定。new 绑定用new调用构造函数时this指向新建的实例对象。new干的事情包括创建新对象、把新对象的原型指向构造函数的prototype、以新对象为this执行构造函数、如果构造函数没返回对象则返回这个新对象。优先级从高到低是new绑定大于显式绑定大于隐式绑定大于默认绑定。注意bind绑定的this无法被call/apply改写这也是判断你是否真懂this的试金石。很多候选人还会混淆箭头函数。箭头函数没有自己的this它继承的是定义时所处外层作用域的this。所以你不能给箭头函数用call/bind去改变this它只会忽略这些方法。2.4 原型与原型链原型链在 JS 中的位置就像基因图谱在生物学中的位置。它决定了对象之间如何继承属性也是很多手写题的底层依赖。每个函数都有一个prototype属性每个对象都有一个__proto__属性指向创建它的构造函数的prototype。访问一个对象的属性时如果对象本身没有就沿着__proto__一路向上找直到Object.prototype的__proto__是null链条终止。面试经常这样考function Person(name) { this.name name; } Person.prototype.sayHi function () { console.log(你好我是 this.name); }; const p new Person(张三); console.log(p.__proto__ Person.prototype); // true console.log(Person.prototype.constructor Person); // true console.log(Object.getPrototypeOf(p) Person.prototype); // true这里需要注意constructor这个属性。默认情况下Person.prototype的constructor指向Person自身。如果你重写整个prototype对象比如Person.prototype { sayHi() {} }constructor就会丢失指向需要手动补回来。原型链的经典应用有两个。一是继承子类构造函数的prototype指向父类的实例或者用Object.create建立关联。二是扩展内建对象方法比如给数组原型加自定义方法但现代工程不太建议这样做容易污染全局。我在面试中有一个百试百灵的问题p.hasOwnProperty(name)输出什么很多候选人答true。接着问p.hasOwnProperty(sayHi)很多人犹豫了。答案是不管存不存在都要沿原型链找hasOwnProperty这个方法本身这才是原型链真正有意思的地方。关于这组基础功我的实际体会是面试时不要只背结论最好能把执行上下文、作用域链、闭包和 this 串成一条线讲出来。比如你可以这样组织回答“当函数被调用时引擎创建执行上下文执行上下文里包含变量对象、作用域链和 this 值。由于 JS 是词法作用域函数在定义时就确定了作用域链闭包就是基于这条链形成的。而 this 是运行时绑定的由调用方式决定跟定义位置无关。”这段话一出来面试官基本不会再追问初中阶问题。3. 异步与事件循环面试的重灾区面试时我经常问一个问题“你写setTimeout(0)想干什么”十个候选人里有七个会说“延迟执行”四个会说“让代码不阻塞”。真正答到点子上的是把它放在事件循环的机制里去理解。3.1 宏任务与微任务的执行顺序前端面试的异步题核心就一张图执行栈 → 微任务队列 → 宏任务队列 → 渲染 → 循环。我把这套机制和浏览器环境绑定在一起因为在 Node.js 里的轮询规则还有细节差别但面试时把浏览器端的模型答明白已经足够了。JS 引擎一次只执行一个任务事件循环的本质是执行栈清空后先清空全部微任务队列再从宏任务队列里取一个执行执行完再回来清空微任务。为什么要插在中间清微任务因为微任务通常代表“当前同步代码的延续”比如 Promise.then 的回调它应该尽可能快地执行不应该被下一个宏任务延迟。来看高频面试题console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); Promise.resolve().then(() { console.log(promise1); }).then(() { console.log(promise2); }); console.log(script end);输出顺序是script start→script end→promise1→promise2→setTimeout。很多候选人能答对但我要追问一句为什么 Promise 反而比 setTimeout 先执行能答出“微任务优先于宏任务”还不够最好能说清楚“promise1和promise2同微任务队列setTimeout是宏任务”这个细节。还有一道更烧脑的升级题async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); async1(); console.log(script end);输出顺序script start→async1 start→async2→script end→async1 end。这里核心点是await关键字后面跟的是 Promise 时await会把当前函数挂起直到右侧 Promise 解决后把剩下的代码作为微任务加入队列。很多人以为async2()执行时async1 end马上会跟着执行其实这里有个隐式的微任务调度。3.2 setTimeout的0毫秒陷阱回到文章开头提到的热搜词js settimeout 返回值的范围 有0存在么。这个问题可以引申出一个知识点setTimeout的延时参数不是精确的而且存在最小值限制。规范上setTimeout的最小延时是 4ms这是 HTML5 规范里规定的。嵌套超过五级的setTimeout浏览器还会自动增加至少 4ms 的延时。所以理论上setTimeout(0)实际生效的延时可能是 0 到 4ms 甚至更多。另外浏览器标签页处于后台时定时器会被节流最小间隔甚至可能被提升到 1000ms 以上用来节省 CPU。还有一点很多人不知道setTimeout的返回值是一个正整数代表这个定时器的 ID。不同浏览器的实现不同有些从 1 开始递增有些从 0 开始。在 Chrome 里第一个setTimeout返回的 ID 通常是 1但在一些旧的页面里也可能是 0。这就是为什么判断定时器是否有效要用timerId ! undefined而不是if (timerId)。经常有候选人把setTimeout(fn, 0)当作“渲染完成后执行”的手段这其实是个误区。setTimeout是宏任务宏任务执行之后会不会进入渲染流程取决于浏览器是否在恰当的时候合并渲染。想要在 DOM 变更后执行下一步正确的思路是先用微任务Promise因为微任务清空之后才轮到浏览器渲染而不是反过来。很多 UI 更新后的测量需求应该用requestAnimationFrame而不是setTimeout。3.3 Promise必须掌握的四个细节Promise 相关的面试题从简单的then/catch/finally到复杂的链式调用、并发控制都有。我认为最容易被忽略又最常被问的是以下四个细节。第一个细节Promise 的状态只能改变一次。resolve之后再调用reject是无效的。这个特性决定了Promise很适合做异步任务的幂等控制。第二个细节.then()的回调里返回一个 Promise会被展开。这个展开机制是实现链式串行的关键。比如Promise.resolve(a) .then((v) { return new Promise((resolve) { setTimeout(() resolve(v b), 1000); }); }) .then((v) console.log(v)); // 1秒后输出 ab第三个细节.catch()不一定要写在最后一个.then()后面。中间任何一个.then()出错都会被后面的同名.catch()捕获但前提是你用了链式写法。如果中间某一步没有链上错误就成了未处理拒绝。很多新人写axios.get().then(resp ...).catch(err ...)时如果中间又包裹了其他 Promise很容易漏掉链式关系导致错误无法被捕获。第四个细节Promise.all和Promise.allSettled的区别。Promise.all只要有一个失败就整体失败它适合“多个请求必须全部成功才行”的场景。Promise.allSettled等全部结束返回每个任务的结果适合“多个任务独立执行即使某些失败也不影响整体”的场景。还有Promise.race适合做超时控制。3.4 async/await与错误处理面试到高级阶段async/await通常会被关联到错误处理上。很多人写完async函数后根本不处理错误这在实际项目里是事故隐患。用try/catch包裹await是基础但要注意try/catch只能捕获await这个表达式直接抛出的错误。如果await后面跟的是不会 reject 的 Promise或者你把异步调用拆开了错误就会逃逸。另一个高频考点是async函数返回的是一个 Promise所以可以在async函数外面继续用.catch()捕获错误。这一点也可以用来优化错误处理与其在async函数内部层层try/catch不如让它的 Promise 抛出来由调用方统一处理。再补充一个实战经验并发请求的等待不应该写成.map(async () await fetch)这种顺序等待的写法。应该先创建全部 Promise再统一await Promise.all。第一版写法会让请求变成串行第二版才是并发。面试官常把这道题伪装成“优化接口请求性能”的项目细节来问如果你能给出Promise.all并用Promise.allSettled处理部分失败的方案就已经展现出了不错的异步编程能力。这组异步考点的备考建议从事件循环的宏观模型出发把所有异步 API 归入宏任务或微任务然后反复手写几个混合执行顺序的代码段。不要只记输出结果要思维上模拟整个执行流程。4. 数据与运算从类型判断到数组和字符串的实战这一组考题的特点是看起来简单但往往失分最严重。因为简单所以很多人不复习因为不复习细节全忘光。4.1 类型判断的四种姿势判断 JS 数据类型方法很多但要搞清楚每种方法的边界。typeof只能判断基本类型它返回七个字符串undefined、boolean、string、number、bigint、symbol、object。坑在于null、数组、正则、日期它都返回object。instanceof判断的是“实例的原型链上是否出现构造函数的prototype”它适合判断引用类型但不能跨 iframe 或跨 realm 使用因为不同窗口的构造函数不是同一个。综合判断引用类型最常用的方案是Object.prototype.toString.call()Object.prototype.toString.call([]) // [object Array] Object.prototype.toString.call(null) // [object Null] Object.prototype.toString.call(/abc/) // [object RegExp] Object.prototype.toString.call(new Date()) // [object Date]这个方法几乎可以判断所有内置类型也是很多工具库isArray的底层实现原理。最后一种姿势是Array.isArray()用来单独判断数组ES6 之后直接用它比toString更简洁。4.2 深浅拷贝的实现与差别浅拷贝只复制第一层属性深层引用仍然共享。常见实现有展开运算符{...obj}、Object.assign、Array.prototype.slice()。深拷贝则是递归复制每一层彻底切断引用。面试手写深拷贝有一个常见的踩坑点循环引用。如果对象里有自己引用自己的属性简单的递归实现会无限递归导致栈溢出。一个合格的手写深拷贝要具备几个特征能遍历对象和数组、能处理正则和日期、能处理循环引用、能判断 Symbol 属性。项目里能直接用structuredClone或者JSON.parse(JSON.stringify())。JSON.parse(JSON.stringify())对新手来说是万能药但它有明确缺陷不处理undefined、函数、Symbol、循环引用而且把Date变成字符串、把正则变成空对象。structuredClone是浏览器和 Node.js 17 之后原生支持的深拷贝方案能处理多数情况但对于函数的拷贝依然不适用。这块如果面试中被问建议顺着这些坑往下讲会让面试官觉得你真的用过而不是背过。4.3 数组方法不只是map和filter数组方法太多了但面试往往只追问少数几个核心的。我建议你把map、filter、reduce、forEach作为一个整体理解再额外掌握flat、flatMap、some、every、find、findIndex。这里有一个很多人都忽略的细节map和forEach的区别。forEach永远返回undefined不能用于链式操作map返回一个等长的新数组。所以“把数组遍历一遍并返回新数组”应该用map而不是先forEach再手动push。还有一个高频考点是reduce的使用。它不仅能做累加还能实现数组去重、数组转对象、按条件分组const arr [ { type: js, name: 闭包 }, { type: css, name: flex }, { type: js, name: 原型链 }, ]; const grouped arr.reduce((acc, item) { (acc[item.type] acc[item.type] || []).push(item); return acc; }, {}); // { js: [{...}, {...}], css: [{...}] }这种写法在真实项目里非常常用面试时能现场写出来是能力展示不是背题。4.4 字符串高频场景包含、忽略大小写与URL验证热搜词里出现了一串字符串操作相关的内容js判断字符串是否包含、js 忽略大小写、js验证url有效性。这三个场景确实太常见了我单独放到一起讲。判断字符串是否包含子串ES6 之后用String.prototype.includes它返回布尔值比旧式的indexOf() ! -1更直观。但要注意includes是区分大小写的。面试里考得比较细的点是为什么includes不支持传正则而indexOf也不支持正则。这是因为 JS 字符串方法分两类接受正则的和不接受正则的。match、replace、search接受正则includes、indexOf、startsWith不接受。忽略大小写的场景通常用toLowerCase()或toUpperCase()把两边归一化再比较const a HelloWorld; const b helloworld; console.log(a.toLowerCase() b.toLowerCase()); // true如果是对单个字符比较也可以考虑localeCompare加sensitivity: accent不过这个 API 的兼容性和使用场景都比较特定写项目时绝大多数用toLowerCase就够。URL 有效性验证是另一个常见的面试手写题。完整的 URL 结构包括协议、域名、端口、路径、查询参数、哈希。一个“验证 URL 是否有效”的需求要拆分来看是验证格式还是验证可访问性浏览器里可以用fetch(url)去探测但它涉及跨域和网络请求不适合在面试题里直接当答案。格式验证可以用URL构造函数function isValidURL(url) { try { new URL(url); return true; } catch { return false; } } console.log(isValidURL(https://example.com)); // true console.log(isValidURL(not-a-url)); // false这里有个容易误判的地方new URL(www.example.com)不会报错因为它会把www.example.com当路径处理生成一个没有协议基础的 URL。所以如果要求必须带协议你还需要手动判断url.startsWith(http://) || url.startsWith(https://)。关于类型和数据操作这一章我强调一遍代码题不仅要会写实现也要能说明白每个方法的适用边界。面试官喜欢追问“还有没有更好的方案”这时你需要能给出不同场景的取舍建议。5. 进阶与实践防抖节流、柯里化与组件通信到了这一组就不再是单纯的语言知识点而是把语言能力转化为工程能力的体现。公司要的不仅是“会写 JS 的人”更是“能处理真实业务场景的人”。5.1 防抖与节流性能优化必考题防抖和节流几乎是前端面试“手写题”中出镜率最高的两道题。它们的应用场景有输入框搜索联想防抖、滚动事件的懒加载计算节流、按钮点击防止重复提交防抖、图表 resize 的渲染控制节流。防抖的核心思路事件触发后延迟执行如果在延迟时间内再次触发就重新计时。要素是setTimeout加clearTimeoutfunction debounce(fn, delay) { let timer null; return function (...args) { if (timer) { clearTimeout(timer); } timer setTimeout(() { fn.apply(this, args); timer null; }, delay); }; }节流的核心思路在一个时间段内只执行一次时间段内重复触发被忽略。两种常见实现时间戳版本和定时器版本。时间戳版本的优势是首次触发能立即执行定时器版本的差异在于控制结束后是否补执行最后一次。很多面试官考这个目的是看你是否理解“函数的执行时机”和“高频触发时的降频策略”。function throttle(fn, delay) { let lastTime 0; return function (...args) { const now Date.now(); if (now - lastTime delay) { fn.apply(this, args); lastTime now; } }; }面试时能答出这两种方案的差异并说明在什么业务场景下选哪一种就已经是较高分了。5.2 柯里化函数式编程的入场券柯里化是“函数返回函数、逐步收集参数直到收集完才执行”的一种函数式编程技巧。它的应用场景包括日志输出、表单校验规则的复用、事件处理中的预配置参数等。手写一个通用curry函数算是中高阶面试题。核心逻辑返回一个函数收集参数参数攒够后就执行原函数function curry(fn) { return function curried(...args) { if (args.length fn.length) { return fn.apply(this, args); } return function (...nextArgs) { return curried(...args, ...nextArgs); }; }; }注意fn.length是被柯里化函数的形参个数这个细节很关键。写完这个函数我一般会追问柯里化解决了什么问题答案不只是“延迟执行”还有“参数复用”——固定一部分参数生成一个新函数减少重复传参。5.3 模块化与组件通信热搜词里出现了一些组件库和前端工程相关的词比如前端组件库、hzero前端开发、前端传参、js三级联动。这些概念的共同底层是模块化和组件间的数据通信。ES6 模块的export/import是目前最标准的模块化方案。面试官常问export default和命名导出的区别、循环依赖问题、动态import()的应用场景。如果你用过 Vite 或 Webpack 的代码分割最好能结合动态import()来说明按需加载的原理。组件通信的常见模式父子组件通过 props/事件通信兄弟组件通过状态提升或事件总线复杂项目用状态管理Vuex/Redux/Pinia。面试中如果给你一个场景题比如“两个列表组件如何联动”核心思路就是状态提升把共享状态放到二者最近的共同父组件。“三级联动”就是状态提升的典型例子省市区三个下拉框选中省份后城市列表根据省份 update选中城市后区县列表根据城市 update。这就是前端传参和组件通信的基础应用题。很多热词搜索都在找这个东西说明它就是一道经典面试题。关于这组进阶内容我再多提醒一句面试官考察防抖节流、柯里化通常不是要求你像背题一样把代码默写出来而是给你一个业务场景让你用这些技巧来解决。所以平时准备的时候一定要把每个手写函数和真实场景对应起来。6. 手写实现与面试回答策略手写题是四组知识点中让候选人最紧张的部分。但我观察下来手写题的高分选手从来不是靠临场发挥而是把高频手写实现练成了肌肉记忆再加上真正理解了原理可以应对变种题目。6.1 手写new和bind手写new的考察点非常经典因为它同时覆盖了原型、this 和函数调用多个知识点。一个常见实现是function myNew(Constructor, ...args) { // 1. 创建新对象并让它的原型指向构造函数的 prototype const obj Object.create(Constructor.prototype); // 2. 以 obj 为 this 执行构造函数 const result Constructor.apply(obj, args); // 3. 如果构造函数显式返回了一个对象则返回那个对象否则返回 obj return (typeof result object result ! null) || typeof result function ? result : obj; }这里的边界点构造函数的显式返回值。如果它返回一个基本类型会被忽略new仍然返回创建的新对象如果返回一个对象则new返回这个对象。多数人第一步、第二步能写对但第三步经常漏掉导致整体得分被扣掉一块。手写bind的考察点则偏向“函数参数处理”和“new 优先级”。简单版本function myBind(fn, thisArg, ...boundArgs) { return function (...callArgs) { return fn.apply(thisArg, [...boundArgs, ...callArgs]); }; }这里有个进阶问题如果用new调用这个绑定函数thisArg应该被忽略。原生bind返回的函数是可以作为构造函数使用的。如果面试官追问这一层能做出来的人非常少但一旦做了面试分就是质的提升。6.2 手写Promise背后的意义手写一个可用的Promise是很多大厂面试的高阶题目。它的考点不只是异步而是类设计、状态管理、回调队列。最简版的Promise至少要有这几个部分三个状态pending/fulfilled/rejected、值或原因的存储、then方法中两个回调、resolve/reject的状态锁定。一个极简骨架如下帮大家建立基本认知class SimplePromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state ! pending) return; this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); }; const reject (reason) { if (this.state ! pending) return; this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); }; try { executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { if (this.state fulfilled) { onFulfilled(this.value); } if (this.state rejected) { onRejected(this.reason); } if (this.state pending) { this.onFulfilledCallbacks.push(() onFulfilled(this.value)); this.onRejectedCallbacks.push(() onRejected(this.reason)); } } }当然这只是演示基础结构真正的Promise实现还需要处理 then 的链式调用、值穿透、异步执行等细节。面试中不一定要写到标准但能体现你的状态机思维和异步处理能力。6.3 面试回答的话术与心态我最后想专门谈谈回答策略因为这块很多候选人吃亏。第一先定性再展开。比如被问到闭包先说“闭包是函数和其词法作用域的组合”再给例子再讲应用和风险。先说结论能让面试官知道你脑子里有框架。第二主动讲边界条件和坑。比如讲typeof null object时主动补一句“这是 JS 的历史 bugES6 之后依然保留”。这种细节能体现你对语言历史的了解。第三手写题先想思路再动笔。一个合格的手写题答案应该包含边界条件的讨论。如果你只说核心逻辑哪怕代码写对了面试官依然会认为你的工程意识不足。而主动讨论边界条件就像在真实代码评审中一样加分。第四被问倒的时候别慌。说“这块我没实际用过但我理解它的思路是这样的……”永远比直接说“不会”好。面试官考察的重点有时候是看你面对未知问题的应对方法。7. 常见问题与避坑实战速查这一节把面试中候选人最容易踩的坑集中整理一下当作实战速查表使用。每一行都是真实面试中出现过的高频错误。考点常见错误说法正确姿势变量提升“let 声明不会提升”let 和 const 也会被提升但会被放入暂时性死区执行到声明语句前无法访问this 指向“箭头函数的 this 由 apply 决定”箭头函数没有 thiscall/apply/bind 对它无效它继承外层词法作用域的 this闭包内存泄漏“闭包一定会泄漏内存”关键看闭包持有的引用是否长期不释放以及是否引用了大对象/ DOM 引用setTimeout“setTimeout(fn, 0) 一定立即执行”最小延迟约 4ms后台标签页节流它属于宏任务排在微任务之后Promise 错误“Promise 出错一定进 catch”只有链式写法中未捕获的拒绝才能被后面的 catch 接住没有 catch 的错误会成为未处理拒绝深拷贝“JSON.stringify 能拷贝一切”会丢 undefined、函数、Symbol、循环引用、把 Date 和 RegExp 转成非期望结构instanceof“instanceof 可以判断所有类型”不能跨 iframe / realm基本类型也不能URL 验证“只要能被 new URL 解析就是合法 URL”有的字符串会被当作相对路径解析成功要结合业务要求判定协议前缀数组方法“map 和 forEach 都能链式”forEach 返回 undefined不能链式防抖节流“防抖节流能同时解决首尾触发问题”两种实现各有权衡要根据业务场景分别配置立即执行和尾部补充这些坑之所以反复出现是因为很多候选人只背了“结论”而没理解“机制”。我特别想强调一个点面试官问每一个问题都会希望你从“这是什么”讲到“为什么这样设计”再到“实际项目里怎么用”。这三个层次都答到了面试就会非常稳。再分享一个我面试时经常用的追问套路你也可以用它自测。我问“事件循环是什么”候选人答“微任务优先于宏任务”。接着我问“那浏览器渲染呢”如果候选人能补充“渲染会在执行完一轮宏任务和微任务之后进行”我就知道他真的理解浏览器线程模型。如果只能背出顺序我会继续追问“看完下面这段代码输出什么”这里十有八九能分出高下。准备面试的节奏我建议分成三步第一周快速过一遍这篇里所有概念确保每个点都能用自己的话说出来第二周围绕每个概念手写实现闭包、防抖节流、深拷贝、bind 每天各敲一遍第三周做模拟面试把每道题当成本人在面试现场用“定性展开举例边界”的方式口头回答一遍。三周下来这 12 个核心点基本能形成肌肉记忆。从面试官视角总结一下我的感受大多数候选人在基础题上翻车不是因为智商不够而是因为太轻视基础总以为会用框架写页面就等于会 JS。但框架更新换代太快JS 的底层逻辑十几年没变过。真正理解执行上下文、事件循环、闭包和原型链的前端面试时几乎不可能被问倒。就算被问到没见过的场景也能靠基础模型推导出正确答案。这也是我坚持在面试中深挖这些基础点的原因——花二十几分钟问一段基础题抵得上聊两个小时的业务项目。
返回列表