小米前端笔试复盘:基础扎实是关键,手写题与滑动窗口经验分享
2026/9/5 3:40:00 网站建设 项目流程

每年秋招的节奏都像打仗,小米这轮前端岗笔试我是在九月中旬收到的通知。说实话,看到“第二批笔试”这几个字的时候,反而比第一批更紧张——因为这意味着网上已经有很多人讨论过题型和难度了,你要是没刷到这些信息,等于裸考。我属于那种考前一定要把信息都摸透的类型,所以这篇复盘我会把“怎么准备”、“考什么”、“怎么答”几个层面串起来写,既记录我的真实经历,也尽量说点对后来人有用的东西。

一个值得先说清楚的结论:小米前端岗的笔试,重点不在算法难不难,而在基础扎不扎实。它不像有些大厂上来就是hard题劝退,但选择题覆盖面极广,手写题又非常考验边界意识,整体风格是“你好像都见过,但一写就漏”。这篇文章适合正在准备秋招春招的前端同学,也适合想了解大厂校招笔试真实节奏的朋友参考。

1. 笔试前一周:确认形式、准备环境、划定复习范围

1.1 第一批考完后的信息收集

我是第二批笔试,时间排在工作日的晚上,和第一批间隔了四天。这四天非常关键,因为第一批考完当晚,牛客、脉脉、小红书就会有人发帖子。我不是鼓励大家去找原题,而是要通过这些讨论判断三件事:题量、题型分布、编程题用不用核心代码模式

我当时收集到的信息归纳下来是这样的:选择题约二十道,涵盖JS基础、CSS、浏览器原理、网络协议、少量框架题;编程题两道,一道偏手写实现,一道偏算法;总时长一百分钟左右。这个信息帮我做了两个决定:第一,复习重点从“刷难题”转向“过基础”;第二,提前在牛客上练了几套模拟题,适应平台那套代码编辑器的手感。

说实话,如果你连考试系统都没用过,直接在正式笔试里花十分钟搞明白答题界面怎么切题、代码怎么运行,那太亏了。

1.2 考试平台与环境细节

小米这批笔试用的是牛客的在线笔试系统。这类系统有几个共同特点:选择题可以标记回头再看,编程题只能本地调试后粘贴提交,或者直接在网页编辑器里写。我建议编程题直接在网页编辑器里写,因为本地编辑器有代码提示、有习惯的快捷键,一旦复制粘贴出了格式问题,反而不容易发现。

另外一个细节是摄像头监控。笔试要求全程开启摄像头,环境要安静、光线不能太暗,桌面不能有笔和纸之外的东西。我当时把桌面清理得很干净,连水杯都放地上了,省的万一误判就麻烦。还有一点容易被忽略:提前测网速,别考试中途断线。我用的有线网络,把笔记本充电器也插好了,因为笔试和面试不一样,笔试一断线,系统可能直接判定交卷,申诉流程极其折腾。

1.3 复习范围怎么划

一周时间不可能面面俱到,我的策略是:把高频考点列出来,按“必看、了解、放弃”三档切分。

必看的内容包括:JavaScript事件循环、闭包、原型链、this指向、异步方案演进;CSS三栏布局、flex与grid、BFC、层叠上下文;HTTP缓存机制、跨域方案、浏览器渲染流程;React Hooks使用规则、Vue响应式原理的基本描述。了解的内容包括:TypeScript的泛型和工具类型、Webpack的loader和plugin区别、前端工程化常见概念。放弃的内容包括:复杂的CSS动画、Canvas底层API、Node.js服务端细节。

很多人准备校招笔试喜欢直接刷LeetCode,但前端岗的笔试和纯后端算法岗不一样,选择题占比高,考得杂,只刷题不复习八股文,等于只练了半条腿。我当时每天把时间切成两块:白天刷三到四道LeetCode热题加看手写题源码,晚上集中过两到三个前端知识点,画成笔记。这套节奏比较适合笔试周期短的情况。

