☰
小米前端笔试题深度解析:从JS原理到框架工程化
2026/10/11 17:55:11 网站建设 项目流程

2020年秋季那会儿,我正处在刷前端面试题最猛的时候。当时最期待也最怕的一类卷子,就是像小米这种大厂校招笔试题——题量不大,但每一道都往原理深处戳,稍不注意就会被绕进去。时隔几年再回头看这套题,越发觉得它值得反复咀嚼:它不像某些题库那样堆砌偏题怪题,而是精准地覆盖了前端工程师日常开发中真正会遇到的底层知识。如果你正在准备校招或者社招,或者只是想检验一下自己的前端基础是否扎实,这套题都是一个很好的标尺。

我印象最深的是这套题的整体编排:JS 基础、浏览器网络、框架工程化、手写代码、算法逻辑,五个板块一个不落,每个板块的出题角度都带着明显的“小米风格”——不考死记硬背,考的是你是否真的理解某个机制为什么存在。接下来我就按自己的回忆和复现思路,把这套题拆开讲讲,顺带把每道题背后的考察逻辑、答题路径和易错点一起说清楚。

1. 这套笔试题的总体印象:它在筛什么人

1.1 题型分布和时间压力

先说布局。整套卷子大概分成几个模块:选择题、简答题、手写代码题、算法题,比例上看,JS 基础占了约三成,浏览器和网络约占两成,框架和工程化约两成,剩下的是手写代码与算法。时间上一般给 90 到 120 分钟,题量不算夸张,但你要是想每道题都写得很完整,时间会非常紧张。

这种时间压力本身就是考察的一部分。它逼着你在看到题目时立刻判断:这道题是直接写答案,还是需要先思考再动笔,还是干脆先跳过。我当时的策略是先把选择题和简答题里会做的全部拿下,再回头啃手写代码,最后留时间给算法。这套策略让我在时间耗尽前几乎覆盖了所有题目,而不是卡在某一道手写题上出不来。

1.2 2020年这个时间节点的特殊背景

为什么我强调“2020年”这个背景?因为那年恰好是前端框架生态的一个重要转折点。Vue 3 正式版还没发布,Vue 2 的 Options API 仍是主流;React 16.8 之后 Hooks 已经普及,函数组件开始全面取代类组件;TypeScript 在大型项目中的地位已经确立但还没完全统治面试题。所以在当时那张卷子里,你能看到对 Vue 2 响应式原理的考察、对 React Hooks 规则(比如为什么不能在条件语句里调用 Hook)的设问,就是非常自然的事情。

放到今天,如果你只是机械地背答案,你会发现有些题的答案“过时”了。但如果你理解了这些题目背后的核心逻辑——比如响应式系统的依赖收集与触发更新、组件状态变更后的渲染机制、虚拟 DOM 的 diff 策略——你会发现它们到今天依然成立,只是具体实现细节变了。这就是这套题的价值所在:它考的从来不是某个框架版本,而是那些不会随版本迭代而消失的底层思维。

1.3 三个“感受”打底的应试策略

刷这套题最大的收获,不是对了几道题,而是总结出三条应试感受。

第一,选择题里的每个干扰项都不是随便写的,它往往对应一个真实开发中常见的误解。比如关于事件循环的题,干扰项里一定会出现“Promise 立即执行”“setTimeout 的延迟时间一定是准确的”这类表述,你要是平时没踩过这些坑,很容易误选。

第二,简答题的评分逻辑几乎一定是“踩点给分”。不是看你写了多少字,而是看你有没有把关键词写出来。比如问到跨域,你光写 JSONP 肯定不行,得把 CORS、postMessage、代理转发都想到,并且能解释各自适用场景。

第三,手写代码题阅卷时会看你的边界处理。一道防抖函数,很多人能写出核心的 setTimeout 逻辑,但如果没处理 this 指向和参数透传,或者没考虑立即执行选项,分数就会差一截。这也是为什么我建议刷题时一定不能只背核心逻辑,要把完整实现写到闭卷能默写的程度。

2. JS 核心机制题:闭包、事件循环和 this 这三座大山

