最近好几个学弟学妹都在准备秋招,有人拿着B站2023校园招聘前端开发方向笔试卷A问我这题值不值得刷。我的看法很直接:这套卷子的含金量不在于题目本身,而在于它把前端校招最常考、最容易翻车的一批知识点全串了一遍。不管你是应届生、打算年后跳槽的初级前端,还是带新人的老手,按这套卷子的考点做一次自查,基本能摸清自己基础技能的底子。
这篇文章我会尽量还原卷面的题型结构,逐个拆解高频考点背后的原理,把代码输出题的推理过程、手写题的解题思路都摊开讲。每道题我都会给出分析和参考写法,还会补充一些笔试时容易踩的坑。内容偏长,建议收藏后跟着敲一遍,尤其是手写题,光看不练真的会翻车。
1. 整套卷子到底在考什么
1.1 题型分布与答题节奏参考
这套卷子整体风格偏基础加综合,题型大体分四块:选择题、代码输出题、手写编程题、简单问答题。按照常见校招笔试的配置,90分钟左右的题量里,选择题大概20道上下,输出题5到8道,手写题2到3道,问答题1到2道。分值占比通常和题量挂钩,但更关键的是考察方向:
| 题型 | 大致题量 | 主要考察方向 |
|---|---|---|
| 选择题(单选/多选) | 20道左右 | JS基础、CSS布局、网络协议、浏览器缓存 |
| 代码输出题 | 5-8道 | 作用域、this指向、事件循环、闭包 |
| 手写编程题 | 2-3道 | 防抖节流、深拷贝、数组处理、算法基础 |
| 问答题 | 1-2道 | Vue/React原理、性能优化方案、工程化理解 |
答题节奏上,我建议选择题平均一分钟内解决,遇到拿不准的不要恋战,先标记跳过。输出题是拿分大项,每题控制在三到五分钟,推理过程写在草稿纸或者代码注释里,方便回头检查。手写编程题一题至少留十五到二十分钟,这类题分值高、批改标准也最硬,跑不出结果基本就是零分。问答题放在最后,只要写了要点就有分,别空着。
1.2 考点背后的能力模型
B站这类平台公司招前端校招生,其实不是想找一个“API调用熟练工”,而是希望候选人具备四个底层能力:语言基础扎实、理解浏览器运行环境、具备工程化意识、算法思维够用。
语言基础对应JS的变量提升、作用域、原型链、异步这些老生常谈;浏览器环境对应渲染机制、缓存、安全策略;工程化对应模块化、构建工具、代码规范、组件封装;算法思维不要求你手撕红黑树,但常见的排序、去重、递归、大数处理要能写出来。这套卷子的题目基本都是围绕这四块出的,了解这层逻辑之后再刷题,心里会更有底,不会每道题都当成孤立的碎片去背。
2. JavaScript基础考点:最容易拉开差距的细节
2.1 变量提升与暂时性死区
JS基础部分,变量提升是选择题和输出题的高频常客。看这段代码:
console.log(a); // 输出什么? var a = 1;很多人第一时间反应是“报错”,但正确输出是undefined。原因是var声明的变量会被提升到当前作用域顶部,但赋值操作还停留在原地。上面的代码等价于:
var a; console.log(a); a = 1;let和const就不一样了,它们虽然也有提升,但在声明之前访问会进入暂时性死区,直接抛ReferenceError。我笔试时见过一道很经典的组合题:
var b = 10; function fn() { console.log(b); let b = 20; } fn();这题输出不是10,而是直接报错。很多人的误区在于“外层有var b,所以内部能读到”,实际上let b在自己的块级作用域里已经把b“锁”住了,在声明前访问就触发暂时性死区。理解这个机制的关键点在于:JS 在执行一段代码前会先完成“变量登记”,var登记时初始化为undefined,let/const登记时不初始化,直到执行到声明语句才绑定值。
2.2 闭包与this指向的组合拳
输出题里,闭包和this经常是一起出现的。先看一个几乎是标配的循环闭包题:
for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }, 100); } // 输出什么?答案是3 3 3,不是0 1 2。原因是var声明的i属于全局作用域,三轮循环结束后i已经变成3,三个定时器拿到的都是同一个变量。想要输出0 1 2,有三种改法:把var改成let,或者用 IIFE 传参,或者用setTimeout的第三个参数:
for (var i = 0; i < 3; i++) { setTimeout(function (n) { console.log(n); }, 100, i); }这套题考的其实是“闭包保留的是变量引用,不是值”这个底层逻辑。至于this指向,记住一条核心规则:this指向函数被调用时的执行上下文,而不是定义时所在的对象。箭头函数除外,它的this是在定义时从外层作用域捕获的。笔试里常出的变体包括对象方法拆出来调用、call/apply/bind改变指向、函数内部嵌套普通函数导致this丢失,这几类最好都亲手写一遍。
2.3 原型链与继承的底层逻辑
原型链这块,选择题喜欢出instanceof的判断,问答题喜欢让手写继承。先看instanceof的原理:它检查的是构造函数的prototype是否出现在对象的原型链上。
function Person(name) { this.name = name; } const p = new Person("zhang"); console.log(p instanceof Person); // true console.log(p instanceof Object); // true手写继承是手写题的高发区,ES6 的class虽然简单,但笔试有时会要求用 ES5 实现。一个比较标准的寄生组合式继承可以这么写:
function Parent(name) { this.name = name; } Parent.prototype.sayName = function () { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;这里重点说下为什么不能直接Child.prototype = Parent.prototype。这样赋值会导致父子原型对象完全共享,往Child.prototype上添加方法时Parent.prototype也会被改掉,污染父类。Object.create(Parent.prototype)则创建了一个以父类原型为原型的中间对象,既继承了父类方法,又不会反向污染。
2.4 事件循环与异步输出顺序
异步输出题基本是这套卷子的压轴题型,因为它的推理链条够长,能一次性考察宏任务、微任务、Promise、async/await多个知识点。先给一个经典例子:
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。关键点在于:Promise.then属于微任务,在当前宏任务的同步代码执行完之后立即执行;setTimeout属于宏任务,要等到下一个宏任务阶段才执行。很多人在笔试时卡在setTimeout和Promise到底谁先谁后,记住一句话:宏任务先排队,微任务清空后,再取下一个宏任务。
async/await的版本再复杂一点:
async function test() { console.log("a"); await Promise.resolve(); console.log("b"); } test(); console.log("c");输出是a、c、b。注意await后面的代码相当于被放进了.then回调里,属于微任务。在浏览器环境里,console.log("c")先于微任务执行,所以b最后才打印。这类题自己敲一遍比死记十个例子都管用,因为每次执行顺序的差异,都能帮你在脑中建立“任务队列”的模型。
3. 浏览器、网络与性能优化
3.1 HTTP缓存机制:强缓存与协商缓存
浏览器网络部分,缓存是必考项。核心要分清两种缓存:强缓存和协商缓存。强缓存由Cache-Control和Expires控制,命中后直接从本地读,根本不会发请求到服务器;协商缓存由ETag/If-None-Match和Last-Modified/If-Modified-Since控制,需要和服务器确认资源是否变化。
| 缓存类型 | 主要字段 | 判断过程 | 触发条件 |
|---|---|---|---|
| 强缓存 | Cache-Control, Expires | 直接读本地,不发请求 | 缓存未过期 |
| 协商缓存 | ETag/If-None-Match, Last-Modified/If-Modified-Since | 发请求给服务器判断 | 强缓存未命中 |
实际开发中,面试官常追问“强缓存过期后怎么办”,答案是进入协商缓存流程:浏览器带着If-None-Match(或If-Modified-Since)请求服务器,服务器返回304 Not Modified,浏览器继续用本地缓存,只有资源真的变了才返回200和新资源。写业务的时候,我习惯给index.html设置Cache-Control: no-cache,给带 hash 的静态资源设置max-age=31536000,这样既能保证入口文件及时更新,又能让静态资源最大化利用本地缓存。
3.2 浏览器渲染流程与回流重绘
渲染机制这块,笔试常考“从输入URL到页面展示发生了什么”,或者在问答题里让你说“如何减少回流和重绘”。整个渲染流程大致是:解析 HTML 生成 DOM 树,解析 CSS 生成 CSSOM,两者合并成渲染树,然后进行布局(Layout)计算几何信息,最后绘制(Paint)到屏幕上。
回流说的是布局阶段重新计算几何属性,重绘说的是像素更新。最直观的理解:改变宽度、高度、字体大小等几何属性会触发回流;只改颜色、背景图等视觉属性只触发重绘。回流的开销远大于重绘。写代码时减少回流的手段包括:用transform代替top/left做动画,把频繁读取布局属性的操作合并,用DocumentFragment批量操作 DOM,这些都属于加分项。
3.3 安全基础:XSS与CSRF
浏览器安全防线上,XSS 和 CSRF 是问答题老面孔。XSS 的完整叫法是跨站脚本攻击,攻击方式是把恶意脚本注入到页面里执行。最常见的防御手段是转义用户输入的内容,在 React 和 Vue 里框架本身就做了默认转义,但如果你用v-html或者dangerouslySetInnerHTML直接渲染用户输入,就要格外小心。
CSRF 是跨站请求伪造,攻击原理是借助用户在目标网站已登录的 Cookie,诱导用户访问恶意页面并发送伪造请求。防御方案主要是校验Referer、使用 CSRF Token,以及把关键接口的 Cookie 设置为SameSite。这一节内容不难,但能体现一个候选人是否写过真实业务、有没有安全意识。
4. 框架与工程化:Vue核心与前端基建
4.1 Vue响应式原理与diff
框架题在这套卷子里通常以选择题或问答题出现,Vue 的概率很大。Vue 2 的响应式原理是基于Object.defineProperty遍历 data 对象的每个属性做数据劫持,Vue 3 改成了Proxy代理整个对象。后者最大的优势是不需要提前递归遍历属性,新增删除属性也能被响应式系统捕获,性能上更优。
关于v-for里的key,卷子喜欢问“为什么不能用 index 作为 key”。原因是 diff 算法会通过 key 判断新旧虚拟节点的对应关系,用 index 做 key 时,如果列表头部插入了一条数据,后面的节点全部要重新渲染,造成不必要的性能损耗。输出题不太会考这块,但问答题很爱从框架原理切到实际项目。
nextTick也是高频考点,它的作用是等 DOM 更新完成后再执行回调。比如在 Vue 里修改了响应式数据,立刻操作 DOM 可能拿不到最新的值,因为 DOM 更新是异步的。nextTick的实现原理是把回调放进一个微任务队列,在下次 DOM 更新循环结束后执行,理解了事件循环再看nextTick就水到渠成了。
4.2 前端工程化与代码规范实践
工程化部分,卷子会考察模块化、构建工具、代码规范这些“软实力”。模块化方面,ES Module 和 CommonJS 的区别是送分题:export/import是静态分析、在编译时确定依赖关系,module.exports/require是动态加载、在运行时确定。ES Module 可以做 tree-shaking,CommonJS 不行,这就是为什么现在新项目都优先用 ESM。
“前端开发规范vue”这个话题这几年越来越热,笔试可能不会直接考规范的条目,但问答题会让你“说说项目里如何保证代码质量”。比较落地的答案是:用 ESLint 做静态检查、Prettier 统一格式化、husky 在 git commit 前跑 lint 和单测,再加一套 component 目录划分规范。我在项目里一般把组件目录按components/业务模块/组件名/index.vue组织,每个组件只负责一个职责,超过三百行就要考虑拆分。
4.3 前后端协作和组件封装细节
现在不少校招卷会加一道“前后端联调时遇到的问题与解决”的开放题,这时候懂一点接口设计和参数处理就很有优势。比如后端返回字段不统一、嵌套层级深,前端写一堆user.info.address.city很容易出错。比较专业的做法是前端做一层数据适配器,把后端字段转换成页面需要的结构,后续后端改动只改适配器一处。
组件封装方面,...arg这个写法经常出现,它叫剩余参数或展开运算符,在二次封装组件时特别常用。比如要封装一个带默认样式的按钮组件,但允许使用方透传原生按钮的所有属性:
<template> <button class="base-btn" v-bind="$attrs"> <slot /> </button> </template>在 React 里写法就是const MyButton = ({ ...props }) => <button className="base-btn" {...props} />。这个细节能看出候选人写组件时有没有“可复用、可扩展”的意识。顺带提一句,这两年 AI 辅助编码确实能提速,不少团队也在尝试 AI 生成基础页面,笔试场上通常不给用工具,但平时练习可以拿它当“结对搭档”,让它帮你 review 代码,会开阔不少思路。
5. 编程题实战解析
5.1 手写防抖和节流
防抖和节流是手写题里的“保底题”,几乎每家都会考。一个容易混淆的点:防抖是“停止触发后延时执行”,节流是“固定间隔内最多执行一次”。业务场景里,搜索框输入用防抖,滚动监听、窗口缩放用节流。
标准防抖写法:
function debounce(fn, wait) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); }; }标准节流写法:
function throttle(fn, interval) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; }两个函数里我都刻意用了...args和fn.apply(this, args),这能保证回调里的this指向不出错,笔试时写上这行是加分项。别只写fn(),那样即便逻辑对了,严谨性上也差了一截。
5.2 手写深拷贝
深拷贝是另一个高概率题。很多人会直接写JSON.parse(JSON.stringify(obj)),这招面试官肯定要追问:遇到undefined、函数、Symbol、循环引用怎么办?所以笔试如果要求手写深拷贝,最好直接上一个能处理常见边界条件的版本:
function deepClone(obj, map = new WeakMap()) { if (obj === null || typeof obj !== "object") return obj; if (map.has(obj)) return map.get(obj); if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); const clone = Array.isArray(obj) ? [] : {}; map.set(obj, clone); Reflect.ownKeys(obj).forEach((key) => { clone[key] = deepClone(obj[key], map); }); return clone; }WeakMap专门用来处理循环引用,Reflect.ownKeys可以拿到包括 Symbol 在内的所有属性。整个写法既不过度复杂,又能覆盖大部分笔试题的边界要求。手写题不是越炫越好,关键是结构清晰、边界完整,阅卷人能一眼看懂你的思路。
5.3 数组去重与扁平化
数组去重属于基础中的基础,但考察方式可以变出花来。简单版本用Set就够了:
const unique = (arr) => [...new Set(arr)];如果要求去重对象数组,根据某个字段去重,就得用Map:
const uniqueBy = (arr, key) => { const map = new Map(); arr.forEach((item) => { if (!map.has(item[key])) map.set(item[key], item); }); return [...map.values()]; };数组扁平化也常考。进阶要求是“手写 reduce 实现 flat”:
function flatten(arr) { return arr.reduce((acc, cur) => { return acc.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []); }如果数组层数很深,还可以加一个depth参数控制扁平化层数。这类题目没什么高深技巧,但很考验代码基本功,平时写业务时遇到数组嵌套多、数据结构复杂的情况,多思考一步,考试时自然就能写出来。
5.4 大数相加与排序算法
算法题通常不会太难,但喜欢考“进阶版的基础操作”。比如大数相加:JS 的Number最大安全整数是2^53 - 1,超过这个范围精度就会丢失,笔试让你用字符串实现两个大数相加:
function addBigNum(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 x = i >= 0 ? Number(a[i]) : 0; const y = j >= 0 ? Number(b[j]) : 0; const sum = x + y + carry; result = (sum % 10) + result; carry = Math.floor(sum / 10); i--; j--; } return result; }核心就是模拟竖式加法,逐位相加并维护进位。排序算法里,快排是最高频的写法,代码不长但要练熟:
function quickSort(arr) { if (arr.length <= 1) return arr; const pivot = arr[0]; const left = []; const right = []; for (let i = 1; i < arr.length; i++) { arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]); } return [...quickSort(left), pivot, ...quickSort(right)]; }如果笔试要求不额外分配新数组,那就要写原地交换版本,不过校招场景下这种递归版已经能拿大部分分数了。
6. 备考思路与实战技巧
6.1 复习优先级怎么排
如果你准备时间有限,我的建议是把精力按这个顺序分配:JS 基础 > 浏览器与网络 > Vue/React 原理 > 手写题 > 算法题。JS 基础是输出题、选择题、编程题全都绕不开的地基,比如事件循环、作用域、闭包、Promise,这四项是重中之重。浏览器和网络考的是概念的准确性,花两个晚上集中背一遍缓存和渲染流程能拿到不少分。框架原理要靠平时积累,临时抱佛脚读源码读不进去,背一些白话版本的核心机制就够了。算法题每天刷个两三道,重点覆盖字符串处理、数组操作、二分查找、二叉树遍历。
6.2 别忽视代码输出题
很多人复习笔试喜欢死磕算法题,结果输出题翻车。输出题难在“看似简单,处处是坑”,一道题里可能同时包含变量提升、this指向、闭包和事件循环。比如这段:
var name = "global"; function A() { this.name = "A"; setTimeout(function () { console.log(this.name); }, 0); } var a = new A();这题的输出是global,不是A。因为setTimeout回调里的this指向全局对象,普通函数不会继承外层this,只有箭头函数才会。这个点是校招生最容易答错的。刷输出题时别只看解析,在浏览器控制台里跑一遍,把执行顺序用文字写出来,形成自己的推理模板,比刷十道新题都管用。
6.3 手写题的高分策略
手写题想要拿高分,不只要跑通,更要“好读、健壮、可扩展”。我总结一个三步骤流程:第一步,确认函数签名,比如防抖函数接收什么参数、返回什么类型;第二步,处理边界条件,比如空数组、null、循环引用;第三步,再考虑性能优化,比如快排是否原地排序、深拷贝是否用 WeakMap 解决循环引用。
很实用的小技巧是,在写代码前先用注释把思路写出来:
// 1. 如果参数不合法,直接返回 // 2. 用一个 Map 记录已经拷贝过的对象,防止循环引用 // 3. 如果是 Date/RegExp,单独处理 // 4. 遍历自身属性(包括 Symbol),递归拷贝这样阅卷人第一眼就能看到你的思考路径,即使某行代码写错了,也能根据注释判断你的思路是对的,能挽回不少分数。笔试系统的在线编辑器没有自动补全,很多同学平时用 IDE 习惯了,考试时连querySelector都拼写不顺畅,考前务必去牛客或者 LeetCode 的在线编辑环境练几道题,提前适应。
7. 常见问题与经验教训
7.1 时间分配出问题怎么办
校招笔试最常见的悲剧是:选择题花太多时间纠结,导致后面的编程题没写完。实际上编程题一道的分值可能顶得上五道选择题,而且编程题只要按题目要求实现了功能,正确率是确定的。我的策略是总体上“先做会做的,再死磕不会的”。选择题遇到没把握的,可以先按第一直觉选一个,标记一下,等编程题写完再回来改。笔试系统一般支持题号跳转,别死脑筋按顺序一题一题磨。
7.2 在线编辑环境的坑
在线笔试的代码编辑器和 IDE 完全是两回事。没有自动补全、没有报错标红、没有 ESLint 提示,甚至连缩进都要手动调。代码里少个分号、括号不匹配,这些都只能靠你肉眼检查。我的经验是,写完代码后做一次“口头跑查”:用几个典型输入在脑袋里过一遍,模拟每一步变量的变化。尤其是循环类题目,手动跑三层循环就能发现索引边界错误这类问题。另外要注意缩进和变量命名,平时写代码越规范,考试时敲出来的代码就越整洁,越容易自查。
7.3 卷子没考但面试大概率会问
这套卷子以笔试为主,但笔试通过后还有至少两轮技术面试,有些题笔试不考、面试必考。比如“讲一个你印象最深的项目,说下技术难点”,这题在笔试卷里很少出现,却是面试开场保底题。建议提前准备一个真实的项目案例,用“背景—方案—难点—效果”的结构讲清楚。另外像 HTTP2 和 HTTP3 的区别、浏览器标签页之间的通信方式、前端监控和错误日志上报机制,这类偏实战的题校招越来越爱问。趁笔试刷题阶段把这些知识点一并整理成笔记,面试时就能直接复用。
写在最后
说实话,这套卷子给我最大的感受是:它不考偏题怪题,考的都是前端日常开发里一定会遇到的东西。变量提升也好、事件循环也好、缓存策略也好,都是在真实业务中决定代码质量和排障效率的关键点。我当年自己做这套题的时候,在事件循环输出题上翻了车,原因就是当时只会背结论,不理解任务队列的工作原理。
如果你正在准备校招,我真心建议不要只刷题,把每个知识点背后的“为什么”搞明白。笔试只是入场券,面试里追问的深度往往超出你的想象。把基础打扎实,把原理吃透,比背十套卷子都管用。这套卷子能帮你发现短板,但真正拉开差距的,是发现短板之后你是否愿意花时间补上。