先说明一个背景:我是在2018届秋招时做这套美团前端卷子的,当时做完之后印象很深——倒不是因为它难到让人崩溃,而是它的选题方向非常典型,几乎每一个考点都是后面面试里反复出现的东西。所以如果你现在正在准备前端校招,或者刚入行想检验一下自己的基础,这套真题是很好的自测材料。这篇文章我会把完整的题目拆一遍,重点讲选择题里的易错逻辑和手写代码题的解题思路,顺带说一些我在笔试现场踩过的坑和复盘心得。
1. 这套卷子的整体印象与应对思路
1.1 2017年秋招前端笔试在考什么
2017年前端生态和现在差别很大。Vue 2.x 刚成为主流,React 16 还没完全普及,TypeScript 也只在部分大厂内部使用。美团的这套前端卷子,整体风格偏基础,几乎不考框架,而是考察 JavaScript 语言本身的功底、浏览器工作原理、网络基础,再加上两三道手写代码题。
现在回头看,这套题的出题思路是经过刻意设计的。前端岗位入门门槛低,但基础扎实的人不多,所以通过笔试筛掉“只会用框架拼页面”的简历型选手,是当时大厂通用的策略。如果你把美团这套题的每个考点都搞明白了,再去应对同期百度、阿里、腾讯的前端笔试,会发现重叠率超过一半。
1.2 题型分布与时间分配建议
卷面结构大致分三部分:选择题、简答题、编程题。选择题约20道,覆盖 HTML/CSS/JS/网络;简答题两三道偏概念;编程题一般有两到三道手写。整套题建议在90分钟内完成。
我的建议是:选择题控制在40分钟以内,遇到不会的不要恋战,先凭直觉选一个并做标记,回头再检查。简答题控制在15到20分钟,因为这类题核心是踩分点,写太多反而浪费时间。编程题留足30分钟以上,这是拉开差距的地方。
有个容易被忽略的细节:笔试系统是牛客网的在线编程环境,不能本地调试,且自动补全非常弱。你平时的 IDE 习惯在这时候会变成劣势,所以建议提前一周用裸编辑器(记事本、Vim)练手写代码,特别是数组操作、字符串处理这类逻辑密集的题目。
2. 高频选择题易错点:这些细节决定成败
2.1 一道让很多人翻车的闭包题
选择题里有一道很经典的闭包题目,大致是这样的:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }问输出结果。答案是连续的五个 5,不是 0 1 2 3 4。这个考点在当年属于“必考送分题”,但每年都有一大批人在这里丢分。原因在于很多人只记住了“闭包会保存变量”这个结论,却不清楚闭包保存的是变量的引用而不是变量的值。
这里要拆开讲清楚:var声明的i是函数作用域,所以循环结束后,i只存在一份,值是 5。而setTimeout的回调函数被推入任务队列后,等到主线程执行完毕才执行,此时读取的i已经是循环结束后的最终值 5。要拿到 0 1 2 3 4,当时的常规解法是包一层立即执行函数:
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }这个题的深层价值在于:它检测你能否区分“执行时机”和“变量绑定”。如果你只是背了答案,考场上略作变形就很容易再次翻车。比如把setTimeout换成Promise.resolve().then,或者把var换成let,结果就完全不一样了。所以复习时一定要把“事件循环 + 作用域 + 块级绑定”这三件事放在一起理解。
2.2 CSS 基础题里的“坑王”——外边距塌陷
CSS 部分有一道题问的是:父子元素中,为子元素设置margin-top: 20px会发生什么。很多人误以为子元素会向下移动 20px,但实际结果是父元素整体向下移动了 20px,子元素紧贴在父元素顶部——这就是经典的外边距塌陷。
这个知识点在今天依然是面试高频题。产生条件有三个:父元素没有border-top、没有padding-top、没有创建新的块级格式化上下文(BFC)。解决方式也很多样,常见的有给父元素加overflow: hidden、display: flow-root,或者给父元素加padding-top替代。
我在做这类题时的心得是:不要死记属性,要在脑子里形成盒模型渲染的画面。当你看到margin就会想到它可能折叠,看到float就会想到父元素高度塌陷,看到position: absolute就会想到它的定位上下文。这种直觉来自大量的 debug 经验,而不是刷题。
2.3 网络与安全高频题速查
选择题还爱考网络基础和前端安全,这块的主要考点几乎每套卷子都差不多。以 HTTP 状态码和跨域处理为考查核心的两道题,基本是美团笔试的标配。
| 状态码 | 含义 | 常见坑点 |
|---|---|---|
| 301 | 永久重定向 | 浏览器会缓存,改造时容易踩坑 |
| 302 | 临时重定向 | 与 301 的区别是是否被收藏/缓存 |
| 304 | 协商缓存命中 | 并不表示服务器返回了资源内容 |
| 403 | 禁止访问 | 权限问题,不是路径错误 |
| 404 | 未找到 | 最常见,但有时是接口地址拼接错误 |
| 500 | 服务器内部错误 | 后端问题,但前端要排查请求参数 |
| 503 | 服务不可用 | 一般是过载或维护中 |
跨域单选题经常问“以下哪种方式可以解决跨域”,选项包括 JSONP、CORS、postMessage、document.domain。这里要注意的是,JSONP 只能支持 GET 请求,这是它最明显的限制;而 CORS 需要服务端配合设置响应头,不是纯前端能解决的。postMessage主要用于 iframe 和 Web Worker 场景。如果你对跨域的各个方案边界不清晰,建议直接去 MDN 把文档逐段理解一遍,因为这类题不是考记忆,而是考你对方案适用场景的判断。
2.4 事件循环必考的“异步任务队列”顺序题
事件循环是美团前端笔试绕不开的硬骨头,选择题里经常给出两段代码,问输出的先后顺序。比如:
console.log('a'); setTimeout(() => { console.log('b'); }, 0); Promise.resolve().then(() => { console.log('c'); }); console.log('d');正确输出是a d c b。关于这道题,关键在于理解宏任务(setTimeout)和微任务(Promise.then)的执行顺序:主线程代码(同步任务)执行完毕后,先清空微任务队列,然后再取一个宏任务执行。setTimeout即使延迟为 0,也会在微任务之后执行。
还有一类进阶变形,是 Python、Java 程序员跨界做前端题时最容易错的:把Promise的构造函数和then回调的执行时机混在一起。请记住:new Promise(executor)里的executor是同步执行的,而then里的回调是异步执行的。把这句话刻在脑子里,能避开至少九成的坑。
3. 手写代码题:每一道都能看出你的工程素养
3.1 深拷贝:最简单的题,最重的坑
手写代码题第一道就是深拷贝:
function deepClone(obj) { ... }题目本身描述很简单,但它是一个典型的“易上手、难写对”的题。不同水平的考生写出来的版本截然不同。
第一层答案是 JSON 方案:
function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }这个方案最大的问题是丢失数据类型。undefined、函数、Symbol、RegExp、Date、Map、Set在序列化时都会出问题。笔试现场如果写这个,大概率只能拿一半分。
第二层答案是递归遍历普通对象和数组:
function deepClone(obj) { if (typeof obj !== 'object' || obj === null) { return obj; } let result = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { result[key] = deepClone(obj[key]); } } return result; }第三层才是笔试真正想看的“工程级”实现,要在递归过程中处理循环引用:
function deepClone(obj, map = new WeakMap()) { if (typeof obj !== 'object' || obj === null) { return obj; } if (map.has(obj)) { return map.get(obj); } let result = Array.isArray(obj) ? [] : {}; map.set(obj, result); for (let key in obj) { if (obj.hasOwnProperty(key)) { result[key] = deepClone(obj[key], map); } } return result; }我当年笔试的教训是:先写判断,再写递归,忘记循环引用处理。后来复盘时意识到,这道题其实在考察“工程思维”——你有没有想过极端情况?当对象里有循环引用时,没有WeakMap的版本会无限递归导致栈溢出,在线上环境里,这样的深拷贝会直接导致页面崩溃。所以当你手写这种实用函数时,一定要上升到工程角度去思考边界条件。
3.2 三栏布局:不是比谁方法多,而是比谁说得全
编程题第二道是经典的“左右固定宽度,中间自适应”的三栏布局。当年市面上流传的答案不下五种:浮动、绝对定位、flexbox、table、grid。很多人能写出两种就算不错了,但这道题考的不是你会几种,而是你能不能把每种方案的局限性和适用场景讲清楚。
先说我推荐的标准解:flexbox。
<div class="container"> <div class="left">left</div> <div class="center">center</div> <div class="right">right</div> </div>.container { display: flex; } .left { width: 200px; } .right { width: 200px; } .center { flex: 1; }这段代码非常简单,兼容性也足够日常使用。问题是,很多面试官会追问“flex 布局在移动端的坑有哪些”,所以你要额外了解flex: 1是flex-grow: 1; flex-shrink: 1; flex-basis: 0%的缩写,以及子元素默认min-width: auto可能导致溢出问题。
浮动的解法已经不太推荐,因为要额外处理清除浮动。但作为“历史知识”你要能说上来一个版本,否则问到经典布局方案时会显得知识面窄。这道题我当时写的是“浮动+负 margin”的圣杯布局,后来和同学讨论发现,大家觉得圣杯布局更代表“底层功底”。
笔试现场我建议这样分配精力:先写 flex,再写 grid,然后在注释里简要说明 float/absolute 的局限。这样的答题方式会让阅卷人感觉你不只是会背代码,而是真正理解布局原理。
3.3 大数相加:JavaScript 数字精度的边界在哪里
还有一道高频题是大数相加。题目通常会这样描述:“实现一个函数,输入两个字符串形式的数字,返回它们的和,不能直接转 Number 计算”。
这道题看着简单,其实在考验两件事:字符串按位相加的算法思路,以及对 JavaScript 数字类型Number.MAX_SAFE_INTEGER(9007199254740991)的理解。超过这个值之后,浮点数表示就会丢失精度。
function add(a, b) { let i = a.length - 1; let j = b.length - 1; let carry = 0; let result = ''; while (i >= 0 || j >= 0 || carry > 0) { const numA = i >= 0 ? Number(a[i]) : 0; const numB = j >= 0 ? Number(b[j]) : 0; const sum = numA + numB + carry; carry = Math.floor(sum / 10); result = (sum % 10) + result; i--; j--; } return result; }这道题的三个核心注意点:
- 从个位开始计算(字符串最后一个字符对应数字的最低位),与手写竖式顺序一致
- 进位
carry必须在循环结束后检查是否还有剩余,否则会漏掉最高位 - 结果拼接时不能使用
result += (sum % 10),否则得到的顺序是反的,需要反转字符串或采用前插法
我在牛客网笔试时犯过一个低级错误:直接把两个字符串转 Number 相加,忽略了输入可能超出安全整数范围。这种错在真实工作中可能不会频繁复现,因为很少有人会手动处理超过 16 位数字的求和,但它暴露了对 JavaScript 数值机制的盲区。如果你在复习这道题,建议把Number.MAX_SAFE_INTEGER、BigInt、浮点数精度这几个知识点一起补上。
3.4 bind 手写:从这道题看清 this 的底层逻辑
手写题的最后一道通常是Function.prototype.bind的实现。这道题不单纯是考 bind,它顺带把this、原型链、new优先级全部串起来了。标准实现:
Function.prototype.myBind = function(context, ...args) { const fn = this; return function(...newArgs) { return fn.apply(this instanceof fn ? this : context, [...args, ...newArgs]); }; };这段代码里最难理解的是this instanceof fn这个判断。它解决的是什么问题?当使用new调用 bind 返回的函数时,this应该是新建的实例,而不是传入的context。如果不加这个判断,new出来的对象就会错误地绑定到原来的 context 上。
我当时第一次看到这个写法时,思路卡了很久。后来想明白了一个类比:bind 就像是给函数配了一位“有默认语气的发言人”,平时发言用的是预设的角色。但当别人用new来调用返回函数时,这位发言人就把话筒交给了新建的实例,保留构造函数原本的行为。这个细节不掌握,面试官一问“bind 返回的函数能不能被 new”,你就露馅了。
3.5 数组去重和扁平化:最容易被忽视的基础题
除了上面几道编程题,选择题和简答题里通常还会夹带一两道“低成本热身题”,数组去重就是其中之一。常见的去重方式包括Set、filter + indexOf、对象键值对、双循环。笔试现场最稳妥的是:
const unique = arr => [...new Set(arr)];这道题需要注意的不是能不能去重,而是能不能答出不同方法对不同数据类型的处理差异。Set的去重基于 SameValueZero 比较,NaN和NaN会被视为相等,而{}和{}不会被去重。如果用对象键值对去重,还要注意'1'和1会被隐式转换成相同字符串,导致错误去重。数组扁平化同理,常见的解法有递归、reduce + concat、arr.flat(Infinity)。笔试现场建议写递归法,因为更通用且不依赖 ES6 以上特性。
这两道题往往是整套卷子的“压分石”:会做的人觉得没什么,不会做的人觉得要背的东西好多。但换个角度看,这也提醒我们:面试过程中,基础不一定是你最出彩的地方,但基础差一定会成为你的致命伤。
4. 这些“隐藏考点”更值得关注
4.1 HTTP 缓存的优先级判断
美团卷子里有一道和网络缓存相关的题,我印象很深。题目给了Cache-Control、Expires、ETag、Last-Modified四个请求头/响应头,问它们之间的优先级。正确答案是:Cache-Control的优先级高于Expires,ETag的优先级高于Last-Modified。也就是说,强制缓存优先看Cache-Control,协商缓存优先看ETag。
这个知识点在后来的面试中被问过很多次。我当时其实没完全搞懂,只记住了“强缓存优先于协商缓存”的大致概念。真正让我醍醐灌顶的是后来在项目中遇到一个 bug:后端修改了Last-Modified时间,但前端资源始终没有更新。排查后发现是Cache-Control: max-age=86400把资源在本地缓存了一天。那次之后我才彻底理解,一道题背后关联的是一个庞大的工程体系。
4.2 跨域携带 Cookie 的条件
除了缓存,跨域相关的简答题也很常见。经典问题是“JSONP 为什么不能携带 Cookie?”以及“CORS 如何携带 Cookie”。
这两个问题的核心是:浏览器的同源策略和 Cookie 的SameSite属性。使用 CORS 时,如果要携带 Cookie,前端必须设置withCredentials: true,同时后端必须返回Access-Control-Allow-Credentials: true,而且Access-Control-Allow-Origin不能是*,必须指定具体域名。很多人记住了withCredentials,却忽略了后端的同意响应头,导致前端代码写对了但请求照样失败。
JSONP 是通过动态创建<script>标签实现的,它的请求是浏览器发起的“资源请求”,和XMLHttpRequest的跨域策略不同。是否携带 Cookie 取决于目标域与当前页面的 Cookie 策略,但 JSONP 本身的机制并不能像 XHR 那样显式控制凭据。这道题在笔试中更多是考你对“同源策略边界”的理解,而不是让你写一长串代码。
4.3 从题目看出前端工程化趋势
2017 年的前端笔试还有一个特点:很多公司开始把代码规范和前端工程化纳入考察范围。美团卷子里有一道简答题,问“Gulp、Grunt、Webpack 之间有什么区别”。放在当时,Webpack 已经逐步取代 Gulp 和 Grunt,所以最佳回答不是背诵三者定义,而是说出“Gulp/Grunt 是任务执行器(task runner),Webpack 是模块打包器(module bundler)”,以及“现代前端更倾向用 Webpack 做资源管理和构建优化”。
如果你现在做这套题,可能会觉得这个考点有点过时。但背后的思考方式依然适用:前端工程史是一脉相承的,今天你用的 Vite 融合了 esbuild 和 Rollup 的能力,很多设计决策都是基于前代工具暴露出来的问题。面试官问工具题的目的不一定是考你会不会用,而是看你能不能理解“为什么需要这个工具、它解决什么问题、代价是什么”。这种思维方式,比背几个 npm 包名值钱得多。
5. 考后复盘:我的丢分点和总结出的经验
5.1 选择题为什么会错
我做这套选择题时,错得比较集中的是两类:一类是 CSS 相关(比如块级格式化上下文、BFC 相关),另一类是涉及“语义化”的题。HTML 语义化的选择题经常给出一段 HTML 结构,问哪个标签用得不恰当。例如用<div>做按钮、用<span>做标题、用<p>放列表等。这种题的正确答案依赖你对 HTML5 标准的理解,而不仅仅是“能用 CSS 改成一样的效果就行”。
我的丢分教训是:复习时只重视 JS,忽略了 HTML/CSS 的细节。后来意识到,前端笔试的根本目标不是筛选“JS 高手”,而是筛选“基本功全面的人”。所以如果你正在准备秋招,建议分配时间去系统过一遍 HTML 标签语义、CSS 选择器优先级、flex/grid 布局、移动端视口适配,这些内容占的分数比你想象中多。
5.2 编程题容易“差最后一公里”
编码题的评判标准往往是“部分用例通过”。有时候你写出了思路正确的代码,但因为某个边界条件没处理好,就只能通过 50% 的测试用例。我在做“数组扁平化”这道题时,给出的实现没有处理空数组的情况,导致[].flat()的测试用例直接报错。放在 IDE 里这不算大事,但在笔试系统里这就意味着至少 20% 的分数没了。
从那时起我养成一个习惯:手写任何函数前,先在脑子里列三个简单测试用例——正常输入、空输入、单一元素。把这三个用例在草稿纸上跑一遍再提交,能显著降低低级错误率。这个方法我后来一直用到现在,面试手写代码时也很管用。
5.3 考前一周的复习重点清单
综合我后来两三年的面试辅导经验,可以把美团的这套卷子提炼成一份“前端校招笔试必备清单”:
- JavaScript 核心:作用域、闭包、原型链、
this、事件循环、类型转换 - 异步编程:Promise 手动实现、async/await 执行顺序、微任务与宏任务
- DOM 与事件:事件冒泡捕获、事件委托、常用 DOM 操作性能
- CSS 进阶:盒模型、flex、grid、BFC、层叠上下文、两栏/三栏布局
- 网络基础:HTTP 状态码、缓存策略、跨域方案、Cookie/Session
- 手写代码:深拷贝、防抖节流、bind/call/apply、Promise.all、数组 APIs 实现
这个清单看起来条目多,但每一条基本都能在一个小时内过一遍。关键是不要用看的,要动手写。哪怕只是把Promise.all的源码默写一遍,效果都比翻十篇博客好。
5.4 笔试之后的“隐藏关卡”
笔试只是校招的一个起点,很多同学在这个阶段过度纠结“能不能过”,其实更值得关注的是:笔试题目暴露了你哪些知识的薄弱点。我后来面试别人时,经常发现一些笔试成绩不错的人,在面试中暴露出短板——原因很简单,笔试可以背答案,但面试可以追问。
所以我的建议是:做完整套真题后,别急着对完答案就扔到一边。把每一道错题对应的知识点写成一张表格,标注“当时为什么错”“正确的思考路径是什么”“如果面试官追问,我还能补充什么”。这个过程比刷十套新题都有效。美团这套卷子的题目如今已经被收录在很多题库网站里,但能找到原题的人很多,真正把每道题背后的知识体系消化掉的人很少。希望这篇拆解能帮你少走一些弯路。