2.1 经典闭包题:for 循环里的 setTimeout 为什么全是6

这套题里有一道非常经典的闭包题,相信大家看到题干就会心一笑:用 var 声明变量,在 for 循环里每次迭代都 setTimeout 打印 i,问最后输出什么,以及如何修改让它输出预期的 0 到 5。

答案是:全部输出 6。原因在于 var 不存在块级作用域,所有迭代共享同一个变量 i,当循环结束 i 已经变成 6,此时 setTimeout 回调开始执行,读取到的自然是循环结束后的值。这在 JavaScript 面试题里几乎是必考题,因为闭包和事件循环在这里产生了交叉。

要修复,最经典的方案是把 var 改成 let——因为 let 每次迭代都会创建一个新的词法环境绑定,循环体内的变量 i 实际上是每个闭包各自捕获的独立副本。如果你还想展示更强的功底,可以继续写两个不用 let 的替代方案:

// 用 IIFE 制造独立作用域 for (var i = 0; i < 6; i++) { (function (j) { setTimeout(function () { console.log(j); }, 0); })(i); } // 用 setTimeout 的第三个参数传递参数 for (var i = 0; i < 6; i++) { setTimeout(function (j) { console.log(j); }, 0, i); }

这道题真正考察的,不仅仅是“你知道 let 可以解决”,而是你是否明白 let 为什么能解决。如果只背到“把 var 改成 let”,那遇到“如果没有 let 怎么办”的追问就会卡住。我在实际面试别人的时候,只要这一道题就能筛掉一大半对闭包理解只停留在表面的人。

2.2 事件循环输出顺序题:宏任务和微任务的先后

这套题里的事件循环题,典型考法是给你一段混合了同步代码、Promise、setTimeout 的代码,让你写出控制台输出顺序。我至今记得我当时看到那道题的汗毛直竖,因为一个不小心就会把微任务排错。

JavaScript 事件循环的核心逻辑:先执行同步代码;然后执行微任务队列(Promise.then、queueMicrotask 等);微任务清空后,才执行下一个宏任务(setTimeout、setInterval 回调)。注意关键点:整个事件循环每轮只处理一个宏任务,但会一次性清空整个微任务队列。这意味着如果有十个微任务排队,它们会在下一个宏任务执行前全部处理完。

举一个很典型的例子:

console.log('script start'); setTimeout(() => { console.log('timeout1'); Promise.resolve().then(() => console.log('promise inside timeout')); }, 0); Promise.resolve() .then(() => console.log('promise1')) .then(() => console.log('promise2')); console.log('script end');

输出顺序:script start、script end、promise1、promise2、timeout1、promise inside timeout。这里面最容易被忽视的是,setTimeout 回调里创建的 Promise 回调,仍然在 setTimeout 这个宏任务执行期间进入微任务队列,但它不会打断当前宏任务,而是要等当前宏任务执行完后、下一个宏任务开始前清空。所以“promise inside timeout”会紧跟“timeout1”输出,而不是等到下一个宏任务之后。

我当时做这道题的经验是:拿到这类题,先在草稿纸上画出两个队列,一个是宏任务队列,一个是微任务队列,然后按执行顺序逐个处理每个任务,同时在队列里推进新加入的任务。这个方法看起来笨,但准确率极高。

2.3 this 指向的四种判定规则

this 指向的题目在这套题里是以选择题和简答题混合形式出现的。题目大概像这样:给定一个对象,取对象里的方法赋值给外部变量再调用,问 this 是什么。

this 的判定规则,业界已经总结得非常清晰,按照优先级从高到低排列:

  1. 显式绑定优先级最高:call、apply、bind,this 指向传入的第一个参数。
  2. new 绑定次之:如果用 new 调用函数,this 指向新建的对象实例。
  3. 隐式绑定:函数作为对象的方法调用时,this 指向该对象。
  4. 默认绑定:严格模式下 this 是 undefined,非严格模式下 this 指向全局对象。