2. 选择题里的高频考点:哪些八股文值得背,哪些可以放弃

2.1 JavaScript语言特性考点

小米这批选择题里,JavaScript部分大概占了百分之四十。具体来说,闭包和this指向几乎必考,事件循环至少出现两到三题,原型链和new的实现逻辑也有一道。

举个例子,关于this指向的题一般长这样:给一段代码,里面有普通函数、箭头函数、对象方法,问最终输出结果。这类题看起来很基础,但正确率其实不高,因为箭头函数的this是在定义时确定的,普通函数的this要看调用方式,再加上严格模式和非严格模式的差异,四个选项各有道理。我复习时会拿一张A4纸把所有调用方式列出来:直接调用、对象方法调用、call/apply调用、new调用、回调函数调用,每种情况对应什么this,写上两遍就记住了。

事件循环的题则是另一个重灾区。它通常会混合setTimeout、Promise、async/await、微任务、宏任务,还要考虑代码的执行顺序。做这类题我有一个笨办法:先画一条时间轴,把宏任务按出现顺序排好,再在宏任务里面标注微任务队列。把Node环境下的process.nextTick和浏览器环境的queueMicrotask区别也理一下,虽然笔试不一定考,但面试一定会问。

原型链的考点主要集中在构造函数的prototype、实例的__proto__、以及Object.create的用法。我记得有一道题是给出一个继承关系,问某个属性在哪里能找到,属于典型的“你懂原型链就秒选、不懂就全靠蒙”的题。建议把下面的代码自己在脑子里过一遍,过不了就动手写:

function Parent() { this.name = 'parent'; } Parent.prototype.say = function() { console.log(this.name); }; function Child() { this.age = 18; } Child.prototype = new Parent(); Child.prototype.constructor = Child;

这样设置的继承,Child实例可以调用say方法,并且child.constructor指向Child而不是Parent。这几个结论连在一起,比死记硬背强得多。

2.2 浏览器、网络与CSS

浏览器和网络的部分,占比也不低。HTTP缓存是必考项,强缓存和协商缓存的字段要背熟:Cache-Control的max-age、no-cache、no-store分别干什么,ETag和Last-Modified的优先级谁高,304出现的场景是什么。跨域问题也考了一道,选项大概是从“CORS、JSONP、postMessage、WebSocket”里挑一个满足某场景的方案。我记得还考了cookie和localStorage的区别、浏览器从输入URL到页面渲染的整个过程。

CSS部分,三栏布局那类题换了个外壳出现。不是直接让你写布局,而是给一段flex代码问最终效果。flex的flex-grow、flex-shrink、flex-basis组合起来确实容易迷糊,建议复习的时候记三条规则:flex-basis定基准尺寸、flex-grow管放大比例、flex-shrink管缩小比例,缺省值分别是auto、0、1。BFC也考了,问的是哪几种方式能形成BFC,overflow:hidden、display:flow-root、position:absolute这些都要能选出来。

选择题的特点是答案相对固定,不需要写理由,但容错率低。每道题大概值两到三分,错五道就等于丢掉了一道编程题的分数。我当时给自己定的目标是选择题正确率百分之八十以上,因为编程题不确定性太高,选择题的分数必须拿稳。

2.3 框架题的占比与应对方式

3. 手写代码题:从用例设计到边界条件的小坑

3.1 防抖节流的两种写法对比

3.2 深拷贝的完整实现与隐藏考点

3.3 手写题作答的展示策略

4. 算法题复盘:一道滑动窗口题目的完整思考过程

4.1 拿到题先问三个问题

4.2 从暴力解到滑动窗口的推导

4.3 时间复杂度的取舍与笔试判题机制

5. 笔试过程中的节奏管理和意外情况处理

5.1 时间分配方案

5.2 遇到不会的题和编译报错怎么办

5.3 考后及时复盘,为面试做铺垫

