十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蘑菇街前端笔试题解读:从JS核心机制到工程实战

蘑菇街前端笔试题解读:从JS核心机制到工程实战 1. 蘑菇街前端校招笔试题考什么从试卷结构看选人逻辑蘑菇街2019届校招前端开发工程师笔试题放在今天回看依然有很强的参考价值。那几年正好是互联网公司校招笔试风格分化的时期——有的公司上来就是五道算法题根本不看前端基础有的公司则偏向于问一堆框架API背过就有分。蘑菇街这套题属于相对综合的那一类JavaScript基础、浏览器机制、前端工程化、手写代码题都有覆盖整体难度中等偏上区分度做得很不错。先说结论这套题考察的不是“你背了多少框架API”而是“你有没有真正理解前端运行时的底层逻辑”。如果你只是会用Vue或者React写页面没搞懂原型链、事件循环、闭包这些底层概念做题会非常吃力。反过来说如果你把这套题吃透了后面面其他电商类公司比如有赞、拼多多、唯品会类似的业务方向的前端岗也基本够用。我当时拿到这套题的第一感受是题目数量不算多但每一道都能往外延伸出三四个面试追问。这就是典型的“笔试当面试用”的出题思路——通过有限几个考点快速判断候选人是不是真正写过代码、真正调试过线上问题的人。接下来我按照试卷的常见结构把这套题的考查逻辑拆开来逐项说每个考点我都会补充原理分析和答题思路不是单纯对答案。1.1 试卷结构从题目分布看公司需求2019届那会儿的校招笔试题一般分三块选择题、简答题、编程题。蘑菇街这套题也沿用这个结构但在每个板块内的选材有自己的倾向。选择题部分主要覆盖了HTML/CSS基础语义化标签、盒模型、flex布局、JavaScript核心变量提升、闭包、this指向、原型链、浏览器与网络HTTP状态码、缓存机制、事件冒泡捕获。这些题目本身不算偏但选项设置得很“刁钻”很多是一眼看上去两个选项都对需要你从底层机制层面去分辨。比如“以下关于闭包的说法正确的是”这种题四个选项里通常会有一个“闭包会让所有外部变量永远无法被垃圾回收”这种半对半错的表述用来筛掉那些对GC机制理解不透彻的人。简答题部分一般会有一道手写代码题比如“实现一个类数组转数组的方法”“手写一个简单的发布订阅模式”这类。我当时遇到的是发布订阅而且题目还额外要求考虑“事件回调里this指向”的问题——这就是在同一个考点上叠加了第二个考点如果只是背过发布订阅的模板是拿不到满分的。编程题部分通常是两道一道偏算法数组去重、排序变体、字符串处理一道偏应用DOM操作、事件处理、数据格式化。算法题难度不算高但是对时间复杂度的要求会写清楚考生需要在代码注释里说明自己的思路。应用题的输入数据往往模拟电商场景比如给一份商品列表需要按照销量和价格排序后渲染到页面上这类题目很贴合蘑菇街做导购电商的业务背景。1.2 为什么这样出题考察目标分析从出题逻辑来看蘑菇街前端笔试有几个明确的筛选目标。第一筛掉“只会写业务、不懂原理”的同学。选择题和简答题里大量考察JavaScript底层机制目的就是确认候选人不只是会调接口、渲染列表而是真正理解这门语言的运行方式。电商前端每天面对的是高并发页面、复杂的交互状态、需要做性能优化的长列表如果对事件循环、内存回收这些机制没有概念迟早会在线上埋雷。第二筛掉“代码规范差、没有工程意识”的同学。编程题里虽然没有明确要求写出webpack配置但对代码风格、函数拆分、变量命名都有潜在考察。面试官后续评卷时会特别关注代码里有没有魔法数字、有没有重复逻辑、有没有写出一眼看不懂的嵌套循环。第三筛掉“只会前端、不懂协作”的同学。简答题里会出现“如果后端接口返回的数据结构不符合前端预期你会怎么处理”这不是单纯的技术题也在考察候选人的沟通意识和项目协作能力。在电商公司前端和后端之间的数据契约问题非常常见处理方式是直接骂后端、还是提需求让后端改、还是前端主动兼容并反馈问题这道题的表达方式能看出一个人的工作习惯。所以这套题本质上不是在考“你会不会写代码”而是在考“你有没有独立负责一个前端模块的能力”。想清楚这一点你的复习重点就清晰了不要死背题要理解每一道题背后的设计意图。2. JavaScript基础考点高频题背后的原理2.1 原型链与this指向一道题看穿你的JS底子蘑菇街笔试的选择题和简答题里必然会有原型链相关的题这几乎是前端笔试的标配。原型链这一块考察的点很集中prototype和__proto__的区别、构造函数和实例的关系、Object.create的作用、原型链查找的规则。我印象比较深的一道题类似这样function Foo() { this.name foo; } Foo.prototype.getName function() { return this.name; }; const bar new Foo(); console.log(bar.getName()); // 输出什么 console.log(bar.hasOwnProperty(name)); // 输出什么答案是foo和true。第一问没问题bar是Foo的实例getName方法挂在Foo.prototype上bar通过原型链找到这个方法方法里的this指向调用者也就是bar所以返回this.name也就是foo。第二问考察的是实例属性和原型属性的区别name是在构造函数里通过this.name赋值的它是bar自己的属性所以hasOwnProperty返回true。这道题本身不难但是后面通常会跟着一个追问如果我把getName改成箭头函数呢比如Foo.prototype.getName () { return this.name; }; console.log(bar.getName()); // 输出什么答案是undefined。原因在于箭头函数没有自己的this它沿用了定义时所在作用域的this。getName定义在Foo.prototype上属于模块/全局作用域所以这里的this指向全局对象浏览器里是window而window.name在浏览器里是一个特殊属性默认是空字符串不是foo严格模式下甚至会报错。这类变形题考的就是你能不能理解this的绑定规则在不同函数类型下的区别。答题要点总结一下this的绑定看调用方式不看定义位置箭头函数的this看定义位置不看调用方式new调用时this指向新创建的实例对象原型链查找只发生在读取属性时赋值属性不会沿着原型链去找2.2 事件循环与异步宏任务和微任务怎么区分前端笔试考事件循环几乎成了固定项目蘑菇街也不例外。这类题通常给出几段代码让你写出输出顺序比如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。这里就涉及事件循环的核心概念——宏任务MacroTask和微任务MicroTask。简单理解宏任务setTimeout、setInterval、setImmediate、I/O操作、UI渲染每次事件循环只取出一个宏任务执行微任务Promise.then、queueMicrotask、MutationObserver在每一个宏任务执行完毕后会把当前队列里所有的微任务全部执行完再渲染所以Promise虽然是异步但它的回调排在微任务队列里会比下一轮的宏任务setTimeout先执行。这就像你去餐厅吃饭宏任务是等待叫号的顾客微任务是服务员在你点完菜之后立刻帮你加的水——每一桌顾客都会在轮到他们的时候先把水喝完下一个顾客才来。我在看笔试答案的时候发现很多同学能记住promise1在setTimeout之前输出但忽略了promise2也会在setTimeout之前输出。原因是promise1的.then回调里又注册了一个.then这个新注册的微任务会排进当前微任务队列的尾部在当前宏任务结束后一起执行完。所以只要是同一轮宏任务内产生的微任务都会在下一个宏任务开始前执行完毕。这类题要拿满分关键在于理解“一个宏任务结束后清空当前微任务队列”这个机制而不是死记输出顺序。2.3 闭包与作用域最容易丢分的概念题闭包在蘑菇街笔试里基本是必考的但考法往往比较“弯”。最经典的一道题就是循环闭包问题for (var i 0; i 3; i) { setTimeout(() { console.log(i); }, 100); }输出是3、3、3很多刚刷题的人会以为是0、1、2。根本原因是var声明的i是函数作用域的循环结束后i已经变成3setTimeout的回调在后续宏任务中执行访问到的自然是同一个作用域下的最终值3。如果改成let声明for (let i 0; i 3; i) { setTimeout(() { console.log(i); }, 100); }输出是0、1、2。因为let是块级作用域每次循环都会创建一个新的绑定闭包捕获的是每次迭代中独立的i。有些同学知道答案但解释不清楚原理在简答题里只写“因为let有块级作用域”是不够的。面试官真正想听的是let在for循环的每次迭代中会创建一个新的词法环境编译器会在循环体内为每一轮迭代创建独立的绑定闭包引用的是当前迭代环境中的i而不是循环外部的同一个变量。我当时是这么记的把var版本的循环想象成大家都在看同一块白板循环每跑一次就把白板上的数字擦掉重写最后所有人都看到那个最终数字而let版本是每次循环都发一张新纸上面写着当前迭代的i值闭包捕获的是各自的纸片互不干扰。这个概念想通了闭包题基本不会再错。3. 编程题与手写题从思路到实现细节3.1 数组去重与排序不要只会用Set数组去重是前端笔试出场率最高的编程题蘑菇街也考了。很多同学一看题就写const unique (arr) [...new Set(arr)];这确实能实现但通常只能拿基础分。因为题目往往会追加要求保持原数组顺序、处理对象数组去重、考虑性能大数据量。如果题目要求去重的同时保持首次出现顺序用Set其实也能做到因为Set本身是按插入顺序迭代的。但如果是对对象数组去重比如按id字段去重就需要另外处理const uniqueById (arr) { const seen new Map(); const result []; for (const item of arr) { if (!seen.has(item.id)) { seen.set(item.id, true); result.push(item); } } return result; };这里用Map而不是Object来记录已经出现的id是因为Map的键可以是任意类型而且遍历顺序是有序的。用Object的话键会被转换成字符串如果id本身是对象或者数字1和字符串1混在一起就会出现问题。再往深一层题目的追加问题可能是如果数组有100万个元素怎么优化去重性能Set去重的时间复杂度是O(n)已经是线性级别一般够用了。但如果数组元素是复杂的嵌套对象Set无法进行深比较这时候可以考虑序列化方式去重const uniqueByJson (arr) { const seen new Set(); return arr.filter(item { const key JSON.stringify(item); return seen.has(key) ? false : (seen.add(key), true); }); };这个方案有局限性比如对象键的顺序不同会导致序列化结果不同{a:1,b:2}和{b:2,a:1}会当成两个不同的对象但如果面试时作为补充方案提出来能体现你考虑问题的全面性。3.2 手写Promise理解状态机是关键蘑菇街笔试的编程题里有较大概率出现“手写一个简化版Promise”这道题。我当时在答题的时候差点做不完因为平时都是调用Promise很少自己去实现它。这里分享一个我后来总结的快速实现思路。核心要点就三个Promise有三种状态pending、fulfilled、rejected状态只能从pending转换到后两者且不可逆then方法注册回调但回调需要延后到状态改变后执行then方法返回一个新的Promise实现链式调用一个简化但能跑通主体逻辑的版本class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state pending) { this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach(fn fn()); } }; const reject (reason) { if (this.state pending) { this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach(fn fn()); } }; try { executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { if (this.state fulfilled) { onFulfilled(this.value); } if (this.state rejected) { onRejected(this.reason); } if (this.state pending) { this.onFulfilledCallbacks.push(() onFulfilled(this.value)); this.onRejectedCallbacks.push(() onRejected(this.reason)); } } }这个版本没有实现链式调用then没有返回新的Promise也没有处理异步穿透和值透传但如果面试时问“请实现一个Promise”先把状态机框架写出来再逐步补充细节思路是清晰的。最忌一上来就想写完整版结果写到一半逻辑乱掉。我在实际面试中发现面试官对“promise.then返回新promise”的考察很执着因为这是链式调用的基础。简化版里then没有返回值导致promise.then().then()直接报错。补上链式调用需要对resolvePromise做递归处理这已经是完整的Promise/A规范了笔试时间有限的情况下先把状态机和事件回调处理好已经能拿大部分分数。3.3 防抖与节流高频面试题的实现细节防抖debounce和节流throttle是蘑菇街笔试中被多次考到的手写题而且经常要求“至少写出一种并说明使用场景”。这两个概念虽然简单但很多人在现场手写时会漏掉边界情况。防抖的核心思路是事件被连续触发时只有在最后一次触发后等待一定时间才执行回调。适合的场景是输入框搜索联想、窗口resize后重新计算布局。function debounce(fn, delay 200) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); timer null; }, delay); }; }注意fn.apply(this, args)这里的this必须用apply把当前执行上下文传给回调否则在Vue组件中使用时this会丢失。这是很多人容易忽略的坑。节流的思路是无论事件触发多频繁每隔一段时间只执行一次。适合的场景是滚动加载、按钮防重复提交。function throttle(fn, interval 200) { let lastTime 0; return function(...args) { const now Date.now(); if (now - lastTime interval) { lastTime now; fn.apply(this, args); } }; }这里有一个边界问题如果最后一次触发落在间隔窗口之外但间隔又没到这个调用会被直接丢弃。有些场景需要“尾部执行”也就是在连续触发结束时再执行一次这时候要升级为“定时器版本”function throttleWithTimer(fn, interval 200) { let timer null; let lastTime 0; return function(...args) { const now Date.now(); const remain interval - (now - lastTime); if (remain 0) { if (timer) { clearTimeout(timer); timer null; } lastTime now; fn.apply(this, args); } else if (!timer) { timer setTimeout(() { lastTime Date.now(); timer null; fn.apply(this, args); }, remain); } }; }笔试时如果时间不够写出基础版本的防抖和节流就够了但如果你能在注释里说明两个版本各自的适用场景比如“防抖适合多次触发最后一次执行节流适合限制高频触发频率”这道题基本就能拿到满分。4. 浏览器、网络与工程化容易被忽略的“送分题”4.1 HTTP缓存从强缓存到协商缓存的完整链路蘑菇街笔试题里会有一道关于HTTP缓存的题一般是选择题或者简答题。这个考点虽然偏基础但每年都能筛掉一票人。原因在于很多人只知道“强缓存”和“协商缓存”这两个名词但分不清Cache-Control和Expires的关系也不知道ETag和Last-Modified的优先级。先理清整条链路第一次请求资源服务器返回资源内容和响应头其中包含缓存相关的字段第二次请求同一资源浏览器先检查本地缓存是否“新鲜”如果命中了强缓存比如Cache-Control: max-age3600且未过期直接使用本地缓存不发请求如果强缓存过期浏览器携带If-None-Match或If-Modified-Since发起协商缓存请求服务器判断资源没变返回304 Not Modified浏览器继续使用本地缓存如果资源变了返回200和新的资源抓重点Cache-Control是HTTP/1.1里的规范优先级高于HTTP/1.0时代的Expiresmax-age是相对时间浏览器计算缓存剩余寿命Expires是绝对时间依赖客户端时间客户端改了系统时间就会出错ETag优先级高于Last-Modified因为Last-Modified只有秒级精度文件在同一秒内被修改但内容变了Last-Modified无法感知而ETag是基于内容生成的标识能更精确判断资源是否变化笔试答题时如果能再补充一句“Cache-Control: no-store和no-cache的区别”会很加分。no-store是真正的不缓存每次都要从服务器拉取no-cache是缓存但必须每次先向服务器验证是否新鲜不能直接使用缓存。很多非前端岗位的人会混淆这两个你能写清楚这个区别说明对缓存规范是有真实理解的。4.2 数据结构和“数组与指针”基础不牢的隐患热词里反复出现“数组和指针笔试题”虽然这更多是C/C方向的考点但前端笔试题里也会以变体形式出现——比如JavaScript里数组引用传递的考察。来看一道经典题const arr1 [1, 2, 3]; const arr2 arr1; arr2.push(4); console.log(arr1); // 输出什么 const arr3 [1, 2, 3]; const arr4 [...arr3]; arr4.push(4); console.log(arr3); // 输出什么第一问输出[1, 2, 3, 4]因为arr2 arr1只是复制了数组的引用两个变量指向同一个数组对象。第二问输出[1, 2, 3]因为[...arr3]创建了一个全新的数组。这种题对前端来说本质就是在考引用类型和值传递的区别。JavaScript里数组是引用类型变量存的是内存地址赋值时复制的是地址而不是数据。扩展运算符可以浅拷贝数组但如果数组里嵌套了对象或数组浅拷贝只复制了外层引用内层对象依然是共享的const arr5 [{ a: 1 }]; const arr6 [...arr5]; arr6[0].a 100; console.log(arr5[0].a); // 输出100这题很多人答错因为不知道浅拷贝和深拷贝的边界。如果笔试里出现“如何实现深拷贝”光是JSON.parse(JSON.stringify(obj))是不够的要能说出这个方案的局限无法处理undefined、Function、Symbol会丢失循环引用Date会被转为字符串。手写递归深拷贝虽然不是最优解但能体现出你对边界情况的掌控力——要能处理循环引用用WeakMap缓存已经拷贝过的对象。4.3 前端工程化模块化与构建工具的基础认知2019届笔试时webpack 4还在普及阶段所以蘑菇街的题比较“温和”只考察了模块化和构建工具的基本概念没有考webpack配置细节。但这类题的价值在于它反映了一个前端工程师有没有“工程化思维”。常见的简答题是ES Module和CommonJS的区别是什么答题要点很简单但要答得准确import是静态导入require是运行时动态加载ES Module的模块依赖关系可以在编译阶段确定所以可以做tree-shakingCommonJS是运行时确定的无法静态分析ES Module导出的值是变量本身的实时绑定CommonJS导出的是值的拷贝从这些区别可以看出为什么业界都往ES Module方向走——静态分析才能做“死代码消除”。这个概念的考察虽然简单但如果你能把“静态”和“运行时”这两个词说清楚面试官会眼前一亮。另外还有一道我在做题时印象很深的问答题你们项目里用过哪些性能优化手段这类题不是考察唯一答案而是考察你有没有系统性思考。我的答题框架是分层叙述资源层图片懒加载、字体子集化、CSS/JS压缩运行层避免频繁重排重绘使用requestAnimationFrame批量更新、添加key优化Vue列表渲染网络层HTTP缓存、CDN加速、接口合并代码层路由懒加载、按需引入、长列表虚拟滚动这样答题的好处是覆盖了研发链路的不同环节而不是只盯着某一个点。面试官能从你的答案里判断你日常开发中有没有性能优化的全局意识这对于做电商页面的前端岗位来说尤其重要——首屏加载速度直接关系到转化率而蘑菇街这类导购平台对性能的要求是刻在业务基因里的。5. 答题策略与避坑经验从笔试到面试的加分项5.1 笔试中的高频丢分点我在帮人看笔试题时发现丢分往往不是“不会做”而是“会做但没做对”。几个高频丢分点值得单独拿出来说第一手写代码题直接裸写没有注释。笔试的改卷人通常不是机器而是后续的面试官。代码可读性差甚至不如留白不写。我答题的习惯是先写一行注释说明函数作用再写关键步骤的注释最后再补边界情况处理方式。哪怕代码敲得不全注释能把思路表达清楚就能拿到思路分。第二选择题不看“下列说法错误的是”这种反向设问。前端笔试题特别喜欢用反向设问来增加难度一旦看错题目方向整题必错。我见过太多人把“错误的是”看成“正确的是”最后对答案时懊恼不已。第三时间分配失衡。有些人在第一道编程题上死磕两小时第二道简单题反而没时间写。正确做法是先快速扫一遍所有题目把会做的题全部做完再回头攻克难点。笔试题的分数分布不一定按题目顺序递增靠后的简答题可能反而更好拿分。5.2 检查代码时的自测清单编程题写完后如果还有剩余时间不要急着交卷。按这个清单快速自测空数组/空对象是否会导致报错数组长度为1时是否符合预期输入全相同元素时输出是否唯一目标值本身就是最小值/最大值时边界条件是否成立以上面数组去重为例如果只写了[...new Set(arr)]至少要在注释里补充说明Set只能处理基本类型数组对象数组需要用其他方法。这样即使代码没有覆盖到对象场景你的意识已经到位了。另外只要是手写函数一定要测试一下this是否正确绑定。在笔试题里这几乎可以算是“必有坑”。面试官出一道debounce题最后问“如果我在Vue组件的methods里用这个函数内部访问this会怎样”就是在等你主动说出fn.apply(this, args)这一步。5.3 从笔试题延伸出的面试追问准备笔试不是终点它更像面试的索引。蘑菇街这类公司面试官在看完你的笔试答题后可能直接从你的答案中挑一题作为面试开场。所以你在复习笔试题时不要止步于“写出正确答案”而是要多问自己一层“为什么”。比如你写const unique (arr) [...new Set(arr)]面试官可能会问“Set去重的底层是用了什么结构实现的”这看起来像在考ES6其实考的是数据结构的理解——Set内部基于哈希表实现插入和查找的时间复杂度近似O(1)所以去重整段代码的时间复杂度是O(n)。如果你能答到这个层次面试官就能判断你是真的理解了这个API而不是只是刷到了这个答案。再比如手写Promise面试官一定会追问Promise的执行顺序为什么是微任务优先于宏任务这是想确认你是真正理解事件循环还是背会了模板。我在面试中经常用一句话反问自己如果Promise.then里的回调是直接同步执行的话会有什么问题答案是会阻塞当前宏任务导致整个页面的渲染被推后。而异步回调的核心目标就是不让当前任务被阻塞所以把回调放到微任务队列是设计上的必然——既保证了不阻塞又保证回调尽可能快地执行。总的来说复习蘑菇街2019届校招前端笔试题时最有效率的方式不是背题而是用“如果我是出题人我想从这道题看到什么”的角度去复盘每一个考点。我当时做这套题最深的体会是前端面试的底层拼的还是计算机基础无论框架怎么换JavaScript的核心机制始终绕不开那些看起来有点旧的题反而是最能筛选出真东西的环节。另外分享一个小技巧做这类笔试题时把每道题对应的知识点整理成一个“问题-考点-延伸追问”的表格考前过一遍比盲目刷题高效得多。比如“防抖和节流的区别”这道题整理成一行就是“问题防抖节流的区别 / 考点事件高频触发优化 / 延伸追问手写实现、this绑定、实际场景”。这样一趟下来你拿到的不只是正确答案而是一棵完整的知识树。
返回列表