2019年那阵子,小米秋招前端卷子在圈子里流传度挺高。我当年也认真做过这套题,后来带新人、帮朋友做模拟面试,又反复拿它当基础训练材料。现在回头看,这套试卷虽然已经是几年前的产物,但它考察的知识点结构,放在今天依然能打——JavaScript 核心机制、浏览器工作原理、网络协议、手写代码能力,几乎是国内大厂前端笔试的通用模板,只要你投的是前端岗,这套题涉及的方向基本绕不开。
这篇文章我会完整复盘这套“小米2019秋招前端开发笔试题(A)”的题型分布、考察逻辑,再挑核心知识点逐个拆解,把每类题背后的原理讲透。同时我会给出一套模拟版的答题实战过程和常见失分点总结,不管你是在准备校招、社招,还是单纯想检验自己的前端基础扎实不扎实,这篇内容都能当一份比较完整的参考。
1. 这套笔试题到底在考什么:考察思路与题型分布
1.1 2019年前端圈的技术背景,决定了试题侧重点
先捋一下时间线。2019年国内前端生态处于一个微妙的过渡期:Vue 2.x 和 React 16.x 是绝对主流,TypeScript 开始被大厂接受但仍未全面普及,小程序开发刚火起来,前端工程化(Webpack、Babel、CI/CD)已经成为团队标配,但还没有今天这么复杂的链路。
这种背景下,小米作为体量很大的互联网公司,前端笔试不会考过于偏门的框架 API——因为框架更新太快,今天考了明天就过时。反而会花大篇幅考察“换一个框架也依然成立”的基础能力,包括:
- JavaScript 语言本身的语言特性(作用域、闭包、原型链、异步)
- 浏览器与网络相关的知识(渲染机制、缓存、HTTP 协议)
- 代码实现能力(给需求,写代码,或者手写常见 API)
面试官真正想通过笔试判断的是:你有没有扎实的底层基础,能不能在脱离文档的情况下独立写出可运行、可维护的代码,遇到性能问题有没有排查思路。这套“A 卷”之所以有参考价值,正是因为它的命题逻辑非常典型,没有偏题怪题,全是高频基础点。
1.2 卷面题型结构:选择题、简答题、编程题三梯队
结合当时网上流传的回忆版和我自己的答题经验,2019 小米秋招前端笔试题(A)大致可以分成三块:
| 题型段落 | 主要考察方向 | 建议用时 |
|---|---|---|
| 选择题/判断题 | 语言基础、浏览器机制、网络协议、框架常识 | 30 - 40 分钟 |
| 简答题 | 原理描述、方案设计、场景分析 | 30 分钟左右 |
| 编程题 | 手写实现、算法逻辑、代码输出 | 60 分钟以上 |
选择题覆盖面广,零散知识点多,很多同学在这块容易“会但不全”——比如知道闭包是什么,但闭包一定造成内存泄漏吗?不一定,得看用法。这类判断题考察的就是概念的精确性。
简答题往往不会让你写大段论文,而是考察“用一两句话把原理讲清楚”的能力。比如“浏览器的同源策略是什么?”、“请描述 Vue 的响应式数据原理”。看起来简单,但答得准、答得稳其实不容易,坑挺多的。
编程题是拉开差距的核心部分,会综合考察语言基本功和逻辑能力。函数防抖节流、数组去重、深拷贝、Promise 执行顺序、事件循环输出题,都是那个年代前端笔试出镜率最高的类型。小米这套题也基本符合这个规律。
1.3 从出题逻辑反推面试官想要什么样的人
我站在面试官角度分析过这套题的设计逻辑,发现它其实在筛选三类能力:
第一类是“语言掌握度”。公司没有精力去验证你背了多少框架 API,但可以通过一道var和let混用的输出题,判断你理解不理解作用域链;通过一道this指向题,判断你能否预测函数调用时的上下文。这些能力一旦具备,不管框架怎么迭代,你都能快速上手。
第二类是“系统理解力”。现代前端早就不只是“写页面”,所以笔试题会考察浏览器从输入 URL 到页面展示的完整过程、HTTP 缓存策略、回流与重绘的触发条件。这类知识决定你定位问题、优化性能时有没有方向感。
第三类是“代码落地能力”。书面答案写得再漂亮,动手写一个debounce或手写new的过程,立刻能看出你对闭包、this、原型链到底理解到什么层次。所以编程题从来不是“背模板”能应付的,真的要自己写过、踩过坑,才写得流畅。
想通这套逻辑以后,再看这张卷子就不会觉得“考点零散”,它其实是一个非常完整的知识地图。
2. 核心考点拆解:从 JavaScript 基础到手写代码
这张卷子如果只看一个核心,那就是 JavaScript 基础。后面三道编程题也完全围绕 JS 展开。我把核心考点拆成几个模块,每个模块都配上典型例题和原理分析。
2.1 变量提升、作用域与闭包:选择题里的高频区
选择题里一定会出现类似这样的题:
var a = 1; function foo() { console.log(a); var a = 2; } foo();输出是什么?答案是undefined,不是1,也不是报错。因为函数作用域内存在var a的声明提升,但在执行console.log(a)时,a仍未赋值,所以是undefined。这就是经典的变量提升。
关于闭包,有一种常见的迷惑选项:“闭包一定会造成内存泄漏吗?”标准答案是:不一定。只有当闭包引用的变量不再需要、但外部引用依然保留时,才会造成内存无法回收。比如:
function create() { let arr = new Array(1000000).fill(0); return function () { console.log(arr.length); }; } const fn = create();fn引用了arr,如果fn一直存活,那arr就永远无法释放。但这是“必要的引用”还是“内存泄漏”,取决于业务场景:如果确实需要持续访问,就不算泄漏;如果引用不再需要却没有置空,那就算泄漏。
做这部分选择题的经验是:每一个选项都不要只看表面,把背后的执行过程在脑子里跑一遍再下笔。宁可慢,不要错,选择题丢分是最可惜的。
2.2 this 指向与箭头函数:最简单也最容易踩坑
this指向是前端笔试的常青树,小米这套题里也出现了。常规考法是这样:
var name = 'window'; var obj = { name: 'obj', say: function () { console.log(this.name); } }; obj.say(); // obj var fn = obj.say; fn(); // window第一行输出obj很容易理解,方法由obj调用,this指向obj。第二行输出window,因为fn是裸调用,默认绑定到全局。如果放在严格模式下,this会是undefined。这里有一个隐藏考点:普通函数this是在调用时确定的,而不是定义时。
箭头函数的规则恰好相反:它没有自己的this,会捕获定义所在上下文的this。比如:
var name = 'window'; var obj = { name: 'obj', say: function () { var fn = () => { console.log(this.name); }; fn(); } }; obj.say(); // obj箭头函数被定义在say内部,所以它的this与say的this保持一致。我在实际面试中发现,很多候选人能答对单道题,但问到“如果箭头函数再嵌套一层呢”、“如果定义在对象字面量里呢”就容易懵。复习时要多造几个变体,用 Node.js 实际跑一遍输出,印象会深很多。
2.3 原型链、继承与new的实现逻辑
原型链相关题目基本属于“必考但不难”的类型。高频考点包括:
instanceof的原理(遍历原型链)Object.create(null)和普通对象的区别(无原型链,不能调用hasOwnProperty等)Function.prototype和Object.prototype的关系- 各种继承方式(原型链继承、组合继承、寄生组合继承)的优劣
笔试简答题可能会让你描述new执行时发生了什么,或者干脆让你手写一个new函数。new的执行过程可以拆成四步:
- 创建空对象
obj - 将
obj的原型链接到构造函数的prototype - 将构造函数内部的
this绑定到obj,并执行构造函数 - 如果构造函数返回对象,则返回该对象;否则返回
obj
手写实现:
function myNew(Constructor, ...args) { const obj = Object.create(Constructor.prototype); const result = Constructor.apply(obj, args); return result !== null && (typeof result === 'object' || typeof result === 'function') ? result : obj; }注意第 4 步的处理:当构造函数显式返回对象时,new的结果会被该对象覆盖,这是很多同学容易忽略的细节。用Object.create建立原型链,比手动设置__proto__更规范。
2.4 异步、事件循环与 Promise 执行顺序
前端笔试里事件循环相关题目是判断一个人是不是真正理解 JS 运行机制的分水岭。经典输出题长这样:
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 的回调属于微任务(microtask),在当前宏任务(macrotask)结束后、下一个宏任务开始前执行;setTimeout 的回调属于宏任务。执行顺序永远是:宏任务 → 当前宏任务产生的所有微任务 → 下一个宏任务。
这个知识点到现在也是面试高频,我曾经在另一家公司面试时也遇到过几乎一样的题。建议大家刷题时不要再靠背答案,而是理解“任务队列”的模型。Node.js 里process.nextTick的优先级甚至还高于 Promise 微任务,这是进阶考点,2019 年小米这套题没细考,但是面试环节很容易追问出来。
2.5 手写编程题:数组、防抖节流、深拷贝
编程题部分通常不会只考一道。据我回忆和圈内交流,小米这套 A 卷编程题覆盖了以下几个方向:
- 手写数组去重(ES5 和 ES6 两种思路)
- 手写防抖(debounce)和节流(throttle)
- 手写深拷贝
- 实现一个
EventEmitter(发布订阅) - 解决一个简单的算法问题(比如数组扁平化、去重排序)
防抖和节流尤其高频,我见过太多人把两者记混。防抖的核心思路是“等一等再执行”:事件触发后,如果等待期内又触发,就重置计时器。节流的核心思路是“固定频率执行”:一段时间内无论触发多少次,都只认第一次或最后一次。
一个比较稳妥的防抖实现:
function debounce(fn, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }这里的this绑定很容易写错。如果用箭头函数包裹fn.apply(this, args),this也会被静默改变,所以必须用普通函数写法保留返回函数的this。很多候选人能写出功能,但问到这里就答不上来,面试官立刻知道他只是背了模板,没有真正理解。
深拷贝则有几个层次:只拷JSON.parse(JSON.stringify(obj))有天然缺陷(不处理undefined、function、Symbol、循环引用);完整实现需要递归遍历,且要解决循环引用,一般用WeakMap缓存已拷贝对象。手写完整版代码量不短,但思路很固定:
function deepClone(target, map = new WeakMap()) { if (typeof target !== 'object' || target === null) return target; if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target); if (map.has(target)) return map.get(target); const clone = Array.isArray(target) ? [] : {}; map.set(target, clone); for (let key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { clone[key] = deepClone(target[key], map); } } return clone; }这版代码虽然在工程里还需要扩充(比如处理Map、Set、Symbol属性),但已经能覆盖笔试 90% 的考察点,而且体现出了循环引用和特殊类型的处理意识。
3. 模拟笔试题:一整套带答案解析的实操复盘
3.1 模拟选择题与判断题
原题无法一字不差复原,这类企业笔试题一般也不公开。但根据我和其他参与过考试的同学复盘,用下面这套模拟题来还原考察方式非常有效。你自己也可以先遮住答案和解析,当成真实考试做一遍,再对照看错在哪里。
第 1 题:以下哪些方式可以判断一个变量是数组?
A.Array.isArray(arr)
B.arr instanceof Array
C.arr.constructor === Array
D.Object.prototype.toString.call(arr) === '[object Array]'
答案:A、B、D。C 有坑:arr.constructor可以被人为修改,不够可靠。最稳妥的是 A 和 D,B 在跨 iframe 场景下会失效,因为不同全局环境的Array构造函数不是同一个,这属于进阶考法,但选择题里偶尔会出现。
第 2 题:以下代码输出什么?
for (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 100); }答案:3 3 3。原因是var声明的i是函数作用域,循环结束以后i已经变成 3,三个定时器回调共享同一个i。如果把var改成let,输出就会是0 1 2,因为let在每次循环中创建了独立绑定。这类题当年必考,今天也是面试常客。
第 3 题:关于==和===,以下说法正确的是?
A.null == undefined为true
B.null === undefined为true
C.0 == ''为true
D.NaN === NaN为true
答案:A、C。==会做类型转换,null和undefined在宽松相等时相等。NaN与自身不相等,这是 JS 语言一个比较反直觉的设计,所以判断NaN必须用Number.isNaN()。我在改代码时也发现,有些 bug 就是隐式类型转换挖的坑,建议公司规范里直接强制要求使用===。
第 4 题:'hello'.length的值是多少?
答案:5。看起来无脑,但有的笔试题会混淆length和size,比如字符串没有size属性,只有数组和类数组有。这种题考察的是“基础是否足够细”。
3.2 模拟编程题:手写防抖、深度扁平化、事件总线
编程题 1:手写一个带立即执行选项的防抖函数
第一道模拟题我设计成“带立即执行选项”的版本,比普通防抖多一个细节,但很能考察边界处理能力。
function debounce(fn, wait = 300, immediate = false) { let timer = null; let isInvoked = false; return function (...args) { const callNow = immediate && !isInvoked; if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; if (!immediate) { fn.apply(this, args); } isInvoked = true; }, wait); if (callNow) { fn.apply(this, args); } }; }这个版本的逻辑是:immediate为true时,首次触发立刻执行,之后在wait时间内触发无效,等到定时器结束才能重新触发。适用于按钮提交这种场景,第一下点击要立刻生效,后续快速点击要屏蔽。
编程题 2:将多维数组扁平化并去重排序
function flattenAndSort(arr) { const flat = arr.flat(Infinity); return [...new Set(flat)].sort((a, b) => a - b); } console.log(flattenAndSort([3, [1, [2, [3, 4]]], 5])); // [1, 2, 3, 4, 5]Array.prototype.flat(Infinity)是 ES2019 引入的,2019 年这个时间点正好可以考察候选人是否关注新特性。如果你不想依赖它,也可以手写递归实现,两种方式都能得分,但手写递归更能体现功底:
function flatten(arr) { return arr.reduce((acc, cur) => { if (Array.isArray(cur)) { return acc.concat(flatten(cur)); } return acc.concat(cur); }, []); }编程题 3:实现一个简单的 EventEmitter
发布订阅模式是前端非常常用的设计模式,笔试考的概率也高。简化版实现:
class EventEmitter { constructor() { this.events = Object.create(null); } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); return this; // 支持链式调用 } emit(event, ...args) { if (this.events[event]) { this.events[event].slice().forEach((cb) => cb(...args)); } return this; } off(event, callback) { if (!this.events[event]) return this; this.events[event] = this.events[event].filter((cb) => cb !== callback); return this; } once(event, callback) { const wrapper = (...args) => { callback(...args); this.off(event, wrapper); }; this.on(event, wrapper); return this; } }需要注意emit里用了slice()复制一份回调数组,防止回调执行过程中off修改原数组导致遍历异常。这是工程里才会踩到的坑,笔试时写出来会很加分。
3.3 答题时间分配与现场经验
以这套模拟题为例,我建议的时间策略是:
先扫一遍全部题目,把会做的选择题快速搞定,不确定的题先标记,不恋战。编程题先写自己能完全 run 通的那道,建立信心,再回头啃难啃的。简答题如果一时想不起精确表述,先写关键词和要点,最后再完善措辞。
我当时做这套类型题的时候,前 40 分钟在选择题上花得有点多,导致最后一道编程题写得仓促,结果很明显——前面多拿的 5 分,比不上后面编程题丢掉的 10 分。所以时间分配上,我后来总结出的铁律是:编程题永远是最高优先级,至少要留出 50% 的时间给它。
4. 常见失分点与面试官视角:怎样答才能拿高分
4.1 高频失分点汇总
结合很多批改笔试、做模拟面试的经验,我发现候选人在这种“传统大厂前端笔试题”上的失分点高度集中。下面这张表可以帮你快速自查:
| 失分点 | 具体表现 | 解决方案 |
|---|---|---|
| 概念模棱两可 | 知道作用域,但说不清“词法作用域”和“动态作用域”的区别 | 用 1-2 句话把概念讲透,别背定义 |
| 手写代码不完整 | 防抖函数漏掉this绑定,深拷贝不处理循环引用 | 练习时手把手跑一遍边界条件 |
| 忽略编译环境 | 不区分严格模式和非严格模式下的this | 主动说明“在严格模式下结果会不同” |
| 事件循环顺序混淆 | 以为 Promise 比 setTimeout 一定先执行 | 画清楚宏任务和微任务的队列图 |
| 简答题没有逻辑 | 讲 Vue 响应式原理只讲一句“依赖收集和派发更新” | 按“数据劫持→收集依赖→触发更新”三段式表达 |
| 编码风格差 | 变量名随意,缩进混乱 | 笔试也当代码评审来写,保持可读性 |
4.2 面试官视角下,什么样的答案算高分
我后来帮朋友批改过几份模拟题,总结下来,高分答案往往有三个共性:
第一个是“有层次”。比如简答题问“浏览器缓存”,很多人只知道“强缓存和协商缓存”两个名词,但高分答案会进一步说:强缓存通过Cache-Control/Expires控制,协商缓存通过Last-Modified/ETag实现,再补充浏览器刷新行为的影响。这种“总——分——细节”的回答结构,面试官一眼就能看出基本功扎实。
第二个是“有边界感”。比如写节流函数时,主动注明“该版本不支持取消上次计时器,如果需要,可以扩展一个 cancel 方法”。候选人不需要把每种扩展都写出来,但能主动说出“有什么限制”和“怎么改进”,比闷头写完一整段没有注释的代码要加分得多。
第三个是“有可运行意识”。手写代码时,很多候选人会写出Array.prototype.flat这类较新的 API,如果面试官并不确认浏览器兼容性,候选人主动补充一句“这个 API 在低版本浏览器需要 polyfill”或者“也可以手写递归实现”,立刻会让面试官觉得你是真正写过实际项目的,而不是在背题。
4.3 备考建议与核心路径规划
如果你现在正准备前端笔试,我的建议是先别急着刷框架题,花一到两周把 JS 基础夯实,再用整套笔试题进行模拟训练。具体路径可以参考:
- 第一周:语言基础专项。复习变量提升、作用域、闭包、this 指向、原型链、异步与事件循环。每天 10 道选择题,不求速度快,只看正确率。
- 第二周:手写代码专项。数组去重、深拷贝、防抖节流、
new、bind、EventEmitter、Promise.all,每个手写实现至少独立写 3 遍,直到能默写。 - 第三周:高频框架 + 浏览器 + 网络。虽然各公司框架偏好不同,但浏览器渲染原理、HTTP 缓存、跨域方案等属于通用考点。Vue 和 React 至少要熟悉一个的响应式/数据流机制。
- 最后:整套模拟。限定 90 分钟,用小白纸当答题纸,尽量模拟真实笔试环境。不要开编译器自动补全,甚至手写代码。
另外我根据“前端开发面试题”这个经常被搜索的方向,总结一个经验:大厂笔试真正筛选的,不是“背了多少题”,而是“遇到没见过的问题时,能不能用已有知识推导出答案”。所以刷题时遇到不会的题,不要立刻看答案,先自己想一分钟,把思路写下来再去对答案,这个过程比单纯刷题有用得多。
5. 从这套题延伸出来的能力成长路径
回过头看,小米 2019 秋招前端笔试题(A)给我留下的最大影响,不是它考到了什么知识点,而是它让我意识到前端知识的“滚雪球效应”。当时我为了准备这套题,把 JavaScript 高级程序设计翻了两遍,又看了不少源码分析和浏览器原理的文章,这个过程建立的知识网络,直到今天我写 Vue/React 项目、排查线上性能问题时仍然在用。
笔试只是入场券。真正决定一个人能不能走远的,是持续把“模糊”变成“精确”的能力。比如你知道==会做类型转换,这不算什么;但你能准确说出 JS 抽象相等比较算法的执行流程,进而解释为什么[] == ![]是true,这就是深度。类似的深度考点还有很多:
- 为什么
0.1 + 0.2 !== 0.3,根本原因是浮点数在 IEEE 754 下的存储精度问题 - 为什么
Array.prototype.sort默认按字符串编码排序,而不是按数值排序 - 为什么
for...in适合遍历对象属性,但不适合遍历数组 - 为什么
async/await可以被看作是 Promise 的语法糖,错误处理机制却不一样
建议每个前端学习者都准备一个“追根溯源”笔记本,遇到一个知识点,就问三个问题:它解决什么问题?它内部怎么实现的?它的局限是什么?这套思考方式不仅用于准备笔试题,放到实际项目里一样是核心竞争力。
以我自己为例,我后来面试别人时,最喜欢问的一个问题是:“你上一份工作里遇到的最难的一个前端问题是什么,你是怎么定位并解决的?”能在这个问题上回答得好的候选人,几乎都能在笔试里拿到不错的分数。因为那类人有一个共同特点——他们会把一个现象追到源码层、协议层、浏览器实现层,而不是停留在“改一改就好了”的层面。
当年我准备小米这套题时,也走过弯路,大量刷题却缺少系统整理,导致有些题换一个问法就不会了。后来我把所有错题按照“语言基础、浏览器机制、框架原理、工程化、算法手写”五个抽屉重新归档,每次复习只针对薄弱抽屉,效率明显提升。这个方法到现在我也推荐给身边准备笔试的朋友。
最后再分享一个小技巧:笔试也好,面试也好,记得把“思考过程”留下来。笔试题如果给你上机环境,多写注释;面试手写代码时,先跟面试官讲思路再动手。国内大厂前端团队普遍重视代码可读性和沟通能力,你展示出“会思考、能表达、敢动手”的特质,往往比某道题的答案本身更重要。这套小米笔试题就是一个很好的训练场,把它吃透,你会收获的不只是一份应试能力,而是一整套前端工程师的底层思维。