手写题是前端岗笔试最有区分度的部分,它不看你会不会背,就看你能不能在白板状态下写出能跑的代码。小米这批的手写题风格偏实用,不像有些厂让你手写一个完整的Promise或者发布订阅。我记得自己遇到的是两个很经典的题目方向:防抖/节流的实现,以及深拷贝的实现。这类题平时写业务的时候经常用到,但真要你从零手写,很多人反而写不完整。

3.1 防抖节流的两种写法对比

先说说防抖和节流。防抖的核心思想是“一定时间内多次触发只执行最后一次”,节流的核心思想是“一定时间内最多执行一次”。笔试的时候经常让你二选一,或者先写防抖再写节流。很多人的误区是背了一个版本,结果没注意是否支持参数传递、是否带立即执行选项。

一个相对完整的防抖实现是这样:

function debounce(fn, delay = 300, immediate = false) { let timer = null; return function(...args) { const context = this; if (timer) clearTimeout(timer); if (immediate) { const callNow = !timer; timer = setTimeout(() => { timer = null; }, delay); if (callNow) fn.apply(context, args); } else { timer = setTimeout(() => { fn.apply(context, args); }, delay); } }; }

这个写法里面有三个容易被忽略的细节:第一,this要保留,很多面试官会追问箭头函数能不能用在这里;第二,immediate参数控制是否立即执行,这个在实际业务里很常用,比如按钮点击防重复提交;第三,定时器执行完之后要把timer置空,否则下一次callNow的判断会出错。

节流我常用时间戳版本:

