1. 这套题目到底在考什么:出题思路还原
如果你经历过2017年前后的校招季,应该对“欢聚时代”这个名字不陌生。这家公司当时最出名的产品是YY语音和虎牙直播,业务线里大量用到实时交互、弹幕渲染、礼物动效这类高复杂度前端场景,所以它家在web前端方向的校招笔试,向来不是那种“背背书就能过”的卷子。
这套A卷我记得整体给人最直观的感受是:不偏不怪,但覆盖面非常扎实。它没有像某些大厂那样上来就甩一堆工程化冷门名词,也没有用“手写一个Vue响应式原理”这种后来才流行的硬核题。整套题的核心逻辑,可以用一句话概括:它默认你已经熟练掌握了原生JavaScript和浏览器运行机制,在这个基础上再考察你能不能解决实际场景里的问题。
从题目结构上大致可以还原出这样几个板块:
| 考察板块 | 占比估算 | 核心方向 |
|---|---|---|
| 基础知识(HTML/CSS/JS) | 40% | 选择、填空为主,涉及盒子模型、原型链、作用域 |
| 网络与浏览器 | 20% | HTTP状态码、缓存机制、跨域方案 |
| 算法与逻辑 | 15% | 数组去重、排序、字符串处理为主 |
| 框架与工程化 | 10% | jQuery/原生DOM操作、模块化基础 |
| 简答与设计题 | 15% | 场景题、代码输出题、性能优化思路 |
这个比例放在今天看也不算失衡。尤其是它把“浏览器工作原理”和“原生JS”放在了一个很高的优先级上,这在当时很多学校还在教jQuery的时代,其实是比较超前的信号。它明确告诉候选人:我招的是web前端开发,不是页面切图工,你必须知道你写的每一行代码在浏览器里发生了什么。
这里也顺便说一下这套题适合谁看。如果你现在正在准备校招,或者工作了两三年想回头补一补基础,那么把这套老题过一遍的价值不在“押题”,而在于它是一份很好的基本功自测清单。它能帮你快速定位:哪些东西你只是“听说过”,哪些东西你其实“没有真正搞懂”。
1.1 基础三件套是绝对主战场
回看这套题,最重头的还是HTML、CSS、JavaScript这三个老伙计。我记得当时有不少人看到题目第一反应是“怎么这么基础”,结果一做发现处处是坑。这其实是出题人有意为之——基础题是区分“背过”和“理解”的最好筛子。
比如CSS部分,考察的绝对不是“flex是哪几个属性”这种背诵题,而是类似“两个div上下排列,上面固定100px,下面自适应撑满剩余高度,有几种实现方案”这种需要你真正理解定位、浮动、弹性布局各自适用场景的题目。再比如盒模型的题目,通常会给一个元素设置宽度100px、padding 20px、border 5px、box-sizing为border-box,问你实际占用的宽度是多少。这种题看着简单,但每年都有相当多的人栽在没搞清楚box-sizing的兼容性上。
JavaScript部分就更明显了,大量选择填空题围绕这几个主题转:原型与原型链、this指向、变量提升、闭包、事件循环。这些知识点用背的也能应付一部分,但一旦出题人把几个知识点叠加在一起考,比如“在setTimeout里调用对象方法,this指向哪里,为什么”,基础不扎实的人立刻现原形。
1.2 题目难度梯度设计有讲究
这套题让我印象最深的一点,是它非常“诚实地”设计了难度梯度。开头的题目基本是送分题,让大部分人能稳住心态;中间的题目开始有区分度,比如“写出下面代码的输出顺序”这类事件循环题;最后则是一两道压轴题,通常是综合场景设计。
这种设计其实和一个web前端开发日常工作的思维模式很像:先处理简单直接的任务,再逐步深入复杂逻辑。出题人有意识地在模拟一个开发者的思考路径。所以如果你在考场上做这套题,最忌讳的就是在中间某一道上死磕太久,因为后面的压轴题往往是按步骤给分的,如果因为时间不够而空着,非常可惜。
另外一个细节是,这套题里几乎没有出现“偏题怪题”。像“CSS hack怎么写”、“IE6的兼容处理”这类当年很多笔试喜欢放的题,它基本没碰。这也从侧面反映了欢聚时代当时对web前端岗位的定义:要的是能解决真实业务问题的人,而不是会背兼容性教条的人。
2. 高频考点逐个拆解:从原生JS到算法
既然这套题的核心是考察基本功,那我们就具体聊聊那些年出现频率最高的几个考点。这里我不会去复述每一道原题,而是把这些考点背后的原理和踩坑点掰开揉碎讲一遍。你能把这些点吃透,不管笔试题目怎么换都没问题。
2.1 作用域、闭包与this指向:最热的送分/送命题
这三兄弟在当年的笔试题里几乎可以说是必考。考察方式通常是两种:一种是给一段代码让你写出输出结果,另一种是让你手写一个防抖或者节流函数然后解释为什么这么做。
先说说作用域和变量提升。新手最容易错的地方在于,以为var声明的变量会“跟着代码顺序走”。实际上JavaScript的变量提升机制是:var声明的变量会被提升到当前作用域顶部,但赋值不会提升;函数的提升优先级高于变量提升。这个知识点结合let/const来对比记忆会更清晰。
console.log(a); // 输出什么? var a = 10; console.log(b); // 会报错吗? let b = 20;答案是第一个输出undefined,因为var a被提升了但赋值没执行;第二个直接报错,因为let存在暂时性死区。这种题几乎年年出,属于送分题,但依然每年都有人丢分。
再看this指向。我记得当时有一个高频题目是:给定一个普通函数、一个对象方法、一个箭头函数,分别在不同调用方式下输出this的值。核心规律其实只有一条,你先记住它,绝大多数题目都能推导出来:
谁调用了函数,this就指向谁;箭头函数没有自己的this,它继承外层作用域的this。
但笔试题目往往不会问得这么直接,而是会结合setTimeout来考。比如这样一个经典场景:
var name = 'global'; var obj = { name: 'obj', getName: function() { return function() { return this.name; }; } }; console.log(obj.getName()()); // 输出什么?这道题的陷阱在于,obj.getName()返回了一个普通函数,然后被直接调用,这时调用者是全局环境,所以this.name取到的是全局的global。很多人以为方法定义在obj里,内部的this就一直指向obj,这就错了。解决方案很简单,要么在外层把this存成self或that,要么用箭头函数改写。
闭包的考察方向就比较统一了。围绕“防抖”、“节流”、“柯里化”、“私有变量”这些实际场景出题。这里特别提醒一下,手写防抖的时候,很多人会忘记处理this指向和事件参数,这在笔试评分里是很严重的扣分点,因为这不是“实现不了”而是“实现得不够完整”,反而更可惜。
2.2 事件循环与异步机制:笔试里的分水岭
我记得这套A卷有一道题,给出一串代码,里面有console.log、setTimeout、Promise.resolve().then(),让你写出打印顺序。这类题现在看起来已经很常规,但在当年,确实能筛掉一大批“只会写回调,不懂异步原理”的候选人。
要理清这类题,你脑子里必须有一张清晰的事件循环流程图。简化版是这样的:
- 执行同步代码,遇到
setTimeout把回调放入宏任务队列,遇到Promise.then把回调放入微任务队列。 - 同步代码执行完毕后,先清空所有微任务。
- 微任务清空后,从宏任务队列取出一个任务执行。
- 每执行完一个宏任务,再检查有没有新的微任务,有就清空。
- 重复以上过程。
用代码来验证一下:
console.log('start'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); console.log('end');输出顺序是start、end、promise、timeout。有同学可能会疑惑,setTimeout明明延迟是0,为什么还排在promise后面?因为即使延迟为0,它也是一个宏任务,必须等当前脚本的所有微任务清空之后才会执行。
掌握了这个规律,你再去看更复杂的综合题,比如多个Promise嵌套、async/await混用,就会有一个清晰的推导路径。当时这道题的正确率据说并不高,但它恰恰是web前端开发日常里最能直接转化为调试能力的知识点。你页面里的接口请求顺序、动画卡顿、渲染时机,最后都能追溯到事件循环机制上。
2.3 数组字符串API与手写实现
这一块在笔试里属于“性价比之王”。考察范围非常广泛,从简单的数组去重、扁平化,到手写reduce、new、call/apply/bind,再到字符串反转、去空格、模板字符串替换,基本是把ES5+ES6的常用API地毯式筛了一遍。
先说一个我当时觉得很重要的小规律:去重类的题目,至少有四种解法,你最好都能写出来,因为不同解法反映了你掌握的API深度不同。最简单的两重循环,中等一点的用indexOf或includes,进阶一点用Set,再进阶一点可以手写一个利用对象键名去重的方法。笔试时建议先写最熟悉的解法保证正确率,再在注释里补充优化方案,让人一眼看出你有复杂度意识。
再说手写bind,这几乎是当年必考的手写题之一。核心要求如下:
Function.prototype.myBind = function(context) { if (typeof this !== 'function') { throw new TypeError('Error'); } var args = Array.prototype.slice.call(arguments, 1); var fn = this; return function F() { // 因为 bind 返回的函数可能被 new,此时 this instanceof F 为 true if (this instanceof F) { return new fn(...args, ...arguments); } return fn.apply(context, args.concat(Array.from(arguments))); }; };这里有个细节容易忽略:bind返回的函数是可以被new的,而且new的时候this不会被替换成传入的context。很多人在手写时根本没考虑这一点,导致实现不完整。这不是“写不写得出来”的问题,而是“有没有真正理解bind”的问题。出题人从你的实现细节里能看出的东西很多。
3. 实操复盘:从题目到答题的完整策略
这一部分想聊聊更“现场”的东西。无论你准备得多么充分,考场上策略不对,照样容易翻车。我把这套笔试当时比较通用的答题策略整理了一下,尤其是时间分配和答题顺序这两件事。
3.1 时间分配:60分钟的选择题,别恋战
典型的一套web前端笔试题量,大概是一两个小时。如果是选择题为主的前半张卷子,时间节奏就很重要。我的建议是这样的:
- 前30分钟,快速过完所有选择题。遇到拿不准的,先标记下来,不要在单选上纠结超过2分钟。
- 中间30分钟,集中攻克填空题和代码输出题。这类题往往有固定答案,写出来就是拿到了。
- 最后的时间,留给手写代码题和简答题。这些题每题分值大,但也是最容易卡住的地方。
我当时在考场上用过一个小技巧,遇到“闭包+循环变量”的经典题时,直接套结论。比如:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }这题输出的是5个5,原因就是var没有块级作用域,循环结束后i已经是5了,所有的回调函数共享同一个i。如果你清楚这个结论,答题只花10秒;如果你非要现场推导,每次都得从作用域链一步步捋,时间就白白耗掉了。
3.2 手写代码题:框架时代的基本功较量
在2017年那个时间点,Vue和React已经大面积普及,但笔试手写题依然坚定地考原生JS。这套题里也延续了这个传统,比如让你手写一个节流函数、手写一个深拷贝、手写一个简单的事件发布订阅类。这些题放在今天依然是很多公司面试的高频题,说明出题人看重的东西一直没有变。
以深拷贝为例,一个稳健的深拷贝实现,要能处理数组、普通对象、Date、RegExp、循环引用这几个场景。在那个年代,很多人还停留在JSON.parse(JSON.stringify(obj))就能解决一切问题的认知里。遇到函数、undefined、循环引用就直接丢数据。而正确的实现思路应该是:先判断类型,基础类型直接返回;引用类型拷贝后递归处理子属性;用WeakMap记录已经拷贝过的对象来防止循环引用导致死循环。
如果你在笔试里完整地写出了一个支持循环引用的深拷贝,这个代码题基本就是满分卷了。因为大部分候选人只会写到递归拷贝那一步,甚至有些人连hasOwnProperty都忘了加,导致原型链上的属性也被拷贝进去。
3.3 简答题如何拿高分
简答题在评分上是最有“信息差”的。它不是简单的对错评判,而是看你答题的条目化程度。比如问你“列举前端性能优化手段”,一个直接写“压缩图片、合并请求、用CDN”的,和一个从网络加载、页面渲染、JavaScript执行三个维度分条作答的,得分差距会非常大。
这里有一个答题模板可以作为参考:
- 先给出一个概括性的分类框架。比如把优化手段划分为“加载阶段”和“渲染阶段”。
- 在每个阶段下面,用一句原理加上一句具体措施展开。比如“加载阶段:对静态资源开启HTTP缓存,通过设置Cache-Control减少重复请求”。
- 最后补充一个“这在项目里是怎么落地的”的细节,比如自己实际配置过哪些参数。
这种“分类->原理->落地”的答题结构,在阅卷老师眼里和那些零散作答的区别,就是一份标准答案和草稿纸的区别。尤其是web前端这个方向,考察的就是条理性和系统性,简答题就是你展示这两点的最佳阵地。
4. 常见问题与排查技巧实录
说实话,当初很多同学做完这套笔试题之后,在群里对答案的场面还挺热闹的。错的题往往不是特别偏的知识点,反而是那些“以为自己会了”的地方。我在这里整理几个当时讨论度最高的坑,以及对应的复盘方法。
4.1 当年大家最常踩的坑
第一个坑:把null判断成对象。有一道选择题问typeof null输出什么,答案是object,但很多人会问:“为什么null不是null类型?”原因其实是个历史遗留问题。JavaScript最初的设计里,typeof通过判断类型标签来返回字符串,而null的类型标签是0,和对象一样,就这么一直保留了下来。这个知识点虽然看起来“没用”,但在笔试里就是一道经典的陷阱题。
第二个坑:以为==和===只是严格程度的区别。更准确地说,==会做隐式类型转换,===不会。题目常常这样出:
0 == ''; 0 === ''; false == 0; null == undefined;正确答案分别是true、false、true、true。这些东西平时开发中都在用,但如果你没有系统总结过隐式转换的规律,考试时只能靠猜。
第三个坑:闭包导致的完美“错误答案”。比如手写一个计数器函数,要求每次调用返回的值加1,不少人的第一版实现是这样的:
function counter() { var count = 0; return count++; }直接调用输出的永远是0。问题清楚,就是没有真正形成闭包。你得返回一个内部函数去修改外层的count:
function createCounter() { var count = 0; return function() { count++; return count; }; }这种题目难吗?不难。但它混淆了一大批“背过闭包定义,却写不出闭包代码”的人。这也正是笔试的意义所在——把知识点放到具体场景里检验,让人没法靠“名词解释”蒙混过关。
4.2 这套题对今天的web前端开发还有什么用
你可能会说,这是2017年的题了,现在都什么年代了,框架都更新好几轮了,翻这些老黄历还有意义吗?我反而觉得更有意义了。
因为框架迭代越快,底层基础就越显得重要。现在的web前端开发,打开一个项目就是Vue3或者React18,模板语法、Hooks、状态管理库一应俱全,很多东西的“默认配置”已经把复杂逻辑藏了起来。但遇到线上问题的时候,比如页面白屏、事件不触发、跨域请求失败,你最终还是得回到浏览器原生机制里去排查。这套笔试里考察的原型链、事件循环、跨域原理、HTTP缓存,在今天的日常开发里依然是排查问题的底层工具。
举个例子,现在大部分项目都会用到前端路由。不管是vue-router还是react-router,核心原理就两个:hashchange事件和history API。有些同学平时用得很溜,但一旦被问到“为什么刷新hash模式不会导致404,刷新history模式会”,就卡住了。这就是典型的基础知识不牢。
拿这套题当镜子,你可以做一次自检:
this在不同调用场景下的指向,你能在30秒内说清楚吗?- 事件循环里,宏任务和微任务的顺序你能画出来吗?
- 深拷贝如果要求处理循环引用,你能手写正确吗?
- HTTP缓存字段
Cache-Control和Expires的区别,你能讲明白吗?
如果这些都能做到,那不管哪一年的笔试,对你来说都不会构成太大威胁。如果有些地方还需要想一下,那我建议把这些点重新捡起来。web前端开发这个方向,看起来很依赖框架更新速度,但真正决定一个人能走多远的,往往是他对底层基础的理解深度。
这套2017年的题目,给我最大的启发倒不是哪道题怎么解,而是它证明了:热闹的框架会变,但核心的知识体系会一直沉淀下来。今天再回头看你当年错过的那些题,能补的补,能总结的总结,这才是校招笔试留给你最实在的东西。