ARTICLE DETAIL

资讯详情

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

蘑菇街前端实习笔试题拆解:JS基础与Vue原理考点全解析

蘑菇街前端实习笔试题拆解:JS基础与Vue原理考点全解析 2019年春天那阵蘑菇街的实习招聘一出不少大三的同学就奔着“电商大厂”这个标签去了。但拿到前端开发工程师的笔试卷后很多人第一反应是“诶怎么跟我在刷题网站上练的不太一样”。这份卷子整体不偏不怪核心就是考察基础中的基础——JS 语法细节、异步机制、手写代码能力、Vue 原理理解以及一点浏览器和网络常识。它不考框架API背诵也不考算法难题倒是更像一场“你平时到底有没有真的写代码”的摸底。这篇文章我按当年题型风格整理了一份还原度较高的试题结构并把每道题背后的出题意图、答题思路、参考实现和易踩的坑都拆开讲。不管你是准备大厂实习、春招秋招还是单纯想检验自己的前端基本功这份拆解都能当一面镜子用。1. 从一份实习笔试卷说起整体风格与考察逻辑1.1 试卷结构还原那年的笔试题型大致分成四块时间给了 90 分钟题量不算大但覆盖范围很典型题型数量考察方向选择题10 道JS 语法细节、类型转换、作用域、this 指向、事件循环填空题5 道代码输出结果、CSS 布局结果、HTTP 状态码语义简答题3 道浏览器输入 URL 到页面渲染过程、Vue 响应式原理、防抖节流区别手写题3 道数组扁平化、防抖函数、事件总线EventEmitter这个结构其实很有代表性选择题考“知不知道”填空题考“会不会算”简答题考“讲不讲得清”手写题考“写不写得出来”。四个层次正好把一个人的前端水平从“看过”到“真会”逐层筛选开。1.2 大厂实习笔试的底层逻辑很多同学喜欢刷各种偏题怪题但蘑菇街这份试卷的风格说明了一件事实习阶段面试官不指望你有丰富项目经验而是想确认你有没有吃透前端这摊子事里的地基。地基稳不稳不需要多华丽的题目来测反而越是基础的东西越能看出差别。举个例子选择题里考了1 1和1 1的输出结果看起来像初中题但它在真实业务里引发的 bug 一点都不初中。表单里拿到的值全是字符串直接做加法变成字符串拼接接口传参就出错。这种题的实际意义是把“背过答案”和“真的踩过坑”区分开。还有一个隐藏考察点做题习惯。手写题部分留白区域很大就是看你会不会写注释、变量命名是否清晰、代码结构是否工整。很多同学代码功能写对了但命名全是a、b、tmp这在阅卷人眼里是要扣印象分的。2. 基础题拆解JS 功底是最硬的筛子2.1 类型转换与怪异行为选择题里关于类型转换的题目几乎每年必出。典型考题类似console.log([] false); // ?? console.log({} false); // ?? console.log(null undefined);// ??要答对这种题只能靠真正理解 JavaScript 的“抽象相等比较算法”Abstract Equality Comparison也就是到底做了什么。简单记几条铁律null和undefined互相相等但不与其他任何值相等对象与原始值比较时对象会先尝试ToPrimitive转换成原始值[]转原始值是空字符串所以[] false等价于 false再换算成0 0结果是true{}转原始值是[object Object]和false比较时变成NaN所以结果为false你可能会觉得“这有什么实际用”还真有。判断接口返回数据是否为空时有人图省事直接if (res.data false)结果空数组和空对象的表现完全不一致排查半天发现是等号比较的坑。这种题的价值就在这儿——它逼你想清楚 JS 在底层是怎么对待不同类型的。2.2 闭包、作用域与 this 指向闭包几乎是必考出题角度通常有两个一个问你闭包输出什么另一个让手写一个闭包应用场景。for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 1000); }这段代码输出什么答案是五个5。原因在于var声明的i是函数级作用域循环结束之后i已经是 5而setTimeout里的回调函数在 1 秒后才执行拿到的自然是同一个i。改成输出 0 到 4 的方式有三种把var换成let、用立即执行函数包一层、或者用bind传参。这里必须理解核心逻辑让每次循环的回调函数捕获一个独立的变量副本。this指向也是重灾区。常见考题var name window; var obj { name: obj, getName: function() { return function() { return this.name; }; } }; console.log(obj.getName()());答案是window因为obj.getName()返回的是一个普通函数执行时它的调用者是全局对象。这种题的套路就是“函数被谁调用this 就指向谁”跟函数在哪里定义没有关系。箭头函数则反过来this 取决于定义位置的外层作用域这两者必须分清楚。2.3 原型链与继承原型链题目在试卷里属于“看起来很基础但一写就错”的类型。经典考法是让你画出new一个对象后的原型关系图或者写出instanceof的判断结果。function Parent() {} function Child() {} Child.prototype new Parent(); var child new Child(); console.log(child instanceof Child); // true console.log(child instanceof Parent); // true这里要注意的是Child.prototype被整体替换成了Parent的实例所以child的原型链上既能找到Child.prototype也能找到Parent.prototype。继承的实现方式里“原型链继承”“组合继承”“寄生组合继承”的区别要能讲清楚特别是为什么要用Object.create(Parent.prototype)而不是直接new Parent()——后者会多执行一次父构造函数可能产生副作用。有个我印象很深的作答技巧遇到原型链题目先把__proto__和prototype的关系在心里默念一遍——prototype是函数身上的属性__proto__是实例身上的属性实例通过__proto__指向构造函数的prototype。理清这一条链绝大部分题目都能顺下来。2.4 事件循环与异步填空题里出现频率最高的就是事件循环输出顺序题比如console.log(start); setTimeout(() { console.log(timeout); }, 0); Promise.resolve().then(() { console.log(promise); }); console.log(end);正确输出是start - end - promise - timeout。核心逻辑是同步代码先执行完微任务队列Promise.then / MutationObserver先于宏任务队列setTimeout / setInterval / I/O清空。这类题的难点在于把 Node.js 环境和浏览器环境的差异也搞清楚。Node 里宏任务还分timers、poll、check等多个阶段setImmediate和setTimeout的执行顺序在不同版本的 Node 里表现都不一样。实习笔试一般不深挖到这一层但答出来绝对是加分项。另外async/await也经常混在里面。await后面代码的执行时机可以理解为“注册一个微任务之后的代码继续往下走”所以async function里await之前的部分是同步的之后的部分是微任务。这个理解能帮你应对 90% 的 async 输出顺序题。3. 手写题实战不只是“背代码”3.1 数组扁平化手写题第一题大概率是数组扁平化。题目描述一般是写一个函数将嵌套数组拍平成一维数组要求不能用Array.prototype.flat。我来给一个完整且可以解释清楚的实现function flatten(arr) { const result []; for (let i 0; i arr.length; i) { if (Array.isArray(arr[i])) { // 递归处理嵌套数组并把返回值展开后推入结果 result.push(...flatten(arr[i])); } else { result.push(arr[i]); } } return result; } // 用法示例 console.log(flatten([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5]这道题几个关键点第一必须用Array.isArray判断数组类型而不是typeof因为typeof []返回的是object。第二递归时要处理返回值不是简单地result.push(flatten(arr[i]))否则会把数组整个塞进去。第三用扩展运算符...展开递归返回的数组这是 ES6 时代比较优雅的写法。如果题目升级为“控制拍平层数”写法要加一个深度参数function flattenByDepth(arr, depth 1) { return arr.reduce((acc, val) { if (Array.isArray(val) depth 0) { acc.push(...flattenByDepth(val, depth - 1)); } else { acc.push(val); } return acc; }, []); }这里用到了reduce也是高频考点。阅卷时我看到不少同学能用递归写出全量拍平但加层数限制就懵了说明对递归的控制条件还不够熟练。平时练习时建议把“拍平 1 层”“拍平 N 层”“全量拍平”三种变体都写一遍面试时遇到任何变体都不慌。3.2 防抖与节流防抖节流是前端面试题的“常青树”蘑菇街这份卷子把两个都考了还让写核心区别。先给一个完整可用的防抖实现function debounce(fn, delay 300) { let timer null; return function(...args) { if (timer) { clearTimeout(timer); } timer setTimeout(() { fn.apply(this, args); timer null; }, delay); }; }注意两个细节一是用...args把事件对象传进去不然fn里面拿不到event二是用fn.apply(this, args)把外层this保留下来否则this会丢失。节流的常见实现是时间戳版和定时器版各有利弊// 时间戳版立即执行停止触发后不会补执行 function throttleTimestamp(fn, interval 300) { let lastTime 0; return function(...args) { const now Date.now(); if (now - lastTime interval) { fn.apply(this, args); lastTime now; } }; }在实际项目中防抖适合搜索框输入、窗口 resize 等场景节流适合滚动监听、按钮点击防止重复提交等场景。你还需要理解一个细节防抖是“最后一次触发后等待 N 毫秒执行”节流是“N 毫秒内最多执行一次”。这个区别在面试里属于必答点用大白话说就是“防抖是只认最后一次节流是雨露均沾”。3.3 Promise 核心机制手写题里若出现 Promise 相关通常不是让你完整实现一个 Promise而是写Promise.all、Promise.race或者实现一个简单的EventEmitter。但理解 Promise 的核心机制对打底很重要。先看一个Promise.all的实现function promiseAll(promises) { return new Promise((resolve, reject) { const results []; let count 0; if (promises.length 0) { resolve([]); return; } promises.forEach((promise, index) { Promise.resolve(promise) .then((value) { results[index] value; count; if (count promises.length) { resolve(results); } }) .catch(reject); }); }); }关键点在于结果数组results要用index直接赋值不能用push否则返回值顺序会乱。count用来记录已完成的数量全部完成才resolve。另外要注意Promise.resolve(promise)包裹兼容传入非 Promise 值的情况。如果题目改成“实现一个简易 EventEmitter”核心思路是维护一个事件名到回调函数数组的映射class EventEmitter { constructor() { this.events {}; } on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] []; } this.events[eventName].push(callback); } emit(eventName, ...args) { const callbacks this.events[eventName] || []; callbacks.forEach((callback) { callback(...args); }); } off(eventName, callback) { const callbacks this.events[eventName] || []; this.events[eventName] callbacks.filter((item) item ! callback); } }这里有个实用细节off的时候要用 filter 重新赋值不能直接循环 splice否则在回调里同时删除和遍历会有问题。这种设计在 Vue 的事件总线、第三方库的发布订阅模式里都能见到。4. CSS 与浏览器网络容易被忽视的送分项4.1 浏览器渲染过程简答题几乎必有一道“在浏览器地址栏输入 URL 到页面显示发生了什么”。这道题看着简单但真正能拿高分的人不多因为它考察的是知识面的完整度。一个理想的回答链路应该是DNS 解析把域名解析成 IP优先查浏览器缓存、系统 hosts 文件、本地 DNS 缓存最后才走 DNS 服务器建立 TCP 连接三次握手确认双方收发能力正常发送 HTTP 请求请求行、请求头、请求体服务端处理并返回响应浏览器解析 HTML构建 DOM 树解析 CSS构建 CSSOM 树合并生成渲染树DOM 树和 CSSOM 树结合剔除不可见元素布局与绘制计算每个节点的几何位置然后栅格化、合成、显示面试官通常会在第 4 步到第 6 步之间追问细节比如“JS 放在哪里会影响渲染”“什么是重排和重绘”。这时候你要能说出来script标签会阻塞解析所以一般放 body 底部或加defer/async属性修改元素几何属性会触发重排修改颜色等样式只触发重绘重排的开销远大于重绘。4.2 居中与布局CSS 题目虽然占比不大但“水平垂直居中”是个高频点尤其是手写代码每种方法背后的适用场景要讲清。/* 方法一flex 布局最推荐 */ .parent { display: flex; align-items: center; justify-content: center; } /* 方法二绝对定位 transform不需要知道宽高 */ .parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 方法三grid 布局写法最简洁 */ .parent { display: grid; place-items: center; }这里要特别注意flex 居中时父容器必须要有确定的高度否则垂直居中不生效。绝对定位加 transform 的方法在父容器高度不确定时也能用但会有像素模糊的问题transform 引起的可以用top: calc(50% - 高度的一半)来规避。另外关于 Flex 布局那道题我建议你把“主轴”和“交叉轴”的默认方向记牢flex-direction: row时主轴是水平方向justify-content管水平align-items管垂直改成column后两者对调。这个基础点一错整个布局就乱套。4.3 HTTP 缓存HTTP 相关内容在前端面试里属于“背熟就有分”的模块。常见考法是问强缓存和协商缓存的区别并举例说明。强缓存相关响应头主要有Cache-Control和Expires。Cache-Control的常用值有max-age3600资源在 3600 秒内直接使用缓存no-cache需要询问服务器资源是否过期不能直接使用本地缓存no-store完全不缓存public/private允许/禁止中间代理缓存协商缓存用的是Last-Modified/If-Modified-Since和ETag/If-None-Match。前者精确到秒如果在同一秒内修改了文件可能判断不出变化后者是文件内容的哈希值更精准。所以一般建议以ETag为主Last-Modified作为兜底。这个知识点虽然不复杂但在笔试里出题率极高而且很容易跟“浏览器刷新时的缓存策略”结合考。按 F5 刷新时即使命中了强缓存浏览器也会带上If-Modified-Since等字段去问服务器而地址栏直接回车导航时则可能直接走强缓存。这个细节我当年答错了后来工作里排查静态资源更新问题时才彻底搞清楚。5. Vue 考察点从“会用”到“懂原理”5.1 响应式原理Vue 题目在蘑菇街的试卷里占比不低毕竟它们当时主要技术栈就是 Vue。最核心的一道题请简述 Vue 2.x 响应式原理。一个比较完整的回答是这样的Vue 2.x 使用Object.defineProperty对 data 中的每个属性进行劫持在 getter 里收集依赖即当前渲染用到了这个属性的 Watcher在 setter 里触发依赖更新从而通知视图重新渲染。整体流程涉及Observe递归遍历 data 对象给每个属性添加 getter 和 setterDep依赖收集器每个属性对应一个 Dep 实例内部维护一个 Watcher 数组Watcher组件渲染 Watcher、计算属性 Watcher、用户自定义 Watcher 三种依赖收集渲染时读取属性触发 getter把当前 Watcher 收集进 Dep派发更新属性变化时触发 setterDep 通知所有 Watcher 执行更新面试常追问的问题是“有哪些属性无法被劫持”。答案是新增属性、删除属性、直接通过下标修改数组元素Vue 2 里的arr[0] xxx不响应。所以要使用this.$set来添加响应式属性用splice方法修改数组。如果能再补充一句“Vue 3 用 Proxy 解决了这些局限”档次立马不一样说明你有技术视野。5.2 组件通信组件通信是 Vue 题目里的简答题常客考察你能否讲清楚各场景下的选择通信场景推荐方案父组件 → 子组件props子组件 → 父组件$emit自定义事件兄弟组件状态提升到共同的父组件或使用 Event Bus深度嵌套组件provide / inject任意组件Vuex / Pinia这里要特别注意provide / inject不是响应式的如果父级修改了提供的值子组件不会自动更新。要让它响应式需要在提供时传一个响应式对象。还有 Event Bus 在大型项目里容易造成事件管理混乱出了问题特别难排查官方现在也不推荐这一点如果能在面试中主动提到会加分。5.3 nextTick 与生命周期Vue 还有一个高频考点this.$nextTick是干什么用的为什么数据变了之后 DOM 不会立刻更新关键在于 Vue 的异步更新队列机制。当你修改数据后Vue 不会立即操作 DOM而是开启一个队列把所有数据更新都缓存起来等同一事件循环里所有数据变更完成后再统一进行 DOM 更新。这样做是为了避免频繁的 DOM 操作造成性能浪费。所以如果你在修改数据后立刻访问 DOM 获取最新值拿到的一定是旧值。$nextTick把回调放进回调队列在下次 DOM 更新循环结束后执行这时候就能拿到最新 DOM。生命周期也需要完整掌握beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。特别要注意的是在created里可以访问 data、调用方法但拿不到真实 DOM在mounted里才能操作 DOM。beforeDestroy里要清理定时器、取消事件监听、解绑自定义事件防止内存泄漏。这个问题我在真实项目中踩过坑——某个页面跳转后定时器还在跑导致不必要的接口请求最后就是在beforeDestroy里补了clearInterval。6. 常见失分点与答题经验总结6.1 书写规范带来的印象分阅卷的时候代码书写的规范性直接影响主观印象分。我见过很多功能完全正确的答案但通篇没有注释、变量名全是a/b/c、缩进混乱这类答案在拿满分上会吃亏。手写代码时建议这样做关键逻辑加一行注释说明思路变量命名使用有意义的英文单词每个函数用空行分隔保持结构清晰写完后在代码下方用一两句话说明复杂度和适用场景比如防抖函数写完后补一句“时间复杂度 O(1)适合搜索输入场景”这会让阅卷人觉得你不仅会写还真的懂怎么用。6.2 知识盲区排查清单结合那份试卷和我后来做面试官的经验整理一份自测清单检查自己有没有以下盲区和的完整转换规则尤其是对象转原始值的步骤closure和this同时出现时的执行结果判断Promise链式调用中.then返回值和后续回调的传参关系防抖与节流的选择依据不是所有场景都适合用同一个Object.defineProperty和Proxy的优缺点对比浏览器强缓存和协商缓存的完整报文头字段Vue 父子组件挂载顺序父created→ 子created→ 子mounted→ 父mounted如果你能不看资料把上面每个点都讲清楚那这份笔试稳过的概率极高。6.3 时间分配与答题节奏90 分钟看起来充裕但如果每题都纠结最后手写题肯定会仓促。我建议这样分配选择题和填空题控制在 25 分钟以内简答题每道 10 分钟剩下的 35 分钟全部留给手写题。手写题不要一上来就写代码先在草稿纸上把思路捋一遍明确边界条件再落笔。比如数组扁平化先想清楚空数组怎么办、深层嵌套怎么办、非数组元素怎么办再动手写。这样写出来的代码结构完整基本不需要涂改。另外如果某道题真的不会千万不要留白。写一个思路、写几个关键 API都能拿步骤分。阅卷时最怕看到空白有过程就有分这个技巧在编程题里尤其重要。说到底这份笔试卷考的内容并不刁钻但它在用一种很巧妙的方式筛选你有没有真正思考过日常写的每一行代码背后的机制。我自己当年考完之后最大的感受是基础这东西平时不补笔试时真的会“现原形”。后来我再看实习生简历时也会刻意在笔试里保留这种“看着简单、实际分层”的题目。建议准备面试的同学别只刷题要把每个知识点从“哦我记得”变成“我能讲明白”。这套卷子之外的收获才是真正值钱的东西。
返回列表