function throttle(fn, interval = 300) { let last = 0; return function(...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }

时间戳版本有个特性:第一次触发会立即执行,最后一次触发如果没到间隔时间会丢掉。如果不想丢最后的调用,可以用定时器版,但笔试一般不会要求双触发边界,写清楚一种加注释说明即可。

这类手写题,真正加分的地方不一定是你写得多完美,而是你代码里体现了对边界情况的思考。我在代码注释里标注了“保留this指向”和“支持立即执行选项”,阅卷官假设是人工在看,也会觉得你平时不是死记硬背,是真正理解了这个工具函数。

3.2 深拷贝的完整实现与隐藏考点

深拷贝这道题,看着简单,其实考的是你的知识网够不够密。

很多人一上来就写JSON.parse(JSON.stringify()),这当然也是深拷贝,但它的缺陷非常明显:遇到undefinedfunctionSymbol会被忽略;遇到Date会变成字符串;遇到循环引用会直接报错。如果笔试题目里面的测试用例包含这些类型,这个写法一定是过不了全部用例的。

一个能覆盖大多数场景的手写深拷贝如下:

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的使用——它的作用不是缓存数据,而是处理循环引用。假如对象a里面引用了b,b里面又引用了a,不做处理就会无限递归下去。用一个WeakMap把已拷贝的源对象和克隆对象对应存起来,再次遇到同一个引用时直接返回之前克隆好的对象,递归就终止了。

还有Reflect.ownKeys这个API,它会把普通字符串属性和Symbol属性一起枚举出来,这是Object.keys做不到的。如果考到Symbol作为属性名的克隆,用Object.keys的写法直接漏数据。至于为什么用WeakMap而不是Map,是因为WeakMap的key是弱引用,不会影响垃圾回收,用Map在这个场景里也不会出错,但面试官问了能答上来就是加分项。

3.3 手写题作答的展示策略

手写题和算法题在作答上有本质区别。算法题只看结果对不对,手写题大概率存在阅卷环节,代码风格、注释、变量命名都会被看在眼里。我的策略有三个:

第一,提前写注释。不是把整段代码注释满,而是在关键判断处写一句“// 处理循环引用”“// 保留this指向”,让阅卷人一眼看到你的思路。第二,先用最保守的方式把核心功能跑通,再补边界。不要一上来就写一个很复杂的版本,结果语法都报错。第三,考虑性能和可读性的平衡。深拷贝用递归是标准做法,但如果递归层数很深,会栈溢出,能提到用迭代法的思路作为后续优化方向写在注释里也足够了。

手写题最怕的不是写不出来,而是写了一半发现思路有问题,划掉重写,最后交上去的代码只能用三个字形容——一团糟。我建议先在草稿纸上把思路的关键节点写出来,再往编辑器里誊。

算法题这部分,我运气不算好,遇到的一道题需要一点思考量,但也没有到劝退的地步。具体题目细节我记不太清了,但类型是无重复字符的最长子串,算是LeetCode上的热门题,考的就是滑动窗口这个经典思路。这类题对有准备的人是送分题,但对没刷过的人就是完全没思路。

4.1 拿到题先问三个问题

看到算法题的完整描述之后,我先逼自己回答三个问题:输入规模大概多大、返回的是什么类型的结果、有没有明显的特殊边界需要单独处理。

输入规模决定你能不能放心用O(n^2)的解法。如果n在500以内,暴力解可能都能过;n到10^4,基本就要往O(nlogn)下面去想了;n到了10^5,线性解法是必须的。有些笔试题会故意不给你数据范围,这时候默认按比较大的规模处理,优先写线性或近似线性的解法。

边界条件方面,我每次都提醒自己检查:空字符串、全部字符相同、全部字符都不相同、字符串只有一位。这几个用例分别覆盖了滑窗的初始化、左右指针移动、更新逻辑的极端情况。

4.2 从暴力解到滑动窗口的推导

“无重复字符的最长子串”这道题,最直观的解法是枚举所有起始位置,然后从每个位置往后扩展,扩展过程中用Set记录出现过的字符,一旦发现重复就停止,记录当前长度。这个方案的正确性没问题,但最坏时间复杂度是O(n^2),当字符串长度到10^5级别的时候,很大概率超时。

滑动窗口的思路本质上是对暴力解的优化:既然右指针扫过的字符已经记录了,左指针只需要重复字符出现之后跳过去,不需要回到前面的位置重新遍历。代码写出来就是一套双指针加一个Set:

function lengthOfLongestSubstring(s) { const set = new Set(); let left = 0; let maxLen = 0; for (let right = 0; right < s.length; right++) { while (set.has(s[right])) { set.delete(s[left]); left++; } set.add(s[right]); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; }

这段代码的思路可以概括为:右指针逐个扩展窗口,一旦遇到重复字符,就把左指针往右移动,直到窗口里没有重复字符为止。每次右指针移动后都计算一次窗口大小,不断更新最大值。整个过程中,左右指针都只往前走,时间复杂度是O(n)。

我当时的思考路径是:先想到暴力解,然后发现暴力解重复扫描了很多无效区间,接着想到如果用Set记录当前窗口内的字符,左指针跳转的时候就不需要从头再来。这个过程也是面试官最想听的“解题过程”,但笔试只认最终代码,所以如果你在注释里写清楚这是滑动窗口,阅卷观感会好很多。

4.3 时间复杂度的取舍与笔试判题机制

笔试判题只看代码能不能在规定时间内跑完所有用例。你写一个O(n^2)的暴力解,如果全用例通过,照样满分;你写一个O(n)的滑动窗口,只要有语法错误,照样零分。所以笔试策略上,优先保证能跑通,其次才追求最优解。

但如果时间允许,我建议把暴力解和优化解都留在草稿上,实际提交时用优化解。为什么?因为有些笔试题的最后几个用例是专门针对暴力解设计的超大数据,暴力解跑不过去。我个人的习惯是:前五分钟写一个暴力解确保思路正确,然后立刻评估它的复杂度,如果判断可能会超时,再写优化版本。

另外一个经验是:尽量别用递归写深度不确定的算法。递归虽然没有硬件栈那么严格,但在牛客这类系统里,过深的递归会导致调用栈溢出,而这道题完全可以用迭代写。能用循环就用循环,能不用递归就不用递归,这是在线判题环境下最稳妥的选择。

笔试现场的时间节奏,其实比很多人想象得紧张。不是说题目难到做不完,而是你需要在有限时间内把会的题都稳定发挥出来,不因为慌乱而丢分。这里分享一些我实际操作下来的时间分配和临场策略。

5.1 时间分配方案

一百分钟,我的分配是这样的:选择题控制在四十五分钟以内,手写题三十分钟,算法题三十分钟,最后留五到十分钟检查。选择题遇到拿不准的,先标记一下,别死磕,因为后面做编程题时可能突然想起来某个知识点。

编程题我习惯先把两道题都扫一眼,判断哪道题更顺手。手写题和算法题,如果水平差不多,先做手写题。手写题是“熟悉题型”,先把稳定的分拿住;算法题可能存在思路卡壳的风险,放到后面心态会更稳。但如果算法题一眼就看出了思路、手写题反而没把握,那就按“先易后难”原则先做算法题。这个没有绝对标准,核心是先拿能拿的分

检查环节被很多人忽略,但实际上非常关键。检查什么呢?一是代码有没有未定义的变量,二是有没有逻辑边界遗漏,三是注释和题目要求是否一致。有时候你在草稿上想的和实际写进编辑器的完全是两回事,用最后几分钟读一遍代码能挽回不少冤枉分。

5.2 遇到不会的题和编译报错怎么办

笔试时遇到完全没思路的题,最忌讳的是坐着发呆。我的做法是先把输入和输出用简单的示例跑一遍,再尝试用暴力解去套,哪怕只能通过一部分用例也比零分强。有时候暴力解写着写着,思路反而顺了,从暴力解优化出正确解的情况并不少见。

编译报错则是另一回事。在线笔试系统对语法错误的提示往往不如本地IDE友好,报错信息看不太懂的时候,先检查几个高频错误点:分号是不是漏了、括号是不是匹配的、变量名是不是大小写不一致。如果是运行结果不对,在关键位置插入一个console.log看中间状态,比靠肉眼看代码快得多。

还有一个容易被人忽略的坑:牛客这类系统的输入输出格式是固定的。如果题目要求读一行整数,你的代码写成读一个字符串再split,会导致格式错误。这类题我建议提前在牛客上练几道输入输出模板题,熟悉readline的使用方式,别把时间浪费在调试IO上。

5.3 考后及时复盘,为面试做铺垫

笔试结束不等于事情结束。我会在自己还能记住题目的时候,把所有题目按“做对、做错、蒙对、没思路”四类整理一遍。做错的题,不一定是没掌握,可能是考场太紧张看错了题目条件;蒙对的题更要重视,因为面试问到同类型问题立刻露馅。

整理完之后,我会把每一类题目对应到相应的知识点,判断自己哪一块是薄弱环节。“做错”的题涉及原型链,那就抽时间重新过一遍原型链的常见问法;“没思路”的算法题是动态规划,那就补几道经典DP题找感觉。这个环节看似消耗时间,但实际上非常高效——它让你下一次笔试或面试前的复习变得非常有针对性

还有一点,考完出结果之前的空窗期,一边等通知一边积累项目经验,别把秋招的节奏停在笔试结束。小米的面试一般会问项目、问基础、问场景题,笔试点出的薄弱点,正好是接下来要补的面试考点。

回头看这场笔试,我最大的感受是:前端岗的笔试不拼题海战术,拼的是广度够不够全、细节够不够细。选择题考察的是你对常见API和原理理解的准确性,手写题考察的是边界意识和编码习惯,算法题则考察分析问题的路径。这三个层面没有一个是临时抱佛脚能解决的,都需要前面几年或者几个月的持续输入。

最后再分享一个小技巧:笔试前我会把前端核心知识点的标题列在一张思维导图上,考试中一旦遇到纠结的选项,就在脑中定位到相应的知识点分支,回忆我当时写下的关键结论。这个方法帮我避免了很多“凭感觉做题”的情况,也推荐给即将参加校招笔试的同学。

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

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

立即咨询