实际上还有最后一条边界规则:箭头函数不绑定 this,它的 this 由外层词法作用域决定。这套题里就有一道专门的箭头函数题,考察的是箭头函数在对象字面量里的 this 指向问题。很多人的误区是认为箭头函数的 this 指向定义它的对象,实际上箭头函数根本没有自己的 this,它会穿透到定义它的上下文——如果是在对象字面量里定义的箭头函数,这个上下文是全局,而不是对象。

要理解这个穿透逻辑,最直观的办法是在对象外面定义一个普通函数作为参照,再把箭头函数放进去对比。清晰得多。

2.4 原型链与 instanceof 的手写实现

原型链的知识点在小笔试里一般是选择题,但在小米这套题里,居然以手写代码的形式出现:让你实现一个myInstanceof,判断一个对象是否属于某个构造函数的实例。

这其实是一道很好的题目,因为它同时考察三件事:你是否理解原型链的追溯路径、你是否知道 instanceof 的边界情况,以及你能不能把思路转成代码。

标准实现:

function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left); const prototype = right.prototype; while (proto !== null) { if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } return false; }

注意两点:第一,要用Object.getPrototypeOf而不是left.__proto__,因为__proto__虽然浏览器里几乎都有,但它并不在标准 API 意义上被推荐使用;第二,要处理left如果是null或undefined的情况,直接调用Object.getPrototypeOf(null)会抛错,所以更健壮的写法应该加一层判断。

原型链还有一个常考的干扰项:Object.create(null)创建的对象没有原型链,所有原型方法都不可用,但typeof依然是'object'。这个边界情况在选择题里出现率极高,因为它直接指向“原型链终点到底是不是 null”这一知识点。

3. 浏览器与网络:缓存、跨域、渲染链路是面试的常青树

3.1 HTTP 状态码与缓存策略

这套题里选择题和简答题都涉及 HTTP,最典型的是让你判断 301 和 302 的区别,以及强缓存和协商缓存分别对应哪些响应头。

301 是永久重定向,302 是临时重定向。这是最基础的。但小米这道题不会这么简单,它会进一步问:如果有一个页面从 HTTP 切到 HTTPS,或者域名发生永久变更,应该用 301 还是 302?答案是 301,因为搜索引擎和浏览器会自动更新链接,不再访问旧地址;而 302 表示这次迁移是临时的,旧地址短期内仍要承担流量。

缓存的题目则更偏工程。强缓存对应的响应头是Expires和Cache-Control,注意Expires是 HTTP/1.0 时代的产物,存在服务器和客户端时间不一致的问题,而Cache-Control的max-age是相对时间,更可靠。协商缓存对应的头是ETag和Last-Modified,其中ETag的优先级高于Last-Modified,因为Last-Modified只能精确到秒,同一秒内多次修改会判断失误。

我建议准备这类题目时,把完整的请求流程画一遍:浏览器发起请求,检查强缓存是否命中,未命中则携带条件请求头(If-None-Match或If-Modified-Since)发送给服务器,服务器根据资源是否变化返回 200(带新资源)或 304(不带资源)。理解了这条链路,比背十个状态码的价值大得多。

3.2 CORS 跨域到底解决的是什么

跨域题是这套卷子的必考项,而且考法通常不止选择题,会伴有一道场景题:你现在需要从a.com的页面去调用b.com的接口,请问有哪些方案,哪种方案最适合生产环境?

跨域的本质是浏览器的同源策略,即协议、域名、端口任一不同都视为跨域。注意:同源策略是浏览器行为,不是 HTTP 协议限制。服务器与服务器之间没有跨域问题,只有浏览器拦截了响应。这也是为什么开发时经常用 Nginx 代理、Webpack DevServer 代理来解决跨域——因为代理本质上让浏览器只跟同源地址打交道,由后端去请求真正的目标服务器。

生产环境最常见方案自然是 CORS。关键在于 CORS 请求分两种:简单请求和预检请求。简单请求要求方法为 GET、HEAD、POST,且 Content-Type 只能是application/x-www-form-urlencoded、multipart/form-data、text/plain之一。不满足条件的请求,浏览器会先发一个 OPTIONS 预检请求,确认服务器允许后,才发真实请求。很多后端联调时遇到的“明明接口没问题,但浏览器报跨域”就是预检请求没过,比如你自定义了一个Authorization头或application/json的 Content-Type,都属于非简单请求。

