ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

小米2019秋招前端笔试题B解析:JS基础到框架原理考点全梳理

2026/8/29 23:14:54 拓冰建站 浏览量
小米2019秋招前端笔试题B解析:JS基础到框架原理考点全梳理 这套题我当年刷过不下三遍每次重看都有新收获。小米2019秋招前端笔试题B题型覆盖了JS基础、浏览器机制、框架原理和手写代码整体难度中等偏上但坑点很密。很多同学觉得笔试题就是背八股其实不是这套卷子里的每一道题背后都对应着一个实际开发中会遇到的场景。这篇博文我把整套卷子的考点拆开揉碎结合我当时刷题和后来做面试官的经验把每道题的解题思路、背后原理、容易踩的坑都讲一遍希望能帮你理清复习方向。1. 先看整体这套卷子到底想考什么1.1 题型分布与时间分配先看整体结构。这份试卷大致可以分为四个模块每个模块的侧重点不一样模块考查内容建议用时分值占比估算JS基础变量提升、this指向、闭包、原型链15分钟30%浏览器与异步事件循环、渲染机制、存储、跨域15分钟25%框架与工程化Vue/React原理、模块化、构建10分钟20%手写代码防抖节流、深拷贝、数组去重等20分钟25%这个时间分配是我实测下来的节奏先做手写代码题因为这类题分值高、答案确定写出来就是得分再做JS基础和浏览器题目这些题需要思考但不太会卡壳最后做框架和工程化的开放题这类题没有标准答案言之有理即可。1.2 出题思路拆解2019年这个时间节点很有意思。当时Vue 3还没正式发布React 16.8刚推出Hooks不久前端框架正处于新旧交替的时期。所以这份卷子的出题思路明显偏向“基础能力优先框架能力验证为辅”。它不像社招面试那样深挖源码而是更关注你作为一个应届生有没有扎实的JS功底、知不知道浏览器工作原理、有没有基本的工程化意识。换句话说这套题筛选的不是“用过什么框架”的人而是“能不能独立解决前端问题”的人。很多同学刷题时只关注正确答案忽略了题目背后的筛选逻辑这是最可惜的。2. JS基础变量提升、作用域与闭包2.1 经典变量提升题var、let、const三兄弟这份卷子里有一道很典型的变量提升题基本上每年秋招都会换个马甲出现var a 1; function foo() { console.log(a); var a 2; } foo();输出结果是undefined不是1也不是2或者报错。原因在于var声明会被提升到函数作用域顶部但赋值不会被提升。所以这段代码实际上等价于var a 1; function foo() { var a; // 声明提升覆盖了全局的 a console.log(a); // 此时 a 是 undefined a 2; // 赋值发生在打印之后 } foo();这里最迷惑人的点在于全局明明有var a 1为什么函数内部的console.log(a)打印的是undefined而不是1因为函数内部的var a声明会遮蔽全局变量哪怕这个声明在打印语句之后。JavaScript的作用域是词法作用域函数内部只要声明了同名变量整个函数作用域内引用的都是这个内部变量。类似的题还会换成letlet a 1; function foo() { console.log(a); let a 2; } foo();这题就会直接报错ReferenceError因为let存在暂时性死区TDZ。变量虽然被提升了但在初始化之前访问会报错。这个区别在笔试里出现频率极高值得单独记一下。2.2 this指向四种绑定规则一次说清this指向是前端笔试的常青树这套卷子自然不会放过。题目一般是给出几段代码让你写出this的指向。我总结的规律是this指向只有四种情况按优先级从高到低排列第一种new绑定。用new调用构造函数时this指向新创建的对象。第二种显式绑定。通过call、apply、bind调用时this指向传入的第一个参数。第三种隐式绑定。通过对象调用方法时this指向这个对象比如obj.foo()中this指向obj。第四种默认绑定。独立函数调用时this指向全局对象浏览器里是window严格模式下是undefined。笔试里最坑的是隐式绑定丢失的情况var name window; var obj { name: obj, sayName: function() { console.log(this.name); } }; var fn obj.sayName; fn();输出是window。因为var fn obj.sayName只是把函数引用赋值给了fn调用时fn()是独立调用this就走默认绑定指向了window。很多同学第一眼觉得是obj就是没搞清楚“谁调用this就指向谁”这句话的本质。**this的指向是在调用时确定的不是在定义时确定的。**唯一例外是箭头函数它不绑定this会沿用定义时外层作用域的this。2.3 闭包考的不是背概念是看你会不会用闭包题在这套卷子里不是单独出一道概念题而是混合在功能实现题里考。比如让你实现一个计数器只能通过特定方法修改值function createCounter() { var count 0; return { increment: function() { count; }, decrement: function() { count--; }, getCount: function() { return count; } }; } var counter createCounter(); counter.increment(); counter.increment(); counter.getCount(); // 2这道题的核心考点有三层第一层知道count变量不会被垃圾回收因为外部函数返回的闭包还在引用它。这是闭包的基本概念。第二层理解闭包不只是“函数嵌套函数”这么简单它本质上是一种“数据封装”机制类似其他语言的私有变量。这在JavaScript里特别重要因为ES5之前没有class语法很多模块化方案都依赖闭包。第三层也是容易忽略的闭包虽然好用但会造成内存占用大量使用或者使用不当会造成内存泄漏。面试官如果追问“闭包的缺点”你答不出来就露馅了。3. ES6与异步编程Promise、async/await、事件循环3.1 事件循环这道输出顺序题错的人最多异步输出顺序题是每次前端笔试的重头戏小米这套B卷也有一道。类似的题目长这样console.log(script start); setTimeout(function() { console.log(setTimeout); }, 0); Promise.resolve().then(function() { console.log(promise1); }).then(function() { console.log(promise2); }); console.log(script end);正确的输出顺序是script start→script end→promise1→promise2→setTimeout。这道题的关键在于理解事件循环的宏观流程当前宏任务里同步代码先执行完然后清空微任务队列最后才从宏任务队列里取下一个任务执行。setTimeout的回调是宏任务Promise.then的回调是微任务。执行完同步代码后事件循环会先检查微任务队列把promise1和promise2依次执行完然后才执行setTimeout。哪怕setTimeout延迟是0它也要等到下一轮事件循环。后来题目升级会加上async/awaitasync function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); async1(); console.log(script end);这个输出顺序是script start→async1 start→async2→script end→async1 end。注意await后面的代码相当于放进了微任务队列所以async1 end要在当前同步代码全部执行完之后才打印。3.2 Promise、async/await混用别凭感觉做题笔试里还有一种进阶考法把Promise的构造函数和async函数混在一起new Promise((resolve, reject) { console.log(promise constructor); resolve(); }) .then(() { console.log(then 1); }) .then(() { console.log(then 2); });这道题的坑在于new Promise里的执行器函数是同步执行的所以promise constructor会立刻打印后面的then回调才是异步执行的。我见过很多同学把构造函数里的代码也当成异步这个误区一定要纠正。其实这类题的解题思路很机械按三步走就不会错先把所有同步代码划出来按顺序执行。把微任务Promise.then、await后面、queueMicrotask和宏任务setTimeout、setInterval分别列队。每一轮事件循环都是“执行一个宏任务 → 清空所有微任务”。只要养成这样的解题习惯不管题目怎么变形都不会被绕进去。3.3 解构、展开、默认参数新特性反而容易忽略细节ES6的新语法在这套卷子里也有涉及但考的不深主要是考查解构赋值、展开运算符、默认参数这些常用特性。有一道题很典型let { a, b 2, ...rest } { a: 1, c: 3, d: 4 }; console.log(a, b, rest);输出结果是1 2 { c: 3, d: 4 }。注意两点第一解构时给b设置了默认值2所以当对象里没有b属性时不报错直接用默认值第二...rest会收集剩余的可枚举属性组成一个新对象。还有一道关于默认参数和展开运算符的function foo(x, ...args) { console.log(x, args); } foo(1, 2, 3, 4);输出是1 [2, 3, 4]。注意args接收的是一个真数组不是类数组arguments。这在很多业务场景里很有用比如写一个求和函数function sum(...numbers) { return numbers.reduce((acc, cur) acc cur, 0); }需要补充的是类数组arguments没有map、filter、reduce这些数组方法而...args生成的数组天生就有。这也是面试官爱问“arguments和...args有什么区别”的原因答出这一点就比背概念的同学高一层。4. 浏览器机制事件循环之外必考的几块硬骨头4.1 从输入URL到页面渲染浏览器做了什么这类题属于基础中的基础但也是很多同学答不全的题。完整流程可以概括为输入URL浏览器解析判断是搜索关键词还是合法URL。DNS解析把域名解析成IP地址浏览器有DNS缓存系统也有缓存都没有才走递归查询。建立TCP连接三次握手。发送HTTP请求带上请求头、Cookie等信息。服务器处理请求并返回浏览器拿到HTML、CSS、JS资源。浏览器解析HTML构建DOM树解析CSS构建CSSOM树。合并DOM树和CSSOM树生成渲染树Render Tree。布局Layout/Reflow计算每个元素的位置和尺寸。绘制Paint把渲染树绘制到屏幕上。笔试中经常在“渲染”这个环节挖坑。比如script标签放在head里会阻塞页面渲染因为JS执行时可能修改DOM和CSSOM浏览器必须等脚本执行完才能继续构建DOM。这也是为什么实践上建议把script放在/body之前或者给外链脚本加defer/async属性。defer和async的区别也经常被单独拎出来考defer是延迟执行脚本在文档解析完成后执行多个defer脚本按顺序执行async是异步执行下载完就立刻执行多个async脚本不保证执行顺序。4.2 存储方案对比cookie、localStorage、sessionStorage、indexedDB浏览器存储也是高频考点尤其喜欢用表格题的方式考区别。这份卷子就有一道对比题核心差异如下存储方式容量生命周期发送到服务器适用场景cookie4KB左右可设置过期时间每次请求自动携带会话管理、用户标识localStorage5MB左右永久需手动清除不会不常变的业务数据sessionStorage5MB左右标签页关闭即清除不会临时表单数据indexedDB大GB级别永久不会大量结构化数据、离线应用有个隐藏考点是localStorage的存储键值只能是字符串如果存对象需要JSON.stringify取出来再JSON.parse。很多同学在笔试里直接localStorage.setItem(key, obj)然后取出来发现是[object Object]这属于基础不牢。另外一个容易踩坑的点是cookie的跨域携带规则cookie默认遵循同源策略但可以通过设置Domain字段实现二级域名之间的共享。如果后端需要读取cookie中的用户信息还得注意HttpOnly属性——设置了这个属性的cookie前端JS是读不到的这能有效防止XSS攻击窃取cookie。4.3 跨域原理、CORS、JSONP与常见考题跨域是前端面试里绕不开的话题笔试题的典型问法是“列举几种常见的跨域解决方案并说明原理”。你至少得答出三种第一种CORS跨域资源共享。这是官方推荐的跨域方案原理是服务器在响应头里加Access-Control-Allow-Origin等字段告诉浏览器“这个源可以访问我的资源”。前端不需要做任何特殊处理只需要后端配置响应头。如果是携带cookie的请求还需要设置Access-Control-Allow-Credentials: true并且不能把Access-Control-Allow-Origin设为*。第二种JSONP。原理是利用script标签的src属性不受同源策略限制的特性。动态创建script标签在URL上带一个回调函数名后端返回一段JS代码并调用这个回调函数。缺点是只能支持GET请求而且现在已经用得少了但笔试还是会考。第三种代理转发开发环境常用。在webpack或Vite的devServer里配置proxy让开发服务器去请求后端接口再把响应转发给前端。原理是服务器之间没有同源策略限制浏览器同源策略只管浏览器。要提醒的是很多同学搞混了“跨域”和“请求发不出去”的关系。实际上跨域时请求是发出去的服务器也正常处理了只是浏览器在收到响应时发现响应头里没有允许跨域的字段把响应拦截了。搞懂这一点在回答CORS原理时就能更准确。5. 框架与工程化Vue/React和构建工具怎么考5.1 响应式原理Vue2和Vue3的核心区别2019年那会儿Vue 2还是主流所以这套卷子考的是Vue 2的响应式原理但你在回答时可以顺带提一句Vue 3的变化显得知识体系更新。Vue 2的响应式原理核心是Object.defineProperty。它遍历data对象的每一个属性用Object.defineProperty把每个属性转为getter/setter。当组件渲染时读取属性会触发getter收集依赖当属性被修改时触发setter通知依赖更新。这个方案有几个天然的缺陷第一个缺陷无法检测对象属性的新增和删除。所以Vue 2才提供了Vue.set和Vue.delete来解决这个问题。第二个缺陷无法直接检测数组的索引变化和长度变化。也就是说arr[0] xxx这种操作不是响应式的arr.length 0也不是。Vue 2通过重写数组的变异方法push、pop、splice等来规避这个问题但直接用索引修改仍然不行。第三个缺陷初始化时递归遍历所有属性对象层级越深性能消耗越大。Vue 3用了Proxy来重写这套机制可以直接代理整个对象不需要递归遍历天然支持属性新增、删除和数组索引变化性能也更好。如果你在笔试里能把Vue 2的缺陷和Vue 3的改进都写出来这个答案就比标准答案更有竞争力。5.2 diff算法与key的作用笔试里的框架题很少直接问“diff算法是什么”更多是给你一个场景比如“为什么v-for要绑定key不绑定会有什么问题”。这个问题要分两层回答第一层diff算法本质是“同层比较”。Vue的虚拟DOM在做diff时只会比较同一层级的节点不会跨层级比较因为跨层级比较的复杂度太高。第二层key的作用是给节点一个唯一标识帮助diff算法在“列表节点顺序变化”时更高效地复用节点。没有keyVue只能按位置比较如果列表首部插入一个元素后面的所有节点都会被重渲染有keyVue能识别出哪些节点可以复用只需要插入一个新节点就行。顺便提一句不推荐用数组索引当key。因为如果列表顺序发生变化比如排序、插入索引会跟着变它对应的数据就错了Vue复用节点时就会复用错的内容导致状态错乱。这是一个笔试里很容易踩的坑也是面试官期望你主动说出的加分项。5.3 工程化模块化、打包、性能优化框架之外的工程化问题这套卷子主要考两方面模块化规范和性能优化手段。模块化规范里最常考的是ES Module和CommonJS的区别。简单总结语法不同ES Module用import/exportCommonJS用require/module.exports。加载时机不同ES Module是编译时静态加载CommonJS是运行时动态加载。输出方式不同ES Module输出值的引用CommonJS输出值的拷贝。浏览器支持不同ES Module是浏览器的原生标准CommonJS是Node.js的模块规范。性能优化方面笔试题常见的问法是“列举你能想到的前端性能优化策略”。你需要从网络层、渲染层、构建层三个维度回答网络层CDN加速、HTTP缓存强缓存和协商缓存、资源压缩合并、DNS预解析。渲染层减少重排重绘、避免频繁操作DOM、用事件委托、图片懒加载和占位符、CSS动画代替JS动画。构建层代码分割、按需加载、Tree Shaking、提取公共代码。笔试答这类题最忌讳“只有名词没有解释”每个策略最好能说清楚为什么有效。比如“图片懒加载为什么能优化性能”原因是减少初始请求数量降低首屏加载时间。6. 手写代码题这些必考题我建议你背下来6.1 防抖与节流先理解场景再写代码手写防抖和节流基本是前端笔试的标配小米这套卷子也有。这两道题最重要的是不要写错应用场景很多同学代码能写对但说不清区别。防抖debounce的原理是事件触发后等待一段时间再执行回调如果在这段时间内又触发了就重新计时。典型场景是输入框实时搜索用户连续打字时不发请求等用户停止输入后再发。function debounce(fn, delay) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }注意这里一定要用...args收集参数并且用fn.apply(this, args)绑定this否则在Vue组件里使用时会拿不到组件的this。节流throttle的原理是事件触发后在固定时间间隔内只执行一次回调。典型场景是滚动事件监听、resize事件防止回调执行过于频繁。function throttle(fn, interval) { let last 0; return function(...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }更简单的一种节流写法是用定时器function throttle(fn, interval) { let canRun true; return function(...args) { if (!canRun) return; canRun false; setTimeout(() { fn.apply(this, args); canRun true; }, interval); }; }这两种写法的核心都是“在一个时间窗口内只执行一次”区别是第一种记录的是时间戳第二种用的是锁标记。笔试写任何一种都能得分但建议把两种都掌握面试官追问时能补充说明各自的优劣。6.2 深拷贝别只会JSON.parse(JSON.stringify(obj))深拷贝题几乎每份前端笔试题都会出现。如果你只会写JSON.parse(JSON.stringify(obj))这一种面试官大概率会追问“这个方案有什么问题”。最常见的问题是拷贝不了undefined、Symbol、函数。拷贝不了循环引用会直接报错。Date、RegExp、Map、Set等特殊对象会变成普通对象或丢失类型。会丢弃对象原型链上的自定义方法。所以手写一个完整的深拷贝是必练的基本功function deepClone(obj, hash new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); if (hash.has(obj)) return hash.get(obj); let cloneObj new obj.constructor(); hash.set(obj, cloneObj); for (let key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] deepClone(obj[key], hash); } } return cloneObj; }这里用WeakMap来处理循环引用因为WeakMap的键是弱引用的不会造成内存泄漏。new obj.constructor()是为了保留原对象的构造函数。笔试时能写出带WeakMap处理循环引用的版本已经能超过大多数人了。6.3 数组去重与拍平多种解法体现功力数组去重和数组拍平是手写题里的常客考察的是你对数组API的熟练程度和解题的多样性。数组去重至少可以写出三种解法解法一利用Set。const unique arr [...new Set(arr)];解法二利用filter和indexOf。const unique arr arr.filter((item, index) arr.indexOf(item) index);解法三利用reduce。const unique arr arr.reduce((acc, cur) { return acc.includes(cur) ? acc : [...acc, cur]; }, []);笔试时建议先写Set解法因为它最简洁再补充其他解法展示功底。如果题目要求对对象数组去重就不能用Set了需要指定去重字段。数组拍平也有多种写法最常考的是手写flatfunction flat(arr, depth Infinity) { return arr.flat(depth); }但面试官想看的显然不是调API而是递归实现function flat(arr, depth 1) { if (depth 1) return arr.slice(); return arr.reduce((acc, item) { return acc.concat(Array.isArray(item) ? flat(item, depth - 1) : item); }, []); }这两个手写题的核心价值不在“背代码”而在于让你理解reduce、concat、Set这些API的语义和用法真正遇到复杂的业务数据转换时才能游刃有余。7. 复盘与应试策略从做题到拿offer的最后一公里7.1 常见错误与时间分配我刷了那么多套笔试题也帮朋友看过不少答卷发现大家在笔试题上栽跟头的原因出奇一致。**第一个常见错误是时间分配失衡。**有些同学在一道手写代码题上死磕太久导致后面的选择题和简答题没时间做。手写题如果卡了五六分钟还没思路我的建议是先跳过去做后面的题目等所有确定能拿分的题做完再回头攻坚。笔试的评分往往不是按题给分的而是整体看的宁可所有题都写一点也不要一道题写完美而其他题全空。**第二个常见错误是只写答案不写过程。**尤其是简答题比如“说说你对闭包的理解”有些同学就写一行“函数内部返回函数”。这种答案通常只能拿到及格分。正确的答法是先给定义再举代码例子最后说应用场景和注意点。一个结构完整的答案即使表达不够精准也会因为信息量充足而得分更高。**第三个常见错误是框架题答不到点子上。**比如问“为什么v-for要绑定key”答“为了性能更好”只拿一半分必须把diff算法复用节点的原理说出来。框架题在笔试中往往篇幅不大但分值占比并不低值得你认真对待。7.2 基于这套题的高频考点复习清单基于这套试卷我整理了一份复习优先级清单按照出题概率和重要性排序事件循环输出题必考必须练到能背出执行顺序this指向题必考四种绑定规则熟练掌握防抖节流手写必考代码要能默写深拷贝手写高频至少掌握能处理循环引用的版本Vue响应式原理与diff算法高频Vue3的Proxy原理要了解浏览器渲染流程高频重点关注script标签的位置影响常见跨域方案高频能说出原理和适用场景数组去重与拍平中高频至少掌握两到三种解法ES6新特性细节中频解构赋值、展开运算符、默认参数前端性能优化中频从网络、渲染、构建三个维度回答这套题做完以后不要急着对完答案就扔到一边。我的习惯是把错题单独整理到一个文档里每道题不仅记录正确答案还写下我当时为什么选错——是对概念没理解还是被题目陷阱干扰了。面试前一周翻一遍这个错题本比盲目刷十套新题都有效。另外多嘴提醒一句刷题的时候可以看看别人的解题思路但一定不要直接背答案。笔试的变体很多把原理吃透了不管题目怎么变形都能应对。我自己当面试官时见到那种只会背答案、一问原理就卡壳的候选人基本是不会给过的。反过来哪怕你某道题没做对但能清晰说出你的思路和卡点印象分会好很多。