
1. 这套笔试到底在考什么题型分布与考察逻辑2020年秋招那阵子我收到第四范式的笔试链接时第一反应是一家做AI平台的公司前端笔试会不会剑走偏锋比如考一堆机器学习概念或者算法推导真点开试卷之后发现它比我想象中正得多整体偏向大厂通用前端能力但有几道题的出题角度确实带着明显的工程化倾向和纯业务型公司的笔试题有明显差异。整套题大致可以分成五个模块JS语言基础、异步与事件循环、CSS与浏览器原理、框架与工程化、算法与数据结构。考试时长大概90分钟题目总量不算夸张但真正拉分的地方在于看着都会写起来全错的细节题比较多而且算法题占比不低需要你在有限时间内直接手写完整解法。从考察逻辑上看这套笔试题的核心意图已经不是筛掉不会写代码的人而是在会写代码的人里筛出真正理解前端运行机制的人。比如同样的一个闭包题普通公司可能考到输出什么第四范式的题目会多追问一步如果想达到预期输出应该怎么改这就把记忆型选手和原理型选手直接分开了。另外因为第四范式本身就是做AI平台和可视化产品的所以CSS布局、Canvas渲染、大数据量列表优化这类题目出现频率比一般电商或内容类公司更高。如果你只是刷过常规前端面试题库没接触过数据密集型应用场景有几道题可能会觉得别扭。下面我把每一类题型的考查重点、题目还原、解题思路和失分点逐个拆开讲。我会尽量把当时试卷里的出题角度还原出来再补充一些同类变体给准备这类笔试的同学一个完整的对照参考。2. JS基础题的温柔一刀原型链、闭包与this指向2.1 原型链题画图谁都会写代码才见真章这套笔试题里JS基础部分的第一道题长这样function Parent() { this.name parent; } Parent.prototype.getName function() { return this.name; }; function Child() { this.name child; } Child.prototype new Parent(); const child new Child(); console.log(child.getName()); console.log(child instanceof Parent); console.log(child.hasOwnProperty(name));考察点非常集中原型链继承的方式、instanceof的判断逻辑、hasOwnProperty和原型链属性的区别。这道题输出结果是child、true、true大部分基础扎实的同学都能答对但它真正的杀手锏是后面一道追问Child.prototype.constructor Child // 输出答案是false。因为Child.prototype new Parent()这行直接把Child.prototype指向了Parent实例而这个实例的constructor是Parent所以Child.prototype.constructor指向的是Parent。这个问题在当时很多同学那儿都是丢分点因为平时写Child.prototype new Parent()已经是惯例写法了很少有人意识到还需要手动补一行Child.prototype.constructor Child来修正。这类题表面考的是原型链实际上考的是你是否真的理解JS继承的本质。原型链继承的本质是让子类的原型指向父类实例子类实例在查找属性时会沿着__proto__链一路向上直到找到为止。画原型链图谁都会但落到代码里constructor丢失这种细节才是笔试想挖的坑。2.2 闭包与作用域不是背概念而是推理执行过程闭包题是必考这套题里考得挺典型for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }这是每个前端人见过无数次的经典题输出是5个5。如果你以为考点只是var没有块级作用域那就低估出题人了。这道题后面紧跟的追问是请在不使用let的前提下用闭包方式修复这段代码并解释为什么你的修复方案能生效。参考答案是for (var i 0; i 5; i) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }关键是解释每次循环时立即执行函数创建了一个新的函数作用域参数j接收了当前循环的i值并保存在自己的作用域中。当100毫秒后回调函数执行时它通过闭包引用的是那个已经被固定下来的j而不是循环结束后已经变成5的i。这类题在笔试中的真正作用是区分背答案的人和理解闭包的人。因为单纯背答案的人能写对修复代码但解释不清楚为什么IIFE能隔离变量。闭包的定义并不复杂——函数能够记住并访问它创建时所处的作用域哪怕这个函数在外层作用域已经执行结束后才被调用——但要把这个定义用在实际代码里讲清楚需要的是对执行上下文和作用域链的完整理解。2.3 this指向谁调用指向谁但也要小心箭头函数this指向题也是高频考点这套笔试题里出了一道混合箭头函数和普通函数的题目const obj { name: obj, getName: function() { return this.name; }, getNameArrow: () { return this.name; } }; console.log(obj.getName()); console.log(obj.getNameArrow());obj.getName()输出obj没问题。obj.getNameArrow()输出什么答案取决于运行环境。在浏览器全局环境下箭头函数的this在定义时就决定了它继承的是外层作用域的this也就是全局对象所以this.name是undefined。这类题目考查的是对箭头函数本质的理解箭头函数没有自己的this它的this是在词法层面绑定的等价于它定义位置的外层普通函数的this。这个特性在普通面试里可能一句话带过但在笔试里它要和谁调用指向谁的普通函数规则放在一起考如果你只是机械记忆箭头函数不能当构造函数这类结论很容易在这种对比题上判断失误。2.4 手写实现深拷贝、防抖节流、函数柯里化除了选择题和输出题这套笔试题的基础部分还有一道手写题要求实现一个深拷贝函数并且要能处理循环引用。这个要求比深拷贝一个普通对象高了不少因为一旦对象内部出现循环引用简单的递归拷贝会直接栈溢出。标准解法是借助WeakMap来记录已经拷贝过的对象function deepClone(target, map new WeakMap()) { if (typeof target ! object || target null) { return target; } if (map.has(target)) { return map.get(target); } const clone Array.isArray(target) ? [] : {}; map.set(target, clone); for (const key in target) { if (target.hasOwnProperty(key)) { clone[key] deepClone(target[key], map); } } return clone; }这里选择WeakMap而不是Map是有讲究的WeakMap的键是弱引用不会阻止垃圾回收机制回收原对象。在深拷贝这种临时场景下用WeakMap避免内存泄漏风险比Map更合适。而且WeakMap的键只能是对象类型正好符合这里的使用场景。这道题在笔试中的区分度其实不在会不会写深拷贝而在有没有考虑循环引用。能写出基础深拷贝的同学很多但主动处理循环引用的可能不到三成。这也反映了笔试的残酷性不是看你会什么而是看你能考虑到什么边界情况。2.5 基础题备考建议别只看结论多追问自己一个为什么从这套基础题来看第四范式考察的基础能力并不是静态的知识点记忆而是动态的执行过程推演。如果你只在面试前背一遍闭包是函数和其词法作用域的组合那是远远不够的。你需要能在纸上一步步推演代码的执行过程知道每一步创建了什么作用域、引用了什么变量、this指向哪里、输出会在什么时机产生。我的建议是刷这类题时不要只看标准答案试着自己把执行过程画成表格标注每一行代码执行时作用域链的变化、变量状态的变化。这个过程本身就是一次深度复习比刷10道新题更有效。3. 异步与事件循环那些你以为稳拿分的题目3.1 一道经典的执行顺序题坑却在最后一个输出异步题是前端笔试的保留项目这套题里出的那道表面上是常见的事件循环题目但最后一个输出项专门给粗心的人埋了雷async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); setTimeout(function() { console.log(setTimeout); }, 0); async1(); new Promise(function(resolve) { console.log(promise1); resolve(); }).then(function() { console.log(promise2); }); console.log(script end);这道题完整输出顺序是script start async1 start async2 promise1 script end async1 end promise2 setTimeout最容易出错的地方在async1 end和promise2的先后关系。如果你对await的理解是await后面的代码会等待Promise resolve后再继续执行那你可能会以为async1 end会输出在promise2之后但实际结果是async1 end先于promise2。原因在于await async2()这一行的语义可以拆解为先执行async2()得到它的返回值一个Promise然后立即把await后面的代码作为微任务排队。而new Promise().then(...)也是把回调作为微任务排队。关键在于排队顺序await后面的代码先进入了微任务队列之后promise1的.then回调才进入队列所以按先进先出的原则async1 end先输出。这道题还有一层更深的考点await后面的代码实际是在当前async函数的Promise状态变为resolved后才被放入微任务队列的而async2内部没有await它的console.log是同步执行的。这就是为什么async1 start和async2会连续输出中间没有任何微任务切换。3.2 宏任务和微任务的底层逻辑不是背顺序而是理解队列机制很多人遇到这类执行顺序题靠的是背诵一张宏任务优先级、微任务优先级表。但笔试的出题人显然知道大家会背所以会在题目里设计一些不在表上的变体来增加难度。比如常见的优先级总结是process.nextTick Promise setTimeout但如果你只背这个顺序遇到上面那道题还是会栽在async1 end和promise2的先后上。根本原因在于宏任务和微任务的分类只是表象真正决定执行顺序的是队列和入队时机。每次事件循环都会先处理一个宏任务然后清空当前的微任务队列清空过程中新产生的微任务也会继续执行。所以不管setTimeout设置的是0延时还是10延时它都只能在当前宏任务的所有微任务执行完毕后才有机会进入执行。而两个微任务之间的先后顺序取决于它们进入微任务队列的时间而不是书写位置。3.3 手写Promise.all实现不算难难在边界条件的完备性异步模块的最后一道题是手写Promise.all这算是笔试中的诚意题目了因为它不只考API还考你能否实现一个足够健壮的Promise方法。我当时的实现思路是Promise.myAll function(promises) { return new Promise((resolve, reject) { if (!Array.isArray(promises)) { reject(new TypeError(promises must be an array)); return; } const results []; let count 0; const len promises.length; if (len 0) { resolve(results); return; } promises.forEach((promise, index) { Promise.resolve(promise).then( (value) { results[index] value; count; if (count len) { resolve(results); } }, (reason) { reject(reason); } ); }); }); };这道题的完整解答需要覆盖几个关键点入参校验、空数组处理、结果顺序与原数组一致、非Promise值兼容、以及任何一个Promise reject后立即reject。能写对Promise.all本身不难难的是在笔试时间压力下还能注意到空数组resolve、结果按下标存放这些细节。3.4 并发控制题的加分点如何设计一个带限流的异步队列第四范式的笔试题里还有一道偏进阶的异步题实现一个并发池限制同时最多并发3个任务任务完成后自动补充新任务。这道题当时我写的时候思路是维护一个执行队列用一个running计数器控制并发数每次启动任务时检查当前并发数是否达到上限。核心代码如下function createConcurrencyPool(tasks, limit 3) { const results []; let running 0; let index 0; return new Promise((resolve, reject) { function run() { if (index tasks.length running 0) { resolve(results); return; } while (running limit index tasks.length) { const current index; running; Promise.resolve() .then(() tasks[current]()) .then((result) { results[current] result; running--; run(); }) .catch((err) { reject(err); }); } } run(); }); }这道题的实际价值在于它把事件循环回调并发控制这些概念串在了一起。很多同学能写出来但解释不清为什么在.then里要继续调用run()。其实run()的地位相当于任务完成后的调度器它在每次任务结束后判断是否还有剩余任务以及当前是否还能再启动新任务。把这个机制讲清楚比代码本身更能体现你的工程思维。这类并发池在实际项目中的应用场景很多比如批量上传文件时控制同时上传数量、爬虫控制请求并发度、前端数据面板同时拉取多个接口但不想压垮服务端。面试官问这道题往往是在考察你有没有真正处理过这种资源有限但任务很多的工程问题而不仅仅是背Promise API。4. CSS与浏览器原理数据密集型产品的前端功底试金石4.1 盒模型与BFC基础概念但追问起来能问哭人CSS部分的第一道题是盒模型的对比标准盒模型和IE盒模型的区别以及如何用CSS切换。box-sizing: content-box和box-sizing: border-box的对比学过CSS的人都能答。但紧接着的第二问就有点刁钻了在什么场景下你必须使用border-box否则布局会出现问题这个问题的答案有很多种我当时写的是栅格布局和百分比宽度场景。如果你在一个容器里设定两个子元素各占50%宽度同时给它们加上padding或border在content-box下两个子元素的实际宽度会超过父容器宽度的100%导致换行。改成border-box后50%是指包含padding和border的总宽度两个子元素才能正好排在一行。BFC这道题考点更经典问如何触发BFC以及BFC能解决什么问题。触发方式包括浮动、绝对定位、display: inline-block、overflow非visible、flex容器的直接子元素等。BFC能解决的核心问题包括清除内部浮动、防止外边距合并、阻止元素被浮动元素覆盖。这些背都能背下来但笔试真正想考的是——给你一段已经出现布局Bug的代码你能不能反推出问题根源是BFC没有被正确建立。4.2 Flex布局从会写到能算Flex布局题在这套卷子里有一个很有意思的考查方式给你一个flex容器它的flex-wrap: wrap每一个子项的flex: 0 0 30%问一行能放下几个子项。很多人答3个但实际要看容器宽度和子项之间是否有margin或gap。如果子项设置了margin: 1%那么实际占用的宽度是30%加2%的margin一行只能放下2个。这类题考的其实是你对Flex布局主尺寸计算规则的理解在flex-basis明确、flex-grow和flex-shrink都为0的情况下子项占用的空间就等于它的基础尺寸加上margin、padding、border。一旦总宽度超过容器宽度就会触发换行。这种需要动笔算的布局题比问justify-content有哪些取值高了好几个维度。这背后反映的是第四范式这类做数据可视化平台的公司对前端的要求布局不是差不多能看就行而是要在不同屏幕尺寸下精确还原设计稿在表格、图表、复杂仪表盘里处理大量密集的子元素排列。如果你的CSS功底停留在会用flex就万事大吉的程度到真实项目的响应式布局场景中会非常吃力。4.3 渲染流水线与重排重绘为什么大数据表格会卡浏览器原理部分的题目核心围绕一个场景给一个展示上万行数据的表格做性能优化你会怎么做这个场景设置得非常贴近他们自己的业务因为AI平台里的数据标注、结果展示、指标看板动不动就是几万甚至几十万条数据。这类题的高分答案通常从三个层面展开第一层是渲染层面。理解浏览器的渲染流水线HTML解析成DOM树CSS解析成CSSOM树两者合并成渲染树再进行布局计算和绘制。如果数据量大直接填充DOM会导致布局计算量巨大所以要用虚拟滚动技术只渲染可视区域内的行而不是渲染全部数据。第二层是更新层面。重排是会导致整个文档流重新布局的操作比如修改元素的宽度、高度、位置重绘是重新绘制外观比如修改颜色、背景。重排必然导致重绘而重绘不必然导致重排。在数据密集型场景中频繁操作DOM属性会引发大量重排解决思路是批量修改DOM、使用DocumentFragment、或者把频繁变化的样式移到独立图层上。第三层是数据层面。即使只渲染可视区域如果每次滚动都要重新创建所有DOM节点性能依然很差。正确做法是节点复用用一个节点池滚动时只修改节点内容而不是新建和销毁节点。这是虚拟滚动实现的核心。这道题没有标准答案但你回答的深度直接反映你是否有过真实的大规模数据渲染经验。纯业务型前端可能一辈子都碰不到这种场景但对做数据产品的团队来说这是日常挑战。4.4 关于CSS变量和现代布局方案的冷门追问卷子里还有一道关于CSS变量的题定义变量用--变量名读取用var(--变量名)变量会继承也可以在运行时通过JavaScript修改。这道题本身不难但它引出了一个更有价值的追问CSS变量和预处理器变量如SCSS变量的区别是什么答案是SCSS变量是编译期的编译完成后就不存在了所以无法在运行时动态修改CSS变量是运行时的可以直接在浏览器里通过document.documentElement.style.setProperty(--xxx, value)修改并且修改后所有引用该变量的元素会同步更新。这在做主题换肤功能时是非常好用的方案。这个知识点单看试卷它只是一个几分的填空题但它背后连接的是如何在不刷新页面的情况下切换整套主题这种实际工程需求。笔试的意义就在这通过一个小的知识点判断你是否真正做过相应的工程实践。5. 框架与工程化Vue考点居多但需求是理解设计而非背API5.1 Vue响应式原理从Object.defineProperty到Proxy2020年的前端笔试Vue 2还是主流所以框架题集中在Vue 2的响应式原理上。但第四范式的题问得比较有层次从简单到复杂分了三步第一步Vue 2的响应式原理是什么标准回答是Vue 2遍历data对象的每一个属性用Object.defineProperty把它们转换成getter和setter。在组件渲染时如果读取了某个属性就会触发getter并收集依赖后续如果修改了这个属性就会触发setter并通知依赖更新。第二步为什么Vue 3要改成Proxy因为Object.defineProperty有几个局限性它只能代理对象上已有的属性新增属性和删除属性都无法被拦截所以Vue 2才需要提供Vue.set和Vue.delete来处理这类操作。而Proxy可以拦截整个对象上的所有操作包括属性新增、删除、in操作符等更全面。同时Proxy的性能在某些场景下更好因为它不需要遍历对象的每个属性只在访问时进行拦截。第三步给你一段代码问你Vue 2中它是否能正确触发更新代码如下data() { return { user: { name: tom } }; }, methods: { updateUser() { this.user.age 20; } }答案是不能。因为age属性在初始化时并不存在Object.defineProperty没有对它做过响应式处理所以给user新增age属性不会触发视图更新。要修复这个问题需要用this.$set(this.user, age, 20)。追问$set的底层原理是什么答案如果目标对象是响应式对象且该属性不是已有属性$set会用Object.defineProperty为它添加响应式定义并手动触发依赖通知。这道题层层递进从概念到应用再到原理几乎把背面试题答案的同学全部过滤掉了。只背Vue是数据驱动这种话的过不了第二步和第三步。5.2 Vue生命周期与nextTick数据更新后DOM什么时候变框架题里还有一道生命周期相关的问题在created生命周期里修改数据DOM会立即更新吗答案是不会。created阶段组件实例已经创建但还没挂载到DOM上此时修改数据Vue会把它放进异步更新队列等待下一个nextTick时统一更新。这里的核心是Vue的异步更新机制它不会在每次数据变化时都同步更新DOM而是把所有的数据变化收集到一个队列里然后在同一事件循环中批量执行DOM更新。这道题的延伸考点是$nextTick的用法。它接收一个回调函数在DOM更新完成后执行。如果你在修改数据后立即读取DOM的尺寸或位置得到的一定是旧值。正确做法是在$nextTick回调里读取。这个知识点的应用场景很多比如获取一个由v-if控制渲染的元素的宽度或者在下一次渲染后操作某个刚出现的DOM节点。5.3 组件通信备选方案与原理都要能说清楚Vue组件通信是框架题的常客这套题里考察的是多种通信方式的对比。题目要求写出至少三种父子组件通信方案并说明各自的适用场景。常见的答案包括props和emit适用于父子组件之间传递数据和触发事件是最基础的通信方式。$refs父组件通过ref直接调用子组件的方法或访问子组件的数据适合用于子组件暴露方法的场景。provide/inject适用于祖先组件向所有后代组件注入依赖跨层级传递数据不适合做响应式数据管理的场景。Event Bus使用一个空的Vue实例作为事件总线适合非父子关系的组件间通信但事件多了容易混乱不好维护。Vuex适用于跨多个组件共享复杂状态配合devtools调试更方面但要注意避免过度使用。一般同学能写出这五种就差不多了但这类题目真正想考察的是你什么时候不用Vuex、什么时候不能不用Vuex、甚至用过provide/inject的隐性风险它不是响应式的除非传入的是一个响应式对象。这种对细节的把控决定了面试官是把你当会写Vue的还是理解Vue的。5.4 工程化webpack的loader和plugin区别以及代码分割思路工程化部分的题目一是问loader和plugin的区别二是问如何优化打包体积。这道题的价值在于它要求你不仅要理解webpack的配置项还要能解释webpack的工作流程。Loader和Plugin的核心区别是loader是转换器它把一种模块的源码转换成另一种模块的源码比如babel-loader把ESNext转成ES5css-loader处理CSS文件中的import和url()。Plugin是扩展器它在webpack构建流程的特定阶段执行额外的任务比如HtmlWebpackPlugin在构建结束后生成HTML文件并自动注入打包产物MiniCssExtractPlugin将CSS从JS中抽离成独立文件。进一步追问为什么是loader先执行、plugin散布在整个流程中因为webpack的本质是一个模块打包器它需要先把各种类型的文件统一转换成它能理解的JS模块才能进行依赖分析和打包。loader处理如何读取和转换文件plugin处理什么时候做什么额外的事情。能把这个逻辑讲清楚说明你真正理解webpack的设计思路而不是只会配一个vue.config.js。代码分割的优化思路我当时答的是用SplitChunksPlugin把公共依赖提取到单独chunk中用动态import()将路由页面拆分成按需加载的模块再把框架类库如Vue、Vue Router单独打成一个vendor包利用浏览器缓存减少重复下载。这个方案在面试官追问具体怎么配置时也完全能展开不会露馅。6. 算法题的实战应对如何在半小时内拿满分数6.1 高频题数组去重、二叉树遍历、LRU缓存算法题是这套笔试题里占比最重的部分也是拉开分数差距的关键。我当时遇到的算法题主要有三类数组去重、二叉树遍历、LRU缓存设计。数组去重题比较简单但要注意不能只写Array.from(new Set(arr))就结束因为出题人会追问如果数组里有对象Set能去重吗答案是不能因为Set去重用的是同值零算法对于引用类型只有引用完全相同才会被认为是重复。面试官想听的是对去重原理的理解而不是API的调用。二叉树遍历的题目比较常规但要求是写非递归版本。中序遍历的非递归实现需要显式维护一个栈function inorderTraversal(root) { const result []; const stack []; let current root; while (current ! null || stack.length 0) { while (current ! null) { stack.push(current); current current.left; } current stack.pop(); result.push(current.val); current current.right; } return result; }这道题的核心在于理解先一路向左入栈弹出访问再转向右子树这个循环过程。如果你平时只写过递归版本笔试现场临时想非递归版本压力会非常大所以这类经典算法题需要在考前就做好模板化的准备。LRU缓存设计题考察的是你的工程设计能力。它要求实现一个get和put方法当缓存容量满时淘汰最久未被使用的数据。这道题的设计要点是用HashMap实现O(1)的读取用双向链表来维护数据的访问顺序。每次get时如果存在数据把节点移动到链表头部每次put时如果不存在数据且缓存已满移除链表尾部的节点再插入新节点到头部。6.2 做题策略先写出暴力解再优化很多人笔试挂掉不是不会做算法题而是把时间卡在想一步到位写出最优解上。我的经验是笔试先别急着写最优解先用最直接、最不容易出错的暴力解法把题目过了拿到基础分数再回来做优化。比如LRU缓存如果你第一反应是用Map加数组维护顺序虽然时间复杂度不是最优但代码逻辑清晰不容易写错。等你在暴力解的基础上验证了思路再有时间就去优化成HashMap加双向链表。笔试系统可不会因为你差一点写出最优解就给分写对第一版比构思最优解更务实。6.3 笔试实战中的常见失误与应对我在那次笔试算法环节踩过两个坑值得说出来给大家参考。第一个坑是没审清题目对时间复杂度的要求就动手设计。LRU那道题如果一开始就用数组的find方法查找元素时间复杂度是O(n)虽然功能正确但肯定拿不到这道题的满分。所以拿到题目先看有没有要求在O(1)时间内完成之类的限定词有的话就要围绕这个约束选数据结构。第二个坑是边界条件检查不完整。比如二叉树遍历时如果根节点为空应该返回空数组LRU的get方法如果key不存在应该返回-1。这些边界条件在笔试里往往占据大量测试用例不写就是白丢分。7. 复盘与秋招准备建议从一套笔试题反推备考方向7.1 从这套题看第四范式前端团队的关注点整套卷子考下来最大的感受是这家公司的前端团队不是业务驱动的而是产品与体验驱动的。他们对前端的要求不止是实现页面而是在复杂数据场景下高效地实现页面。这从CSS计算题、浏览器渲染性能题、大数据列表优化题都能看出来。另外一点他们对工程化的重视程度比一般公司高。webpack的loader和plugin、代码分割、构建优化这些话题如果只是会配脚手架是不够的你得知道构建链路里每一步在做什么。这不是面试官为难你而是他们的产品里确实存在大量需要精细控制构建过程的场景。7.2 给准备前端秋招笔试的通用建议结合这次笔试经历我整理了三条对多数公司笔试都有用的建议第一基础题一定要建立执行过程推演的习惯。别只看代码的输出结果要自己一步一步推演变量、作用域、调用栈、事件队列的变化。能用纸笔画出来才说明真懂了。第二算法题要准备模板。二叉树的前中后序遍历、层序遍历、DFS、BFS、快排、递归转非递归这些是高频中的高频考前最好能默写出来。笔试的时间压力很大不要指望现场推导。第三框架题不要只背API要理解设计动机。比如Vue为什么用数据驱动、为什么需要异步更新、为什么模板编译器和运行时分离。追问一个为什么往往就能分辨出你是背的还真的实践过。7.3 笔试结束后的复盘方法笔试结束并不代表这个环节就结束了及时复盘才是把笔试价值最大化的方式。我当时把每一道做错的题、蒙对的题都整理到一个文档里标注出错因和正确思路。之后每次面试前翻一遍比刷新题还有用——因为自己的错题记录的是真实的思维盲区针对性极强。复盘时还要注意一个点有些题你虽然做对了但用的方法不一定是最优解。比如深拷贝那道题如果你只是自己写了一个没用WeakMap的版本虽然测试用例可能过了但面试官在后续面试中追问如果对象循环引用怎么办时你可能就答不上了。所以复盘时也要回头审视自己做对的题看看有没有更好的解法尽早补齐盲区。7.4 我自己踩过的一个比较惨的坑最后分享一个真实的教训。我在那次笔试里有一道关于事件循环的代码题第一遍输出顺序写对了但复查时又改错了——我把async1 end和promise2的顺序改反了。原因是我在复查时过度依赖记忆中的await是异步的所以async1 end一定最后输出这个错误直觉反而推翻了自己最初基于推演得到的正确结果。这个经历让我养成了一个习惯遇到事件循环、Promise、async/await这类题目复查时不再靠我觉得而是重新走一遍入队顺序的推演。如果推演结果和之前一致就坚决不改。在笔试这种环境下直觉有时候比记忆可靠而基于规则的推演又比直觉可靠。希望这篇复盘能帮你少走一些弯路。第四范式的笔试难度在当年算中等偏上但它的考点结构其实非常典型认真吃透这套题的底层逻辑对你准备任何一家公司的前端秋招笔试都有帮助。