如果你能在面试答案里主动提出“CORS 只是表象,本质是浏览器安全模型”这个层次,然后再把预检请求的原理讲透,这道题的得分基本就稳了。

3.3 从输入 URL 到页面渲染,哪一步最容易被忽视

“输入一个 URL 到页面展示,经历了哪些过程?”这几乎是前端面试的“满分送分题”,但也恰恰是最容易被答得大而空的题。在这套卷子里,它是以简答题出现的,要求写出尽可能完整的过程。

完整的链路大致是:DNS 解析域名到 IP → 建立 TCP 连接并完成三次握手 → 如果是 HTTPS,还需要 TLS 握手 → 浏览器发送 HTTP 请求 → 服务器处理并返回资源 → 浏览器解析 HTML 构建 DOM 树 → 解析 CSS 构建 CSSOM 树 → DOM 树与 CSSOM 树合并成渲染树 → 布局(Layout)计算几何位置 → 绘制(Paint)到屏幕 → 合成(Composite)图层。

这道题最容易忽视的环节有两个。

一个是 CSSOM 构建过程中的阻塞问题。很多人知道 CSS 会阻塞渲染,但不知道它不会阻塞 HTML 解析,只会阻塞渲染树的构建,也就是说浏览器会继续解析 HTML,但等到所有 CSS 下载解析完才渲染。这也是为什么推荐把 CSS 放在 head 里,因为越快获得 CSS,渲染越快。

另一个是 JavaScript 的解析执行会阻塞 HTML 解析。默认情况下,遇到<script>标签,浏览器必须停下来下载并执行脚本,因为脚本可能会通过document.write修改当前的 DOM。这也是defer和async存在的意义——defer延迟到文档解析完再执行,async是异步加载完立即执行,两者都不再阻塞解析过程。

如果你在答案里能把“浏览器主线程只有一个,解析和渲染都在上面,所以后端返回大 JS 文件就会白屏”这个逻辑讲透,面试官会觉得你是真的做过性能优化,而不是背的八股。

4. 框架与工程化:Vue 响应式和 webpack 的考察逻辑

4.1 Vue 响应式原理不是背出来的,是推出来的

小米这张卷子里最“唬人”的简答题,应该是问 Vue 2 的响应式原理。很多人上来就答“Object.defineProperty”,然后卡住了。但面试官真正想听的其实是整个链路:组件初始化时,Vue 会对 data 对象的每个属性调用Object.defineProperty进行 getter/setter 劫持;模板编译时会创建渲染函数,渲染函数执行时读取到属性,触发 getter,getter 里调用dep.depend()将当前 Watcher 收集到依赖数组中;当属性被修改时,setter 触发dep.notify(),通知所有依赖该属性的 Watcher 重新执行渲染函数。

这里的关键点是:依赖收集是在 getter 里完成的,而不是初始化时预先把所有 watcher 收集好。因为 Vue 是惰性收集依赖的——只有真正被渲染函数读取到的属性才会被收集,没有被读取的属性即使改变了也不会触发更新(数据仍然会更新,但不会触发视图重渲染)。

用一个小实验可以辅助理解:在 Vue 实例的 mounted 里读取某个属性但不把它渲染到模板上,然后 setTimeout 修改它,观察控制台不会输出新的渲染。这就说明该属性没有进入任何依赖收集。

到了 Vue 3,这套逻辑被 Proxy 重构,但底层的“依赖收集-派发更新”模型没有变,变的只是拦截手段。如果你在答案里能提到 Vue 3 用 Proxy + Reflect 解决数组索引监听和动态新增属性的问题,这道题就从一个“背诵题”变成了“理解题”。

4.2 key 的作用和 diff 的高效逻辑

框架题里还有一个高频小问:为什么 v-for 里必须加 key?这个问题看起来简单,但往往答得不完整。

