我记得那天晚上,打开在线笔试链接之后,整个人对着一个空荡荡的文本框懵了很久。那道题要求手写一个带防抖功能的函数,并且要正确处理this指向和参数透传。日常开发里这不是什么难事,可用处在一个没有自动补全、没有任何提示的网页编辑框里,心里那点底气一下子就虚了。那是小米2018春季实习生前端开发工程师笔试,也是我第一次真正意识到,准备前端开发面试题不能靠背,得靠理解。后来我把那套题里涉及的考点全部重新梳理了一遍,发现一份笔试考卷其实就是一张前端开发技能的体检表,每一个考点背后,都对应着实际开发中的一个真实场景。今天这篇文章,我就把这场笔试的题型、考点和复习思路完整拆开,给正在准备 Web 前端开发实习的同学提供一份可以直接照着用的清单。
1. 2018春季实习生前端笔试的整体画像与考察逻辑
1.1 第一次笔试现场:在线判题系统里的真实体验
先说说整体形式。那年小米的春季实习生笔试,前端开发工程师岗给我的感觉是:题量不算特别大,但时间很紧张。整场考试大概还是走了“选择题 + 填空题 + 手写代码题 + 场景设计题”的常见组合,限时大概在 90 到 120 分钟这个区间。选择题里会穿插一些代码输出题,需要你在几个选项中判断运行结果;手写代码题则要在一个文本框里写出完整实现,没有运行环境,没有 lint,甚至没有格式化。
我印象最深的不是某道特别偏的题,而是环境本身。它逼着你在写代码之前,先在脑海里把逻辑完完整整跑一遍。因为一旦下手,你很难像平时开发那样一边 console.log 一边调试,只能在有限的时间内靠脑内编译。这个经验很残酷,但也很有价值:它逼着我把“会写”变成“写对”。
所以如果你现在准备这类笔试,我建议平时就刻意练习“不看编辑器写代码”。可以开着记事本写,也可以直接在纸上写,写完再复制到 IDE 里验证。这个过程会让你迅速发现自己在 API 细节、边界条件、标准写法的记忆上还有多少漏洞。
1.2 命题逻辑:要从一场笔试里看出什么
我复盘时想明白了一个事:笔试不是用来出难题难倒你的,它更像个漏斗,第一层筛掉基础不扎实的人。大厂招聘实习生,最怕的不是候选人不会某个冷门技术,而是候选人连 JavaScript 里非常核心的概念都搞不清。实习生进来是要跟项目产出的,如果连作用域、闭包、异步都说不明白,那带起来会非常辛苦。
所以你会发现笔试题看似在考某个 API,实际上都在考底层理解。比如var和let的区别,表面上是语法差异,背后是作用域和变量提升;比如setTimeout的输出顺序,表面上是异步问题,背后是事件循环和任务队列;比如box-sizing的取值,表面上是 CSS 属性,背后是盒模型理解。
这套逻辑到今天依然成立。前端框架一年换一批,但 JavaScript 的核心机制、浏览器工作原理、网络协议基础,这些是不会一夜之间失效的。笔试真正想筛出来的,是那些“遇到新问题也能用基础能力推演”的人。
2. 核心考点拆解:从JavaScript到浏览器原理
2.1 JavaScript基本功:变量提升、闭包和this
JavaScript 基础在任何一次前端开发面试题里都是绝对的主角,小米这次也不例外。我印象里有几类题几乎必出。
第一类是变量提升和函数声明优先级。比如:
console.log(a); // 输出什么 var a = 10;答案当然是undefined,因为var会把声明提升到作用域顶部,但赋值不会。这里还有一个延伸考点:
function foo() { return 1; } var foo; console.log(typeof foo); // 输出什么答案是function,因为函数声明比变量声明优先级更高。如果对这两个概念不够熟悉,笔试里很容易丢分。
第二类是闭包。闭包几乎是所有前端笔试的保留节目,考察方式非常多。最常见的是循环和异步结合在一起:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }输出是 5 个 5,而不是 0 1 2 3 4。原因是var没有块级作用域,循环里的setTimeout回调共享同一个i变量。等到 100ms 之后执行时,循环已经结束,i已经变成了 5。
当年我看到这道题,第一反应是“怎么会想到这么刁钻的考法”。后来才明白,这就是真实开发里容易踩的坑:异步回调里读取了一个循环变量,结果全是同一个值。解决方法也很经典,可以用 IIFE 立即执行函数或者 ES6 的let:
for (let i = 0; i < 5; i++) { setTimeout(() => console.log(i), 100); }第三类是this指向。考题通常会给一段代码让你判断输出,比如:
function getName() { console.log(this.name); } const obj = { name: '小米', getName: getName }; obj.getName(); // 输出什么 const fn = obj.getName; fn(); // 输出什么第一个输出'小米',因为调用方式是obj.getName(),this指向obj;第二个输出取决于全局环境下有没有name变量,在严格模式下就是undefined,非严格模式下可能指向window.name。核心判断标准就是一句话:看函数被谁调用,调用点决定了this指向。这里还经常延伸到箭头函数,箭头函数没有自己的this,它会继承定义时所在作用域的this。
2.2 函数进阶:call/apply/bind与...args的考点
前端开发里经常有人问“函数里的...args是什么”,这个问题我在那年的笔试复习资料里也看到了。它其实是剩余参数(Rest Parameters),顺便还和扩展运算符(Spread)一起出现。笔试不会直接问你定义,而是会放在代码题里让你填写。
比如:
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }这里的...nums会把所有参数收集成一个数组。还有一个方向是把数组展开成参数列表:
const arr = [3, 5, 1]; Math.max(...arr); // 5 Math.max.apply(null, arr); // 5这里就带出了call、apply、bind的差异。call和apply都会立刻执行函数,区别仅仅是参数传递方式不同,call是逐个传,apply是传数组。bind不执行,它返回一个新函数,并且会把传入的this固定下来。
笔试里更高频的是让你手写一个bind,或者写一个“让类数组对象也能使用数组方法”的功能。比如:
function getArgs() { return Array.prototype.slice.call(arguments); }这段代码就是利用call借用数组的slice方法,把arguments这个类数组对象转换成真正的数组。在 ES6 里可以直接用Array.from(arguments)或者[...arguments]完成,但理解底层的借用思路依然重要。
如果你能自己手写一个bind,那这部分的印象分会涨不少:
Function.prototype.myBind = function(context, ...args) { const fn = this; return function(...rest) { return fn.apply(context, args.concat(rest)); }; };这里面包含了两层参数合并:绑定时的参数和执行时的参数要拼在一起,同时还要保证返回函数被new调用时的表现。笔试不一定会让你写这么完整,但如果能写出来,说明你对函数调用的底层机制是真的理解到位了。
2.3 异步编程与事件循环:笔试里的“送分题”与“送命题”
前端一天到晚都在跟异步打交道,所以事件循环是必考项。笔试最典型的是一道输出顺序题:
console.log('A'); setTimeout(function() { console.log('B'); }, 0); Promise.resolve().then(function() { console.log('C'); }); console.log('D');正确输出顺序是A D C B。很多人会搞错C和B的前后,原因在于事件循环里微任务和宏任务的机制。同步代码先执行完,然后清空微任务队列,Promise.then属于微任务,会排在setTimeout这个宏任务之前执行。
我当时复习的时候,会把任务队列画成一张图。画一次你就理解了:每次事件循环都会先取出一个宏任务执行,执行完后再把所有微任务清空;如果微任务里再产生微任务,会一起清完再进入下一次宏任务。所以setTimeout虽然设的是 0ms,也只能在微任务之后执行。
另外,常见的进阶题是async/await结合Promise的推导题:
async function test() { console.log(1); await Promise.resolve(); console.log(2); } test(); console.log(3);输出是1 3 2,因为await后面的代码相当于放进了.then()里,属于微任务。这些题看着绕,但只要把“同步执行完再清微任务”的规则吃透,就不会错。
笔试考这个不是在秀偏门语法,而是因为在真实项目里,请求数据、DOM 更新、页面渲染、动画执行,全都依赖事件循环的调度。不懂它,写出来的代码就会出现“明明先发出去的请求,结果后到的数据覆盖了先到的数据”这种经典 bug。
2.4 CSS布局、盒模型和前端页面实现
前端开发面试题里不一定全是 JavaScript,CSS 也占了不少篇幅。我印象里那年笔试至少有这么几个方向。
第一个是盒模型。题目会问content-box和border-box的区别,或者让你算一个元素的实际宽度。content-box是默认值,宽度只包含 content,padding 和 border 会往外撑;border-box是宽度包含 content、padding、border,实际布局更可控。现在很多项目都会在全局设置:
* { box-sizing: border-box; }就是为了避免 padding 把布局撑乱。
第二个是居中问题。垂直水平居中可以说是前端笔试里最经典的 CSS 题。常见方案有 flex:
.parent { display: flex; align-items: center; justify-content: center; }也可以使用绝对定位加 transform:
.child { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }如果元素尺寸固定,还可以用负 margin。关键是搞清楚每种方案的使用条件:flex 对子元素数量不确定的场景很舒服,transform 不要求知道元素宽高,但会生成层叠上下文。笔试里如果要求“不用 flex 实现”,说明出题人想考你对定位和 margin 的理解。
第三个是响应式。可能会问你 rem 和 px 的区别,或者媒体查询怎么写。这类题主要考察你平时做页面时有没有考虑过多尺寸适配。复习时可以重点准备一下 rem 计算方式,以及flex布局在移动端常用的几个属性。
2.5 网络、缓存与性能优化题
前端笔试如果只考代码,那是不完整的。那年的题目里也有不少网络和浏览器相关的内容。比较常见的是 HTTP 缓存机制,分强缓存和协商缓存。强缓存相关的头是Expires和Cache-Control,协商缓存相关的头是Last-Modified/If-Modified-Since和ETag/If-None-Match。
有一道经典连环题:用户在地址栏输入一个 URL 到页面展示,整个过程发生了什么?这个题很适合用来复习,因为知识点特别集中:DNS 解析、TCP 连接、HTTP 请求、服务器响应、浏览器解析 HTML、构建 DOM 树、CSSOM 树、执行脚本、布局和绘制。
笔试可能只考其中一段,比如“为什么 CSS 放在头部,JS 放在尾部”,或者“什么是渲染阻塞资源”。答案一句话就能概括:CSS 会阻塞渲染,JS 会阻塞解析,所以 CSS 要尽早加载,JS 可以延迟执行或用defer。这种常识虽然简单,但背后是浏览器渲染流程的理解,还是值得认真整理的。
性能优化方向也常考,比如懒加载、图片压缩、CDN、HTTP/2、代码分割。我当时的复习思路是记住一个优化链路:减少请求体积,减少请求数量,提升加载并行度,减少主线程阻塞。有了这个框架,笔试里遇到具体优化题就能往里面套,不会太空洞。
3. 备考核心:Vue、手写题与工程化思维
3.1 Vue技术栈:数据响应式、组件通信与开发规范
2018 年那会儿,Vue 已经是大厂实习笔试里的高频词了。小米的笔试题虽然没有直接让写一个组件,但围绕 Vue 的知识点出现得不少,比如数据响应式原理、组件通信方式、生命周期顺序等。
数据响应式是我复习得最深的一个点。Vue 2 的响应式核心是Object.defineProperty,它通过 getter/setter 拦截属性的读取和修改,再配合依赖收集和派发更新,让数据变化自动触达视图。面试里会进一步问:Object.defineProperty有什么缺点?答案大家应该也都知道:无法监听属性的新增和删除,对数组的索引变化无法感知。所以 Vue 2 才需要提供$set和$delete这种补丁式 API。Vue 3 改用Proxy之后,这些问题就迎刃而解了。
组件通信也是必背清单。父子之间靠props和$emit,兄弟组件靠事件总线,复杂项目可以上 Vuex。这里面会延伸出一个问题:props为什么不能直接修改?因为它会造成数据单向流被破坏,后续的数据状态变得不可预测。笔试如果只让你回答“能不能改”,你最好能解释清楚“为什么不能改”。
还有一个容易被忽略的是 Vue 开发规范。虽然笔试不会考具体配置文件,但会从一个更高层面问:怎么保证一个项目长期可维护?这就要涉及组件命名规范、目录结构划分、ESLint 统一代码风格、状态管理按模块拆分等。热词里提到的“前端开发规范vue”,本质上就是在说:框架能力只是起点,真正拉开差距的是你在团队协作里能不能写出一致、清晰、可维护的代码。
3.2 高频手写题:数组去重、防抖节流、Promise.all
手写题是笔试里最考验人的部分,因为它没法蒙。我那年的复习范围里,有几类是反复出现的。
数组去重。最简单的写法当然是 ES6 的Set:
const unique = arr => [...new Set(arr)];但笔试如果只写这种,说明你在基础层够了,但在“展开思考”上还不够。可以再写一个reduce版本,或者手写一个时间复杂度 O(n) 的对象标记方式,顺便讨论一下不同数据类型的去重问题。这会让面试官看到你思维的层次。
防抖和节流。这两个高频题也是我在笔试现场翻车的点。防抖是“最后一次触发后才执行”,节流是“固定时间内只执行一次”。代码模板如下:
function debounce(fn, wait) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); }; } function throttle(fn, wait) { let previous = 0; return function(...args) { const now = Date.now(); if (now - previous >= wait) { previous = now; fn.apply(this, args); } }; }写成这样之后,还要能解释什么时候用哪个。搜索框输入建议用防抖,因为等你停下来再去请求;滚动加载或按钮点击限频用节流,因为要保持一个节奏。
Promise.all。这个手写题我当时背了很多遍,核心思路是等待所有 Promise 都完成,并且结果按传入顺序排列:
function promiseAll(promises) { return new Promise((resolve, reject) => { const results = []; let count = 0; if (promises.length === 0) { resolve(results); return; } promises.forEach((p, index) => { Promise.resolve(p).then(value => { results[index] = value; count++; if (count === promises.length) { resolve(results); } }, reject); }); }); }这里有一个关键点:结果数组不能直接 push,因为 Promise 完成的顺序不一定是传入顺序,所以要用results[index]去填充。手写完了还要准备口头追问:如果其中一个 Promise 失败会怎样?答案是走 reject,并且不会继续等待其他 Promise。这种边界问题在笔试后的面试环节极容易问到。
3.3 工程化思维:从“会写页面”到“会组织项目”
笔试里出现开放式题目时,通常不是在考你某个 API,而是在考你工程化意识。比如“如果让你设计一个商品列表页,你会怎么拆分组件”或者“你会如何设计一个前端项目结构”。这种题没有标准答案,但可以判断你的组织能力。
我准备这类题的心得是,养成一个“组件思维”:把页面拆成可复用的最小单元,再考虑每个单元的数据输入和事件输出,最后再考虑状态提升到哪一层。这个思路放到 Vue、React、原生 JS 里都通用。
工程化还包括模块化、构建工具、代码规范、自动化测试。笔试不一定直接让你写测试,但可能问“如何保证模块之间的依赖关系清晰”或“如何减少打包体积”。我当时把 Webpack 的几个核心概念,比如 entry、output、loader、plugin,花了一天时间彻底理清,到了笔试里遇到相关选择题,基本一眼就能选对。后来我在企业级项目里还接触过 HZero 这类集成度很高的中后台开发平台,它把权限、路由、页面配置都标准化了,但如果你不理解前端工程的整体脉络,用这些平台也只是一知半解地按文档填配置。底层组织能力,最终还是从基础工程化思维里长出来的。
4. 复盘后的备考方法论:题源、题库和路线
4.1 建立“错题—知识点—场景”三级题库
备考前端开发面试题,最忌讳的是拿着一堆题从早刷到晚,刷完就忘。我经过这次笔试后,整理了一个三级题库:第一级是错题本身,第二级是它对应的底层知识点,第三级是这个知识点在真实开发里会出现在什么场景。
比如遇到“闭包循环输出”这道题,你会写答案之后,应该继续问自己两个为什么:为什么var没有块级作用域?为什么let能解决这个问题?然后再联想一个真实场景:在循环里给一批按钮绑定点击事件,如果回调里访问了循环变量,会不会出现同样的 bug?这样一来,你复习的就不是一道孤立题目,而是一整片知识。
| 笔试题目 | 背后考点 | 真实场景 |
|---|---|---|
| 循环 + setTimeout 输出 | 作用域、闭包、异步 | 循环绑定事件、异步数据索引 |
| 函数防抖手写 | this、闭包、定时器 | 搜索框输入、窗口 resize |
| Promise.all 手写 | 异步并发、数组操作 | 多个并行请求后再渲染页面 |
| 输入 URL 全过程 | 网络、渲染原理 | 首屏优化、性能排查 |
这种表格式的整理方式,建议你也试一下。它能让知识不散落,还能在复习时很快定位到自己的薄弱点。
4.2 不要只学前端:前后端协作与全栈视角
那年笔试的开放题里,有一个让我印象很深的场景:设计一个登录流程。只懂前端的人可能会写“写一个登录表单,点击后调接口,拿到 token 存起来”。但真正站在项目角度思考的人,会继续拆出这些问题:token 过期后怎么刷新?登录态要存在 localStorage 还是 cookie?请求拦截器怎么统一加 token?遇到 401 错误如何处理?后端的接口是做了幂等设计还是需要前端防重复提交?页面权限和按钮权限分别在哪个层级控制?
如果你能理解前端和后端的协作边界,回答这类问题会一下子丰满很多。这就是很多热词里提到的“前端和后端要一起看问题”。我建议平时复习时,至少要把 HTTP 请求、状态码、RESTful 接口设计、跨域方案、Mock 数据这些内容过一遍。不需要真的写 Java 或 Go,但要知道一个请求从发起到后端处理再返回,基本流程是什么,以及后端返回的数据结构为什么会是这样。
后来我用了 HZero 这类企业级开发平台时更有体会:一个完整的前端功能往往不只是页面本身,还涉及权限点配置、菜单路由、后端数据源对接。如果你只盯着组件库文档看,很容易陷进局部,忽略了整体流程。笔试开放题想看的,就是你有没有这种全局视角。
4.3 AI工具时代,为什么基础题仍然关键
站在现在的视角回头看,前端开发环境已经发生了很大变化。AI 辅助编码越来越普遍,很多人觉得“基础还需要手写吗”开始成为一个新问题。我在复盘时依然认为,基础题不会被淘汰。
因为 AI 可以帮你快速生成代码,但它不能替你理解问题。比如你让 AI 写一个防抖函数,它可能写得很快,但当线上出现“为什么快速滚动时还是发了很多请求”的 bug,你面对的是一段别人写的代码,你需要有能力读懂闭包、读懂this、读懂事件循环,才能定位到问题。这些能力恰恰就是笔试在考的东西。
我自己的做法是:把 AI 当成“带教老师”,而不是“代写工具”。生成答案后,我要求自己能一步步解释每一行的作用,并且重新手写一遍,直到完全不依赖提示。这样训练会让基础越来越稳,而不是越来越依赖工具。
如果你现在正准备前端开发实习,我建议先别急着刷一千道题。花两天时间把 JavaScript 的核心机制理一遍,把浏览器的工作原理过一遍,把 Vue 或者 React 的框架核心逻辑搞懂一遍,再去做题,效率会高很多。基础这件事,在哪个时代都不会贬值。