从闭包到性能优化:小米2018秋招前端笔试题考点全解析

发布时间:2026/8/29 23:15:01
从闭包到性能优化:小米2018秋招前端笔试题考点全解析 1. 题目概览与考点地图1.1 我印象里的这份卷子长什么样提起小米2018秋招前端笔试题老前端应该多少都有点印象。那年的题目风格和现在动辄手写Promise/A或者用TS实现一个类型工具的路子不太一样整体更偏基础功底和工程落地能力的考察覆盖面比较广但每一道题单拎出来都不算偏门。用一句话总结就是它考的不是你背了多少新名词而是你平时写代码的时候到底有没有把底层逻辑吃透。当时我做完这套题的第一个感觉是出题人应该是个实战派因为卷子里有几道题明显是工作中踩过坑才会拿来考别人的类型。比如闭包和定时器结合的那道经典循环输出题还有关于事件循环执行顺序的题目这些知识点看起来基础但真到面试现场能完整、准确、有层次地答出来的人其实不多。这份卷子对后续准备其他大厂笔试也很有参考价值考点覆盖面广难度梯度合理从热身题到区分度题都有。1.2 整套题的难度梯度与出题逻辑我个人把这份卷子大致分为三个梯度。第一梯度是送分题主要是基础的HTML/CSS/JS语法题比如数据类型判断、作用域、选择器优先级这些这部分只要基础扎实基本不会丢分。第二梯度是拉分题集中在闭包、异步、原型链、手写函数等JS核心机制上这部分考察的是你真的理解还是你只是见过。第三梯度是拔高题包括浏览器渲染机制、性能优化策略、框架原理等这部分没有标准答案考的是知识面的广度和思考问题的深度。值得强调的一点是小米这套题的出题逻辑很务实——它不追求题目本身的新奇而是追求能不能通过题目筛出真正能干活的人。所以很多题目背后都藏着一个实际业务场景比如问如何实现图片懒加载其实是在考察你对页面性能优化的理解问跨域有哪些方案其实是在考察你平时对接后端接口时有没有真正处理过这类问题。理解了这层逻辑你备考的时候就不会死记硬背而是会去思考出题人想通过这道题知道我有没有这方面的实战经验。2. JS基础题别以为简单就能拿满分2.1 数据类型与判断typeof、instanceof、Object.prototype.toString 的三层递进这道题几乎是所有前端笔试的必备菜小米2018也没落下。题目通常会这样问typeof null的结果是什么typeof []的结果是什么instanceof的判定原理是什么如何准确判断一个变量的类型坦率说第一问很多人能答对但第二问和第三问就开始露馅了。typeof null返回object这是JS语言设计之初留下的bug但直到现在也没有修复——因为修复它会破坏大量现有代码。typeof []返回object这同样是typeof的局限它无法区分数组、对象、null这些引用类型内部的具体差异。真正能拿到满分的回答是三层递进第一层用typeof判断基础类型string、number、boolean、undefined、symbol、bigint注意typeof null是object第二层用instanceof判断引用类型的具体构造函数它的原理是通过原型链查找看构造函数的prototype是否出现在对象的原型链上第三层用Object.prototype.toString.call()做终极判断它会返回[object Array]、[object Object]、[object Null]这类精确结果。我当时备考时专门整理过一段代码可以直接回答如何实现一个通用的类型判断函数function getType(value) { const type typeof value; if (type ! object) { return type; // 基础类型直接返回 } return Object.prototype.toString .call(value) .slice(8, -1) .toLowerCase(); } // 测试 console.log(getType(null)); // null console.log(getType([])); // array console.log(getType({})); // object console.log(getType(new Date())); // date这道题背后的逻辑很清晰出题人想确认你对JS类型系统的理解不是停留在背结论层面而是真的知道每种判断方式的适用边界。如果你能顺带说一句Object.prototype.toString之所以可靠是因为它读取的是对象内部的[[Class]]属性而普通方法可以被重写或污染那这道题基本就是满分级别了。2.2 作用域与闭包经典循环输出题的多种解法演进小米这套题里有一道非常经典的闭包题给出一段for循环代码循环体内用setTimeout输出变量i问最终输出什么以及如何修改才能输出预期的结果。这道题在2018年属于烂大街的考法但放到今天依然是高频考点因为它一题能考三个核心概念作用域、闭包、异步执行机制。原题大概是这样的for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 1000); } // 输出结果是什么答案是连续输出五个5而不是0、1、2、3、4。原因是var声明的变量没有块级作用域i是全局变量等到一秒后定时器回调执行时for循环早就跑完了i已经变成5。而闭包捕获的是变量本身不是变量的值——这是最核心的考点。要修正这个问题2018年的标准答案是两种。第一种是用let替换var因为let具有块级作用域每次循环都会创建一个新的词法环境i的值会被冻结在当前迭代第二种是用立即执行函数IIFE传参把每次的i作为实参传给闭包函数形成值传递的效果。现在再回头看这道题其实还有第三种解法就是setTimeout的第三个参数for (var i 0; i 5; i) { setTimeout(function(val) { console.log(val); }, 1000, i); }第三种解法当年能写出来的人很少它利用的是setTimeout在调用回调函数时会透传额外参数的特性。如果面试时你能从var IIFE讲到let再讲到setTimeout第三参数最后再补充一句其实最根本的解决思路是让每个回调函数捕获独立的变量副本面试官对你的评价会明显不一样——因为这说明你不是死记答案而是真正理解了问题本质。3. 手写代码题这里才是真正的拉分项3.1 数组去重从能用到优雅的四个版本小米这道题要求手写数组去重看起来简单但阅卷人其实在看你能写出几个版本、每个版本考虑了哪些边界情况。我当时在考场上写了两个版本就停了后来复盘才发现这道题其实是有层次感的——它考察的是你对ES6新特性、Set的底层原理、以及对象属性机制的掌握程度。最基础的是用两层循环去重时间复杂度O(n²)能跑通但效率低好一点的是用indexOf判断是否已存在再往上是用Setconst unique (arr) [...new Set(arr)];这是最简洁的写法但在当年的考场上如果你只写这一行面试官可能会觉得你只是知道Set能去重未必真正理解它为什么能去重。因为 Set 的去重逻辑是严格相等SameValueZero所以它无法区分NaN和NaN实际上Set能去重NaN因为NaN ! NaN但SameValueZero认为它们相等也区分不了{}和{}。如果题目要求保留对象类型数组中的第一个相同引用那Set依然能处理但如果要求按JSON结构去重就得自己写递归或者用JSON.stringify做缓存键。我当时整理过一道扩展题的完整解法是对任意数组的深度去重可以补充一下const deepUnique (arr) { const cache new Map(); const result []; for (const item of arr) { const key JSON.stringify(item); if (!cache.has(key)) { cache.set(key, true); result.push(item); } } return result; };当然这种写法也有局限——对象属性顺序不同会导致JSON.stringify结果不同比如{a: 1, b: 2}和{b: 2, a: 1}会被视为不同。但这个补充思路在考场上说出来绝对是加分项。3.2 防抖与节流手写实现背后的设计思路差异防抖debounce和节流throttle是2018年小米笔试的高频手写题到现在依然是面试标配。原因是它们太常用了——搜索框输入联想、滚动事件监听、resize 窗口变化、按钮点击防重复提交随便一个真实项目里都能找到应用场景。防抖的核心思想是在事件被连续触发时只有在最后一次触发后等待指定时间才执行回调。它的实现原理是每次触发都清除上一次的定时器然后重新设置一个新的定时器。这样如果用户一直快速触发回调就永远不会执行直到停下来。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指向和调用返回函数的上下文一致。如果漏掉这行在Vue组件里用防抖函数时this就会丢失导致this.data访问报错。能主动提到这个细节的人说明是真的写过实际业务的。3.3 深拷贝从JSON.parse(JSON.stringify())到递归处理的边界讨论深拷贝这道题2018年出现频率极高小米也不例外。题目通常分两问第一问如何实现一个深拷贝第二问如果对象里有函数、Symbol、循环引用怎么处理第一问的标准偷懒答案是JSON.parse(JSON.stringify(obj))但面试官显然不会满足于这个。这个方案有几个硬伤undefined、函数、Symbol 类型的属性会被直接丢弃Date 会被转成字符串RegExp 会变成空对象如果对象存在循环引用直接报错。所以要拿高分得手写递归版本。我当时写的思路是这样的function deepClone(target, cache new WeakMap()) { if (target null || typeof target ! object) { return target; } // 处理循环引用 if (cache.has(target)) { return cache.get(target); } const clone Array.isArray(target) ? [] : {}; cache.set(target, clone); // 处理Symbol属性 const symbols Object.getOwnPropertySymbols(target); for (const sym of symbols) { clone[sym] deepClone(target[sym], cache); } for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { clone[key] deepClone(target[key], cache); } } return clone; }那段代码里WeakMap的使用是整个实现的关键。如果不是用WeakMap而是用普通对象做缓存缓存本身不仅可能造成内存泄漏而且循环引用的判断也就失效了。WeakMap的键是弱引用当原对象被回收时缓存项也会被销毁这是它适合这个场景的原因。考场上能讲清楚这一层就已经超过绝大多数候选人了。另外这里的for...in只遍历可枚举属性所以如果对象属性是用Object.defineProperty定义的enumerable: false那这个实现就不会拷贝它——这个边界可以在答题时主动提一句体现你的思考深度。4. 浏览器与网络题目解析4.1 从输入URL到页面渲染这道大综合题怎么答才能出彩小米2018的笔试题里有一道开放性的大题原题大意是在浏览器地址栏输入一个网址按下回车到页面完整显示中间发生了什么这类题目现在已经是前端面试的必考题了但2018年考和现在考侧重点略有不同。完整的回答链路大致是URL解析 → DNS解析 → TCP连接三次握手→ 发送HTTP请求 → 服务器处理并返回 → 浏览器接收响应 → 解析HTML构建DOM树 → 解析CSS构建CSSOM树 → DOM树和CSSOM树合成渲染树 → 布局Layout→ 绘制Paint→ 合成Composite。把这条链路完整拎出来已经能拿到基础分。但要拿高分每一环节都要有细节。比如DNS解析环节要能说出浏览器缓存、系统缓存、路由器缓存、本地DNS服务器、根域名服务器这个逐级查找的完整链路以及DNS缓存对性能的影响。比如TCP连接环节要能解释为什么是三次握手而不是两次确认双方收发能力都对这个点是核心。再比如HTTP请求环节要能区分HTTP/1.0、HTTP/1.1的keep-alive机制和头部字段含义。最有区分度的是渲染部分。很多候选人只会背DOM树 CSSOM树合成渲染树但深挖下去有几个关键细节第一渲染树只包含可见节点display: none的节点不会出现在渲染树中但visibility: hidden的节点会占位第二JS的加载和执行会阻塞DOM解析所以script标签建议放在body底部或者使用defer、async属性第三CSS会阻塞渲染但不会阻塞DOM解析。如果时间允许你还可以补充说明现代浏览器的渲染流水线中布局Layout和绘制Paint都会分图层最终由GPU合成这就是CSS动画为什么用transform比改left更流畅的原因。4.2 跨域问题CORS、JSONP、postMessage、代理一个都不能漏跨域是2018年笔试的常客小米这份卷子出了一道请列举跨域解决方案并说明各自的适用场景的简答题。这道题看似送分但真正答得全面的人不多因为很多人只记得JSONP和CORS对后面两种方案了解很少。先梳理一下完整的跨域方案清单JSONP、CORS、postMessage、window.name、location.hash、document.domain、WebSocket、代理服务器。其中JSONP的核心原理是利用script标签不受同源策略限制的特点动态创建script标签通过src发起带回调函数名的GET请求服务器返回callback({...})形式的JS代码浏览器执行后调用本地回调函数。它有个明显缺点只支持GET请求。CORS跨域资源共享是现代开发中最常用的方案它依赖于服务器在响应头中设置Access-Control-Allow-Origin。这里有个面试官喜欢追问的点CORS的简单请求和预检请求preflight的区别。当请求方法为GET/POST并且Content-Type为application/x-www-form-urlencoded、multipart/form-data、text/plain时属于简单请求浏览器直接发出如果携带了自定义头或者Content-Type是application/json浏览器会先发一个OPTIONS预检请求确认服务器允许后再发真实请求。能把OPTIONS请求这条路讲清楚就说明你真的处理过跨域问题。postMessage适合iframe嵌套场景比如父页面和子页面通信window.parent.postMessage(data, targetOrigin)配合window.addEventListener(message, handler)一起用接收方必须校验event.origin的合法性这是安全红线。代理服务器方案则是后端转发思路开发环境和生产环境各有实现开发环境用webpack-dev-server的proxy功能生产环境用Nginx反向代理把前端请求转发到后端服务从浏览器视角看请求是同源的自然不存在跨域问题。5. 框架与工程化题目的思考路径5.1 框架原理题不会直接问你什么是虚拟DOM2018年大厂笔试已经开始出现框架题小米也不例外。当时Vue和React正处于交替上升期题目不会直接出什么是虚拟DOM这种概念题而是放在具体的场景里考。比如为什么在Vue组件的列表中不建议使用index作为key值React中setState是同步还是异步的先说key的问题。key我们平常写的时候基本都会带但为什么不能用index、真正理解的人不多。这要追溯到虚拟DOM的diff算法。经典diff算法中有个核心假设是同一层级的节点可以通过唯一key做最小化比对当列表顺序发生变化时如果key是稳定且唯一的标识旧节点就可以被准确复用、移动而不是重建。如果使用index作为key当列表头部插入一条数据时所有后续节点的index都变了React或Vue会认为这是完全不同的节点于是全部卸载后重建不仅性能浪费而且可能导致组件状态错乱。举个最常见的例子一个带有输入框的列表第一行输入了内容在头部插了一条新数据后因为index全部变了输入框的对应关系会错乱内容看起来跑到了别的行。这就是为什么key必须用唯一的id而不是index。再说setState的同步异步问题。React的setState在合成事件和生命周期钩子里是异步的在原生事件、setTimeout、setInterval、Promise回调里是同步的。原因是React内部的batchedUpdates机制它在合成事件处理器中会把多个setState合并成一个更新避免频繁渲染。这个问题当年能解开的人很少因为它需要你确实读过React源码而不是只调过API。被问到这类题目时我的建议思路是先给结论再解释实现机制最后补充一个实际场景来证明自己真的踩过这个坑。5.2 模块化与打包CommonJS、AMD、ES Module 的演进逻辑2018年笔试里还有一类题是考察模块化规范的比如问CommonJS和ES Module的区别。这个问题放到现在依然是高频题但在2018年考它的意义更偏向于你有没有在工程化项目里解决过模块依赖问题。CommonJS是Node.js默认的模块规范使用require和module.exports。它的加载是同步的模块在加载时执行并缓存结果。ES Module是ES6引入的标准使用import和export它是静态分析、编译时加载的所有import都会被提升到模块顶部。关键区别在于CommonJS输出的是值的拷贝ES Module输出的是值的引用所以ES Module可以做到在模块内部修改一个变量外部读到的是最新值。再往深一层CommonJS是运行时加载所以它可以在if语句里写require但ES Module是编译时加载import语句必须写在顶层不能放在条件语句里。这也是为什么ES Module能支持静态分析的Tree Shaking只有依赖分析是静态的打包器才能安全地摇掉未被使用的导出。如果答到这里你可以顺带提一句webpack在打包时会把ES Module转换成浏览器可执行的代码同时保留Tree Shaking能力而CommonJS因为运行时特性做Tree Shaking非常困难这样就把模块化和打包器串起来了答题层次会高很多。5.3 性能优化一道题看你有没有大局观小米笔试有一道性能优化方向的题题目大致是你负责的页面首屏加载太慢你会从哪些维度去排查和优化。这道题没有标准答案考的是候选人的全局意识和工程经验。我当时答题的思路是按网络层、渲染层、代码层、缓存层四个维度来组织回答。网络层减少HTTP请求数合并JS/CSS、雪碧图、小图片内联base64、启用CDN加速静态资源、使用HTTP/2多路复用、按需加载路由懒加载、组件懒加载。渲染层CSS放头部、JS放底部或添加defer/async、避免CSS表达式、减少重排重绘、合理使用GIF转WebP图片、懒加载页面图片。代码层避免内存泄漏及时移除事件监听器、清理定时器、使用事件委托减少事件绑定数量、使用Web Worker处理密集型计算、对大列表做虚拟滚动。缓存层合理设置Cache-Control和Expires头、使用ETag做缓存校验、CDN缓存策略、Service Worker离线缓存。这道题的加分项是你能说出**优先优化哪一块**的判断逻辑而不是把所有方案都罗列一遍。比如你可以说如果首屏加载时间超过3秒我会先用Chrome DevTools的Network面板看是什么资源占了大头如果是图片考虑压缩和懒加载如果是JS考虑拆包按需加载如果HTML本身太大考虑服务端渲染或流式渲染。这种思路展示的是解决问题的能力而不是背诵能力。6. 面试官视角的备考建议与避坑指南6.1 从2018到现在的题型变化哪些没变哪些早变了以2026年的视角复盘2018年的笔试题一个很明显的感受是基础题的考察逻辑几乎没变变的是考察载体。当年的var和let的循环输出题现在可能变成了这段TS代码里的类型推断结果是什么当年的手写深拷贝现在可能变成了实现一个带深浅控制参数的工具函数。但核心考察点始终是那几个作用域与闭包、异步与事件循环、原型链与继承、数据结构的灵活运用。另一个明显变化是现在的大厂笔试越来越偏重工程化场景比如给你一段“有问题的webpack配置”让你找出影响构建速度的原因或者给你一个“线上白屏”的排查场景让你按排查顺序写出步骤。这种从考知识点向考排查能力的转变意味着准备面试时不能只刷题而是要真真切切地动手去搭项目、排查问题、看报错。你踩过的每一个坑可能在笔试时都是加分项。还有一个趋势是随着AI编程工具的普及现在的部分笔试题已经开始允许使用AI辅助但面试官会追问你为什么会选择这个实现方案这个AI生成的代码有什么边界问题反而不太考察手写代码的速度了。但这不意味着基础能力不重要——恰恰相反只有你真正理解底层原理才能判断AI生成的代码是否合理、哪里需要加缓存、哪里会有内存泄漏风险。6.2 考场上最容易丢分的五个习惯第一只看结果不看过程。手写题阅卷时会看代码结构就算最终结果不对如果思路清晰、步骤完整也能拿到大部分分数。所以遇到不会写的题目尽量把思考过程写下来不要留白。第二忽略边界条件。手写深拷贝忽略了循环引用手写防抖忽略了this绑定手写数组去重忽略了NaN——这些都是考场上最常见的丢分点。宁可多一点代码也要把边界条件说清楚这恰恰是程序员思维和会写代码之间的区别。第三时间分配失衡。整套笔试题的时间是有限的前面选择题纠结太久后面手写题就来不及。我自己的经验是遇到卡壳的题先标记跳过先把有把握的题写完再回头啃硬骨头至少保证会做的题都拿到分。第四只答不解释。笔试题比面试题更残酷的地方在于你没法通过讲解来挽救评分。所以答题时尽量写出你的思路注释比如在关键代码行后加一行// 防抖核心清除上一次定时器阅卷人扫一眼就能理解你的思路。第五对框架题的态度。很多候选人看到框架题就紧张觉得自己水平不够。其实大厂的框架题大多是以框架为背景考JS基础把虚拟DOM原理拆到底层依然是JS对象和树形结构遍历的问题。所以备考时与其背框架源码不如把基础的数据结构和算法补扎实。6.3 回到备考本身一份可持续刷题的路线清单刷题不是目的真正提升能力才是。我的个人建议是分三条线并行准备第一条线是基础知识线把JS核心机制作用域、闭包、原型、异步、this绑定过三遍以上每过一遍都尝试不看资料手写实现第二条线是工程线动手做一个包含打包配置、代码规范、接口请求、错误监控的中型项目把所有考过的工程化知识点在真实项目里跑一遍第三条线是算法线按数组、字符串、链表、二叉树、动态规划的顺序刷题每天保持1-2道目标是看到题目能快速判断这题考什么、用什么思路解。笔试只是第一步它考察的是你有没有资格进入面试环节。在笔试中展现出来的不只是会不会还有是否具有工程师的思维习惯——比如面对问题时的分解能力、边界意识、表达条理。这套能力练好了不管是2018年的卷子还是2026年的卷子都是同一个内核。最后分享一个我在实际面试候选人时的观察很多人笔试分数高但到了面试环节一聊就发现所有的知识点都是背出来的没有形成自己的理解框架。而少数笔试中等偏上的人因为能在答案里写出为什么这么实现有什么替代方案有哪些边界情况反而成为最终拿下offer的人。所以刷题的时候多问自己一句为什么比多刷十道题更有价值。这也是我做完小米这套2018年笔试题后最大的收获。