2023年秋招那波,途虎养车的前端笔试我印象还挺深的。当时好几个学弟学妹拿了这道题来问,我也跟着把整套题型翻了个底朝天。途虎的笔试题不算偏,但胜在覆盖面广,基础题、框架题、工程化题、场景题全都有,而且有些题目看似简单,实则埋了不少坑。这篇文章就把我对着这套题做的完整复盘写出来,从考点拆解到逐题解答,再到做题节奏和避坑技巧,一次性讲清楚,希望能帮后面准备前端笔试的同学少走点弯路。
1. 途虎前端笔试的考察思路:从题型反推团队要什么样的人
先说结论:途虎养车作为一家深耕汽车后市场的互联网公司,前端团队规模不算小,业务线覆盖用户端、门店端、供应链端。这种业务形态决定了他们招人时不是只要“会写页面”的人,而是需要能快速上手业务、有一定工程化思维、遇到复杂交互能扛住的开发者。笔试就是在第一轮帮你筛选掉那些基础不扎实、代码风格随意的候选人。
从题型分布来看,途虎这套笔试卷子大致分四个模块:计算机基础与网络、JavaScript语言特性、Vue框架深度、手写代码与场景设计。这四个模块的比例大概是2:3:3:2,前后端通用的基础题会占一部分,但真正拉开差距的是后面三道——手写题和场景题。
有一个点值得注意:途虎笔试题里几乎没有偏题怪题,不考那种“打印出浏览器的诡异输出”的冷门细节,而是把精力放在高频、实用的知识点上。这说明出题人很清楚团队需要什么——你可以不知道某个冷门API的细节,但你得知道闭包怎么用、Promise怎么处理并发、Vue的响应式原理是什么。这也给我们划了一个很清晰的复习范围:与其花时间背冷门API,不如把高频核心知识点吃透。
另外,途虎的笔试时间一般给60到90分钟,题量在20到25题左右,包含选择题和编程题。这个时间压力意味着选择题必须得快,编程题必须一次过。很多人挂在笔试上不是因为不会做,而是因为时间分配不合理,后面的大题没时间写。我后面会专门讲做题节奏的问题。
2. 核心考点拆解:六类必考题型逐一分析
2.1 网络与浏览器基础:别在这些送分题上丢分
这块题目在途虎笔试中占比不大,但基本是必考的,通常是两三道选择题。考点集中在HTTP协议、浏览器缓存、跨域这几个方向上。HTTP状态码是高频中的高频,尤其是304、403、502这三个,很多同学只记得200和404,遇到304就开始瞎猜。304表示协商缓存命中,服务器告诉你“你可以用本地缓存”,这个知识点在后续实际开发中非常常用,因为静态资源的缓存策略基本都围绕着它设计。
浏览器缓存那一套也需要梳理清楚:强缓存和协商缓存的区别、Cache-Control和Expires的关系、ETag和Last-Modified的对比。笔试里常考的方式是给你一个响应头,问你下一次请求会不会发到服务器。这类题不难,但前提是你脑子里得有那张完整的请求流程图。
跨域问题也是必考项。JSONP的原理要能写出来,CORS的简单请求和预检请求要能分清,正向代理和反向代理的区别也得知道。途虎的业务里前端经常需要对接第三方平台的数据,跨域是绕不开的点,所以这题几乎是送分题,丢了太可惜。
2.2 JavaScript核心机制:闭包、原型链、异步三板斧
JavaScript部分的题目是拉开第一轮差距的地方。途虎的选择题和简答题都会涉及这块,核心考点高度集中在三个方向:闭包与作用域、原型链与继承、事件循环与异步。
闭包的题目通常是给一段代码,问输出结果,或者是让你手动实现一个私有变量。这类题考察的是执行上下文和作用域链的理解,不是背概念。原型链则是考察函数、对象、实例三者之间的关系,经典的instanceof原理、new的过程、原型链继承的写法都是高频考点。
异步这里要重点说,因为这是途虎笔试里占比最重的一块。事件循环机制、宏任务与微任务的执行顺序、Promise的链式调用和错误捕获,这些几乎是必考。我见过很多人在setTimeout和Promise的执行顺序上栽跟头,其实只要理解了一个核心点就好办:同步代码执行完,先清空微任务队列,再取一个宏任务执行,执行完再清空微任务,如此循环。
async/await的出现让异步代码更优雅了,但笔试里反而更容易出坑。因为很多人只学会了用async/await,却没真正理解它背后的Promise语义。比如await后面的代码什么时候执行?async函数里抛出异常会返回什么?这些问题在途虎笔试里出现过不止一次。
2.3 Vue框架考察:从API使用到原理理解
途虎的技术栈里Vue占了大头,所以框架题主要围绕Vue展开。考察层次有两种:一种是纯API使用,比如v-if和v-show的区别、computed和watch的区别、组件通信方式有哪些;另一种是原理理解,比如Vue2的响应式原理是怎么实现的、nextTick的原理是什么、key属性的作用是什么。
API层面的题相对简单,但容易答不全。比如问组件通信方式,如果你只回答了props和$emit,那最多拿一半分。完整的答案应该包含:父传子用props、子传父用$emit、兄弟组件用事件总线或Vuex、跨层级用provide/inject、任意组件用Vuex或Pinia。这种题考察的是你是否有完整的知识体系,而不是零散的API记忆。
原理层面的题才是分水岭。Vue2的响应式原理考察Object.defineProperty的用法和局限性,Vue3则考察Proxy的优势。这里建议把两者对比着复习,因为笔试很可能让你说说两者的区别。computed的缓存机制也是一个高频考点,很多人只知道computed有缓存,但不知道缓存是怎么实现的,更不知道watcher和computed之间的关系。这块如果时间允许,建议去看一下Vue源码里的lazy选项和dirty标记,看懂了就一通百通。
2.4 手写代码题:防抖节流、深拷贝、Promise并发
编程题是途虎笔试的重头戏,也是最能拉开分差的部分。根据2023年秋招同学的反馈,手写题集中在防抖节流、深拷贝、Promise并发控制这几类。这些都是前端面试的经典题目,但笔试题有自己的特点:要求更严格,可能连注释都要求写清楚。
防抖和节流是必考的手写题。考点不仅在于能写出来,更在于边界情况的处理。比如防抖的this指向问题、参数透传问题、立即执行版本和延迟执行版本的区别,这些细节没处理到位,代码虽然能跑,但面试官一眼就能看出来你的水平。
深拷贝这道题看起来简单,但想拿满分不容易。需要考虑循环引用、特殊类型(Date、RegExp、Map、Set)、symbol作为key的情况。如果你只是简单地用JSON.parse(JSON.stringify(obj))蒙混过关,那这道题基本就没分了。
Promise并发控制是我最想强调的一道题。它考察的是对Promise原理和异步流程控制的理解,通常以“实现一个limit函数,控制并发请求数量”的形式出现。这道题有几个层次:能写出来是基础,能处理错误是加分,能动态添加任务才是真正的理解。笔试现场时间有限,大多数同学只能做到前两层,但如果你能做到第三层,这道题基本上就锁定胜局了。
2.5 场景设计题:从业务出发,考察架构意识
途虎笔试的最后一道题通常是场景设计题,这也是最有意思的部分。2023年的题目大致方向是:设计一个门店列表页面,要求包括搜索、筛选、分页、滚动加载、状态管理等功能,并说明技术选型理由。
这种题没有标准答案,考察的是你的架构意识和业务理解能力。出题人会通过你的回答判断:你有没有做过真实项目,还是只会照着文档写组件。如果你回答的时候只列了一堆组件名和API,没有讲清楚数据流怎么走、状态管理怎么设计、性能优化怎么做、错误处理怎么兜底,那这道题基本就废了。
正确的思路是先拆解需求,再设计数据流,最后才考虑具体实现。比如:搜索和筛选的参数应该放在URL上还是放在Pinia里?滚动加载和分页如何共存?列表项组件的key怎么设计?图片懒加载怎么实现?页面离开后如何保持状态?这些细节才是面试官真正想看到的。
2.6 综合素养题:基础概念和代码规范
途虎笔试偶尔还会穿插一两道综合题,比如git操作命令、ES6新特性、代码规范相关的选择题。这类题占比不大,但体现了团队对工程化基础的看重。git的rebase和merge的区别、如何撤销上一次提交、如何修改commit信息,这类操作题是实际开发中每天都会碰到的,不会的话确实说不过去。ES6的考察重点是解构赋值、扩展运算符、模板字符串、箭头函数和普通函数的区别。代码规范则会涉及命名规范、组件设计规范、CSS规范等,这些虽然没有标准答案,但基本常识要懂。
3. 典型真题解题实录:三道题的完整拆解与满分答案
3.1 手写题:实现一个带缓存的深拷贝函数
深拷贝这道题,我先把笔试现场最常见的错误版本写出来:
// 错误版本:只处理了普通对象和数组 function deepClone(obj) { if (Array.isArray(obj)) { return obj.map(item => deepClone(item)); } if (typeof obj === 'object' && obj !== null) { const result = {}; for (let key in obj) { result[key] = deepClone(obj[key]); } return result; } return obj; }这个版本的问题有两个:一是没有处理循环引用,遇到循环对象会栈溢出;二是没有处理Date、RegExp等特殊类型,拷贝出来是空对象。满分答案需要解决这两个问题,我给出一个笔试现场能写完的版本:
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.source, obj.flags); } if (obj instanceof Map) { const result = new Map(); map.set(obj, result); obj.forEach((value, key) => { result.set(key, deepClone(value, map)); }); return result; } if (obj instanceof Set) { const result = new Set(); map.set(obj, result); obj.forEach(value => { result.add(deepClone(value, map)); }); return result; } // 处理数组和普通对象 const result = Array.isArray(obj) ? [] : {}; map.set(obj, result); Object.keys(obj).forEach(key => { result[key] = deepClone(obj[key], map); }); // 处理 symbol 类型 Object.getOwnPropertySymbols(obj).forEach(sym => { result[sym] = deepClone(obj[sym], map); }); return result; }这个版本有几个关键点需要说明。WeakMap是我在笔试中用来处理循环引用的核心数据结构,它的特点是key必须是对象,而且不会阻止垃圾回收。Map和Set的处理逻辑是先把自身放进map里,再递归处理内部数据,这样循环引用时能正确返回。symbol类型用Object.getOwnPropertySymbols获取,很多人容易忽略这一点。
笔试现场写代码,建议在草稿纸上先列出需要处理的类型清单,再动笔写,这样可以避免漏掉分支。另外,函数不需要拷贝,因为函数是引用类型,直接返回即可,这个在注释里说明一下,面试官会认为你考虑到了这个细节。
3.2 手写题:实现一个限制并发数的异步调度器
这道题在途虎笔试里属于压轴题,考察的是Promise异步控制能力。题目通常这样描述:实现一个函数,接受一个并发数limit,返回一个方法,调用这个方法可以往队列里添加异步任务,并且保证同时执行的任务数不超过limit。
我给出一个笔试现场能写出来的标准答案:
class Scheduler { constructor(limit) { this.limit = limit; this.runningCount = 0; this.queue = []; } add(task) { return new Promise((resolve, reject) => { const runTask = async () => { this.runningCount++; try { const result = await task(); resolve(result); } catch (error) { reject(error); } finally { this.runningCount--; this.next(); } }; if (this.runningCount < this.limit) { runTask(); } else { this.queue.push(runTask); } }); } next() { if (this.queue.length > 0 && this.runningCount < this.limit) { const task = this.queue.shift(); task(); } } } // 使用示例 const scheduler = new Scheduler(2); const timeout = (time) => new Promise(resolve => { setTimeout(resolve, time); }); const addTask = (time, name) => { scheduler.add(() => timeout(time)).then(() => { console.log(name, 'done'); }); }; addTask(1000, '任务1'); addTask(500, '任务2'); addTask(300, '任务3'); addTask(400, '任务4'); // 输出顺序:任务2、任务3、任务1、任务4这道题的核心思路是用一个count计数器和一个任务队列。添加任务时,如果当前执行数小于limit,立即执行;否则推入队列。任务执行完毕后,从队列里取出下一个任务执行。这个思路是最朴素的版本,但已经能正确工作。
写这道题时有几个容易错的点。第一个是add方法必须返回一个Promise,这样调用方才能拿到任务的结果。第二个是runTask函数内部的try/catch/finally结构,catch用来Promise rejection的正确传递,finally用来保证计数减一和下一个任务启动。第三个是队列里的任务必须是“可以被调用”的函数,而不是原始task,因为task只有在轮到自己执行时才需要被正式调用。
如果笔试时间充裕,可以进一步扩展:给Scheduler添加一个暂停和恢复的方法,或者支持指定每个任务的优先级。这些虽然不要求写,但如果能写在注释里,会让面试官对你的设计能力印象深刻。
3.3 场景题:门店列表页面的设计思路完整回答
场景题我摘录一个典型的回答框架,大家可以直接参考。题目是:设计一个门店列表页面,要求支持搜索、筛选、分页、滚动加载,说明你的设计思路和技术选型。
我会这样回答:
在技术选型上,首选Vue3 + Pinia + TypeScript。Vue3是当前团队的主力框架,Pinia相比Vuex更轻量,TypeScript能在编译阶段发现类型错误,减少运行时bug。
在数据流设计上,我倾向于把搜索关键词、筛选条件、当前页码、列表数据都放到Pinia store里,而不是放在组件内部。原因有两点:一是这些状态是跨组件共享的,搜索框、筛选面板和列表组件都需要读写这些状态;二是页面需要保持状态,如果用户从列表页跳转到详情页再返回,店列表的搜索条件和滚动位置应该保留。用Pinia可以很方便地做到这一点。
具体设计如下:
// store/modules/storeList.js import { defineStore } from 'pinia'; export const useStoreListStore = defineStore('storeList', { state: () => ({ keyword: '', filter: { region: '', brand: '', serviceType: '' }, page: 1, pageSize: 10, list: [], total: 0, loading: false, finished: false, scrollPosition: 0 }), actions: { async fetchList() { if (this.loading || this.finished) return; this.loading = true; try { const { list, total } = await fetchStoreListApi({ keyword: this.keyword, ...this.filter, page: this.page, pageSize: this.pageSize }); this.list.push(...list); this.total = total; this.finished = this.list.length >= this.total; this.page++; } finally { this.loading = false; } }, resetList() { this.page = 1; this.list = []; this.finished = false; this.fetchList(); } } });组件层面只负责渲染和派发事件,搜索框的输入事件和筛选面板的点击事件都不直接发起请求,而是调用store里的resetList。列表组件监听滚动事件,滚动到底部时调用fetchList。
关于URL参数同步,这部分是区分普通开发者和高级开发者的细节。搜索关键词、筛选条件和页码应该同步到URL的query上,这样用户刷新页面后状态不丢失,也能直接通过URL分享一个带条件的列表页。具体实现可以用Vue Router的query来实现,监听route变化,反向同步store状态。
性能优化方面有三个点需要覆盖:列表项组件用v-memo或者v-once优化、图片懒加载用VueUse的useIntersectionObserver、列表数据分片渲染。最后一点尤其重要——如果一次性往DOM里塞几百个节点,即使数据拿到了也会卡顿。
错误处理方面,fetchList失败时要展示错误占位图并提供重试按钮,而不是让页面白屏。滚动加载失败时要有toast提示,并允许用户手动点击重试。
这样回答下来,整套方案有数据流设计、有分模块职责、有性能优化、有错误兜底,面试官想要看到的架构意识就展示出来了。
4. 做题节奏与常见问题排查实录
4.1 时间分配:选择题30分钟,编程题40分钟,场景题20分钟
时间分配是笔试中最重要也最容易被忽视的环节。我的建议是:选择题和简答题控制在30分钟内完成,编程题留40分钟,场景题留20分钟。整体节奏是“前紧后松”——前面的题做快一点,后面的大题才有充足时间思考和调试。
选择题里遇到不会的题,不要死磕,先标记跳过,做完所有的题之后再回头琢磨。因为很多选择题是知识面覆盖类的,你不会就是不会,想破头也没用,不如先保证拿满你会做的分数。
编程题的节奏是最难把控的。很多同学看到题就开始写代码,写了一堆才发现思路走了弯路,被迫推翻重来。我的习惯是:先花两三分钟把题目读透,在草稿纸上写清楚输入输出、边界条件和核心数据结构,然后才动笔写代码。这样可以大大降低写到一半发现思路错了的概率。
4.2 编程题的验证方法与边界检查
编程题写完只是第一步,验证才是关键。这里分享一个我在笔试现场常用的“三段验证法”:第一步,用题目里给出的示例跑一遍,看输出是否符合预期;第二步,自己构造一个边界测试用例,比如空数组、单元素数组、全是重复值的数组;第三步,在心里模拟一遍代码的执行过程,看逻辑是否正确。
防抖节流这类的题,边界条件尤其重要。防抖函数需要测试连续触发和停止触发两种场景,节流函数需要验证第一次是否立即执行、最后一次是否延迟执行。这些细节虽然笔试环境里没法真正跑起来验证,但如果你能在代码注释里把边界条件写清楚,阅卷人是能看出来的。
错误排查方面,最常见的三个问题分别是:变量名拼写不一致、函数参数顺序传错、忘了return。千万别觉得这些都是低级错误,笔试现场的时间压力下什么错误都可能犯。养成写完代码后逐行检查的习惯,能帮你挽回很多不该丢的分。
4.3 高频失分点总结:来自阅卷视角的复盘
我在组织这次复盘时,特意找了几位参加过阅卷的前端同事聊了聊,总结出以下这几个高频失分点:
第一,API记混了。Vue2的$set和Vue3的reactive、watchEffect和computed的区别说不清楚,把Vue2的写法套到Vue3的题目上。这需要在复习时建立对比表格,把新旧版本的知识点对应着记忆。
第二,事件循环题掉坑。输出结果题考察宏任务微任务执行顺序,很多人死记硬背“Promise优先于setTimeout”,但遇到嵌套的Promise和async/await时还是出错。原因是没有真正理解微任务队列的入队和出队机制。建议复习时自己画一下执行流程图,把每一步的入队出队情况写清楚。
第三,手写题边界处理不完整。深拷贝不处理循环引用、防抖不处理this指向、节流不处理参数透传,这些都是高分题变低分题的常见原因。面试官看一道手写题,优先级是:能用 > 边界处理 > 代码质量,很多人能写出来,但边界处理不完整,从满分掉到六七分。
第四,场景题没有得分点。很多人写场景题时只写了“用Vue写个页面,用axios请求接口,用v-for渲染列表”这种流水账,没有状态管理、没有数据流设计、没有性能优化、没有错误处理,整体看起来像课程设计而不像工程方案。场景题的得分点在于细节,细节越多越能体现你的真实项目经验。
4.4 笔试环境的准备与在线OJ的坑
最后补充一些笔试环境方面的经验。途虎这样的公司笔试一般通过牛客网或赛码网进行在线答题,这类在线OJ和本地IDE有几个不同点需要注意。
在线OJ的代码编辑器通常没有自动保存,也不是每次change都保存,最好在动手写代码之前先点击“保存”或“格式化”按钮确认环境状态。其次,有些OJ系统在刷新生效CSS样式上比较慢,推荐用Chrome浏览器打开笔试链接,其他浏览器在走一些前端框架题的时候可能会出现兼容问题。
编程题如果是在线OJ模式,需要严格按照系统指定的函数名和参数名来写。有些同学在自己本地IDE跑通了代码,粘贴上去却报错,原因是函数签名和系统预期不一致。提交代码前花一分钟检查函数名、参数列表和返回值类型,这个动作能帮你躲过很多莫名其妙的报错。
还有一点,笔试过程中如果遇到网络波动导致代码提交失败,一定要截图保留证据,有的公司会在笔试结束后提供邮件补交的渠道。不要因为环境问题把题目做了却传不上去,又没留证据,那损失就大了。
我个人在做了这么多场笔试复盘之后,最大的感受是:前端笔试其实是一个“熟练度游戏”,知识面上不会超出常见的那些考点——网络、JS、框架、工程化、场景设计——拼的就是你对这些常规知识点的掌握深度和快速应用能力。与其海量刷偏题怪题,不如把高频考点练到“肌肉记忆”的程度。希望这篇复盘能帮到正在准备前端笔试的同学,少踩一些坑,多拿一些分。