最核心的原因:key 是虚拟 DOM 节点(VNode)的唯一标识,diff 算法在比较新旧节点列表时,通过相同 key 的节点来新增、删除和复用,这能让组件实例和 DOM 元素被正确复用,避免不必要的重建。如果没有 key,Vue 会使用“就地复用”模式,也就是按位置比对,这会导致在某些情况下(比如列表排序、插入元素)状态错乱,特别是当列表项包含输入框或具有内部状态时。

还有一个常被忽视的点:key 不应该用数组的 index。因为 index 会随着数组的删除、插入而改变,导致本应复用的节点被重建。最好的做法是使用数据的唯一 id,哪怕用Date.now()生成的临时 id 都比 index 强。

顺便说一下,笔试题里有道选择题问 key 可以省略吗,答案是“可以,但破坏体验”。这一点在 React 里面同样成立,所以这套题能帮你打通框架边界。

4.3 loader 和 plugin 的区别,为什么这道题出现率这么高

工程化这一块,小米的卷子大概率会出一道 webpack 概念题:loader 和 plugin 有什么区别?

这题之所以出现率高,是因为它能直观反映你是真的用过 webpack,还是只会跑脚手架。正确的回答应该围绕职责划分:

  • loader 是处理器,通过 pipeline 方式对模块源码进行转换,它接收文件内容,然后返回转换后的内容。比如 babel-loader 把 ES6+ 转成 ES5,css-loader 把 CSS 转成 JS 模块,file-loader 处理资源路径。loader 本质上是一个函数。
  • plugin 是增强器,能监听 webpack 构建生命周期里的各种事件,在适当的时机执行扩展操作。比如 HtmlWebpackPlugin 在打包结束后根据模板生成 HTML,MiniCssExtractPlugin 把 CSS 从 JS 中提取成独立文件,copy-webpack-plugin 复制静态资源。plugin 是一个实例,通过 apply 方法挂载到 compiler 上。

一个问题:为什么需要 plug-in 而不是把 HTML 插入也做成 loader?因为 loader 是针对模块的转换,它只能处理模块内容;但 HTML 文件的生成是整个打包生命周期的产物,它需要考虑多入口、多个 bundle 的 hash 值,这不是“转换某个模块”能解决的,所以必须通过 plugin 在 emit 阶段操作编译结果。这个逻辑清楚了,遇到 webpack 的配置题就不会乱。

4.4 手写题里偷偷藏着的工程化思维

除了概念题,这套卷子里有一道手写题其实也是在考工程化思维:让你封装一个fetch请求函数,要求支持超时、取消、重试。这看起来是网络请求题,但考察的其实是前端工程中抽象能力和设计能力。

我当时是一个小时内连蒙带猜写出来的,后来复盘时发现思路其实有章可循。首先要解决的问题是超时:用AbortController控制 fetch 的中断,并配合setTimeout触发 abort。其次是重试:捕获异常,判断错误类型,如果超时就重试,最多重试 N 次。代码如下:

async function request(url, options = {}, retries = 3) { for (let i = 0; i < retries; i++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), options.timeout || 5000); try { const response = await fetch(url, { ...options, signal: controller.signal, }); clearTimeout(timer); return await response.json(); } catch (err) { clearTimeout(timer); if (i === retries - 1) throw err; } } }

这道题实际考的是“你会不会在真实工程里面对失败的请求做降级处理”,而不只是“你会不会 fetch”。这也是小米那种大厂和一般小公司面试的区别——它不满足于你会调用 API,希望你面对异常、边界情况时有自己的工程方案。

5. 算法与手写代码:前端笔试的“硬通货”

5.1 防抖节流的应用场景和实现差异

防抖和节流是手写题里出镜率最高的两道题之二。小米这套题里同时考了它们,而且还多问了一层:分别在什么场景下使用?为什么会用?

防抖的核心思想:事件触发后延迟执行,如果延迟期间再次触发,则重新计时。所以防抖适合“连续操作后只执行一次”的场景,典型例子是输入框搜索:用户不断输入,直到停顿一段时间后才发送搜索请求,防止每敲一个字就发一次请求。

节流的核心思想:一段时间内只允许执行一次。适合“持续操作中按时间频率执行”的场景,典型例子是滚动监听,比如判断是否滚动到底部去加载更多,你不能每滚动一像素就触发一次,而是每 100ms 判断一次。

一份标准实现:

function debounce(func, delay = 300, immediate = false) { let timer = null; return function (...args) { const callNow = immediate && !timer; clearTimeout(timer); timer = setTimeout(() => { timer = null; if (!immediate) func.apply(this, args); }, delay); if (callNow) func.apply(this, args); }; } function throttle(func, interval = 300) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; func.apply(this, args); } }; }

注意两处细节:第一是 this 指向要保留,因为事件处理函数里 this 通常绑定了 DOM 元素,不能弄丢;第二是参数透传,因为callback可能会用到事件对象event。这两个细节即便你核心逻辑写对了,但漏了 this 或者参数,阅卷时依然会扣分。

5.2 深拷贝为什么每次笔试都有

深拷贝几乎是所有前端笔试的必考手写题。小米这套题也不例外,但它加了限制:不能使用JSON.parse(JSON.stringify(obj))的“作弊”方案,并且要处理循环引用,同时要保证函数、Date、RegExp 等特殊类型不被丢失。

为什么不能只用JSON.stringify?因为JSON.stringify会丢失undefined、函数、Symbol 类型的值,会把 Date 转成字符串,会把 RegExp 转成空对象,还会直接因循环引用抛错。所以一份好的深拷贝实现必须自己遍历属性,同时识别特殊类型。

我当时写的简化版后来成了我源码阅读的入门素材:

function deepClone(target, map = new WeakMap()) { if (target === null || typeof target !== 'object') return target; if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target.source, target.flags); if (map.has(target)) return map.get(target); const clone = Array.isArray(target) ? [] : {}; map.set(target, clone); Reflect.ownKeys(target).forEach((key) => { clone[key] = deepClone(target[key], map); }); return clone; }

注意几个设计选择:用WeakMap存储已克隆对象,它的 key 是弱引用,不会阻止原对象被垃圾回收,比Map更合适;用Reflect.ownKeys而不是Object.keys,因为Reflect.ownKeys能同时获取字符串键和 Symbol 键,覆盖更全面。

这类题的考察重点不是你背了多少行代码,而是你有没有考虑过绕过各个原生 API 的坑。

5.3 数组去重的三种层面

数组去重看起来很基础,但在笔试题里通常会要求你写出多种方案,并说明每种方案的局限性。这恰恰是检验前端基本功是否扎实的一个窗口。

最直观的方案是Array.from(new Set(arr))或[...new Set(arr)],简洁且时间复杂度低,但问题在于它会将 NaN 视为同一个值,且只能做严格相等去重,无法处理{ a: 1 }这种引用类型对象。

第二种方案是用filter加indexOf:

arr.filter((item, index) => arr.indexOf(item) === index);

这种方式能判断出是否首次出现,但时间复杂度是 O(n²),大数据量下性能堪忧。更关键的是indexOf无法区分+0和-0,也会把 NaN 漏掉,因为NaN === NaN为 false,indexOf(NaN)返回 -1,导致 NaN 被误去重。

第三种高级方案是结合Map做复杂类型的去重:

function unique(arr, key) { const map = new Map(); return arr.filter((item) => !map.has(item[key]) && map.set(item[key], 1)); }

这个方案能按对象的某个属性去重,是实际业务中更常见的需求。如果面试题里问的是“去重多场景下你怎么做”,你最好能从简单到复杂,把这三层都列出来。这也是为什么我特别推荐刷题时做笔记——每个知识点都不只是答案本身,而是一系列方案的演进过程。

5.4 手写 Promise 的最小可用版

手写 Promise 是很多大厂校招的压轴题,小米这套题里也出现了,不过要求是“实现一个支持 then 和 catch 的最小可用 Promise”。别小看这个“最小可用”,它包含了完整 Promise 的核心机制:状态机、then 回调注册、异步执行、链式调用。

思路是:Promise 有 pending、fulfilled、rejected 三个状态,状态只能从 pending 变为 fulfilled 或 rejected,一旦改变不可逆。then 注册的回调在状态变更后才异步执行。

简化版如下:

class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.reason = undefined; this.onFulfilled = []; this.onRejected = []; const resolve = (value) => { if (this.state === 'pending') { this.state = 'fulfilled'; this.value = value; this.onFulfilled.forEach((fn) => fn()); } }; const reject = (reason) => { if (this.state === 'pending') { this.state = 'rejected'; this.reason = reason; this.onRejected.forEach((fn) => fn()); } }; try { executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { if (this.state === 'fulfilled') { setTimeout(() => resolve(onFulfilled(this.value))); } if (this.state === 'rejected') { setTimeout(() => reject(onRejected(this.reason))); } if (this.state === 'pending') { this.onFulfilled.push(() => resolve(onFulfilled(this.value))); this.onRejected.push(() => reject(onRejected(this.reason))); } }); } catch(onRejected) { return this.then(null, onRejected); } }

真正的完整 Promise 还要处理返回值穿透、值可能是 Promise 的情况、处理器执行顺序、错误捕获等。手写一个最小版最大的价值,是让你彻底明白 then 为什么能链式调用——因为 then 返回了一个新的 Promise,而不是返回 this。

6. 从这套卷子反推的备考策略和答题技巧

6.1 整理知识图谱,别把自己困在题目里

刷完这套题,我最大的感悟是:不要为了一道具体面试题去背答案,而是按照“知识块”来组织复习。比如事件循环是一个知识块,就把它和 microtask、macrotask、requestIdleCallback 放在一起复习;缓存是一个知识块,就把它和 CDN、Service Worker、网络请求合并起来理解。这样当面试官换一套新的问法时,你也能用核心原理推导出来。

我后来整理前端面试题时,一直用的方法是横向对比和纵向深挖。横向对比是把一些容易混淆的概念放一起比较,比如防抖和节流、强缓存和协商缓存、loader 和 plugin;纵向深挖是沿着一个点层层剥开,比如从“为什么不能直接用 JSON.stringify 做深拷贝”出发,一直挖到 WeakMap 的 GC 机制。你会发现真正能让自己面试时不慌的,不是背过多少题,而是建立起来的知识体系够不够密。

6.2 答题顺序和时间分配要提前演练

很多人在笔试场上答不完题,不是因为不会,而是因为时间分配不合理。我的建议是:拿到卷子先花 30 秒秒览全卷,标记出会的题和不会的题。先做选择题,因为速度快;再做简答题里直接能写答案的;然后做手写题中比较有把握的防抖节流这类;最后攻坚算法题和比较陌生的手写题。一旦某道题想了三分钟还没有思路,立刻标记跳过,最后有时间再回来填。

有一个小技巧值得分享:遇到简答题,哪怕一时想不全所有要点,也要先把核心关键词写上去,然后慢慢往周边扩展。因为阅卷老师看的是得分点,不是小作文。先保证核心得分点不丢,再谈表达。

6.3 从“会做题”到“能讲题”的跨越

笔试只是门槛,真正拉开差距的是接下来的面试。在面对一题多解的问题时,不要只把答案列出来,而是要有逻辑地表达出你的思考过程:先用什么方案,为什么用这个方案,这个方案的缺陷是什么,遇到边界问题怎么兜底。面试官想听的不是一个“正确”的答案,而是一个“有工程判断力”的思考过程。

就拿跨域来说,最低级的回答是“用 CORS 解决”。好一点的回答是“分场景:开发时用 devServer 代理,生产用 CORS,还要注意预检请求”。最好的回答是“我会先判断接口是不是必须跨域,能不能走同域网关;如果必须跨域,再选 JSONP(只支持 GET,适合老接口)或 CORS(现代项目推荐),然后聊一聊预检请求怎么减少、如何配置 max-age 缓存预检结果”。

这套逻辑不仅适用于跨域,几乎适用于所有技术方案题。通过小米这套题,我发现所谓“刷题”,刷的不是题库,而是你能否把每个知识点从“知道是什么”提升到“知道为什么”、再到“知道怎么选”的层次。这个过程不能靠背,只能靠写代码、踩坑、复盘,一遍又一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询