ARTICLE DETAIL

资讯详情

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

字节跳动前端秋招笔试题复盘:考点分析与备考策略

字节跳动前端秋招笔试题复盘:考点分析与备考策略 前段时间整理网盘里的旧资料翻出一份2017年秋天整理的笔记——字节跳动前端工程师秋招笔试卷的回忆版和我的答题复盘。这几年陆续带过一些准备校招的学弟学妹经常有人问我大厂前端笔试到底考什么字节的题难不难我每次都会把这份旧笔记翻出来讲一遍。虽然时间过去几年前端技术栈也变了几轮但那份卷子里暴露出的考察思路放在今天依然有很强的参考价值。这篇文章适合两类人看一类是正在准备前端校招或实习笔试的同学另一类是刚带新人、想了解大厂前端基础要求的工程师。我会从整张卷子的整体印象、选择题考点、手写代码题、算法题以及题后的备考复盘五个部分展开尽量把当时踩过的坑和后来复盘得出的结论都写清楚。1. 2017年秋招的技术背景与这份卷子的整体印象1.1 那年前端技术栈正处于转型期卷子也因此有了特殊印记2017年这个时间点很有意思。ES6已经不算新东西了但很多同学还在用ES5的写法写笔试代码React 16刚发布不久Vue 2.4/2.5正是最活跃的阶段但大学课程里教的还是jQuery那一套。字节跳动在2017年正处于业务快速扩张期招聘量不小笔试筛人的标准也卡得比较死。这份卷子给我的第一印象是它并没有刻意考特别冷门的知识点但覆盖面很广而且有一种“我就是要看看你平时有没有真正写代码”的态度。选择题部分有大量关于JavaScript语言细节、浏览器渲染机制、HTTP缓存策略的题手写代码题则集中在深拷贝、防抖节流、Promise的实现这类“平时开发中每天都在用但很少有人深究底层”的工具函数上最后还有两三道算法题撑难度。放在当时的环境里这套题的筛选逻辑非常清晰基础不扎实的人选择题就会挂掉一大片只会用框架不会写原生逻辑的人手写代码题会暴露痕迹没有刷过题的人算法题基本拿不到分。到了今天这种“三段式”考察逻辑依然是大厂前端笔试的主流框架。1.2 题量与时间分配带来的真实压力我记得当时的在线笔试系统是一个网页选择题和编程题分成了两部分整套题目的时间大概是一个半小时到两个小时。选择题数量不算少印象里有三四十道每题的分值不高但容错率很低。编程题通常是两道到三道需要手写完整代码有些题还要求在输入输出格式上严格符合要求。这里要特别提醒一下后来备考的同学字节的在线笔试系统对输入输出格式要求非常严格。本地跑得通不代表在线OJ能通过。常见的问题是多余的打印输出、没有处理多组输入、忘记显式转换类型。我当时就因为在递归函数里多加了一行调试用的console.log导致一个用例都过不了白白丢了一大题的分。这种失误在真实笔试里特别可惜因为代码逻辑本身是对的。整张卷子的难度分布并不均匀。很多选择题属于“一看就知道答案”的送分题但中间会突然穿插一道需要仔细推导的异步执行顺序题或者一道让你比较两个相似API区别的题节奏感很强。如果时间分配不当很容易在选择题上磨太久导致后面编程题时间不足。我的策略是拿到卷子先花两分钟把整张卷子扫一遍大概判断哪些题有把握、哪些题需要思考先做有把握的把需要推导的题留到后面。1.3 印象里的题型占比整理回忆版笔记的时候我顺手统计了一下题型分布大致是这样语言基础与JS细节约三成集中在闭包、this、原型链、事件循环、数组方法。浏览器与网络约两成集中在渲染流程、缓存、跨域、状态码。CSS与页面布局约一成集中在盒模型、flex、层叠上下文。框架与工程化约一成半React生命周期、Vue响应式原理、模块化与构建。手写代码约一成半集中在实用工具函数。算法与逻辑约一成分布在字符串、数组、链表和少量动态规划。这个比例在当年算比较常规的和很多一线互联网公司的大方向一致。字节的特点是JS细节题挖得比较深不是浮在表面的那种“闭包是什么”而是真的给你一段代码让你判断输出结果考察你对作用域和异步队列的理解程度。2. 选择题里的细节考点真正拉开差距的是知识边界2.1 原生JavaScript的高频陷阱与记忆方法选择题不是简单的背概念而是给一小段代码让你判断输出。很多题考的就是大部分人记忆模糊、模棱两可的地方。我复盘时把最容易出错的几类整理到了一起这些考点在当年的卷子里反复出现现在面试问基础题也依然绕不开。第一类是typeof 与数据类型判断。比如typeof null的结果是object但null并不是Object的实例typeof function(){}返回function而typeof []返回object。这类题本身不难但紧张状态下容易凭直觉答错。我当时给团队新人培训的时候常说别死记结论理解底层的逻辑typeof 是基于“类型标签”的判断null的类型标签历史上被错误地标记为对象这个是语言设计留下的坑ES6也没法改只能记住。第二类是关于this的指向。2017年的笔试选择题很喜欢考严格模式下函数的this、箭头函数没有自己的this、对象方法中嵌套函数的this指向window还是undefined。比如var name global; var obj { name: obj, say: function() { console.log(this.name); } }; var fn obj.say; fn();这个例子输出global因为fn调用时调用者是全局对象this指向global。很多同学以为对象方法提取出来再用this仍然指向原对象其实是错的。要记住一条规则this的绑定发生在调用时而不是定义时看调用点是谁.第三类是事件循环与异步顺序。经典的例子console.log(a); setTimeout(() console.log(b), 0); Promise.resolve().then(() console.log(c)); console.log(d);输出顺序是a d c b。Promise的回调属于微任务会在当前宏任务末尾清空setTimeout的最小等待时间即使传0也会在下一轮宏任务执行。这个顺序在2017年属于比较进阶的考点因为当时很多人的理解还停留在“setTimeout就是立即执行”的层面。第四类是数组方法之间的区别。map、forEach、filter、reduce、some、every这六个方法笔试里喜欢考“哪些方法会改变原数组”“哪些方法跳过空值”“map是否可以链式调用”。比如[1, 2, 3].map(parseInt)的结果——这个题是经典陷阱因为parseInt接收两个参数map会把索引作为第二个参数传进去所以结果是[1, NaN, NaN]。刷过题的人一眼就能看出来没见过的同学很容易掉坑。2.2 浏览器原理与网络协议的基础题前端工程师笔试里必然会有浏览器相关的题。字节这份卷子也不例外而且考得比较细不是浅尝辄止那种。HTTP缓存是高频考点。当年考了一道关于强缓存和协商缓存的题给了几个响应头字段让你判断哪些情况下浏览器会重新发起请求。我需要把这两类缓存的关系彻底理清楚。强缓存对应的是Expires和Cache-Control命中后根本不发请求协商缓存对应的是Last-Modified/If-Modified-Since和ETag/If-None-Match需要发请求由服务器决定返回304还是200。Cache-Control比Expires优先级高etag比Last-Modified优先级高。这些结论我记得很牢因为后来在性能优化场景里天天用到。状态码也是选择题的常客。301和302的区别、304表示未修改、403和404的区别、500和502的区别。2017年的卷子考过一道某个接口返回了302问浏览器接下来会做什么。这里有个容易忽略的点302的Location头部是必须有的浏览器会自动跳转到Location指定的地址。如果题目里没给Location那这个302是有问题的。跨域那部分考了JSONP的原理和CORS的基本规则。选择题里会问“以下哪些方式可以解决跨域”选项里会混着“WebSocket”“postMessage”“window.name”“修改document.domain”等各种方案。这些方案从不同角度解决不同场景下的跨域问题只背名词不看场景的人会选错。比如document.domain只适用于主域相同、子域不同的情况不是万能的。还有一类题关于页面渲染过程——从输入URL到页面展示发生了什么。这个题在笔试里经常以选择题或者简答题的形式出现字节当年是把它拆成了几个选择题考察每一步发生的顺序DNS解析发生在什么时候”“CSS会阻塞DOM解析吗”“JS脚本放在head里和body底部的区别”。这类题的关键在于理解HTML解析过程中遇到script标签会阻塞解析直到脚本执行完毕遇到CSS样式表不会阻塞DOM解析但会阻塞后续脚本的执行。理解了这一点就知道了为什么生产环境下把css放head、把js放body底部的原因。2.3 CSS与布局题看似简单其实暗藏陷阱CSS在字节的前端笔试卷里占的份量不算重但考得很有水平。比较典型的是flex布局那一题给定一个flex容器几个子项分别设置了flex属性问你最终每个子项的宽度是多少。这里考的是flex-grow、flex-shrink、flex-basis三者的复合计算很多人以为flex1就是等分宽度实际上要先把flex-basis计算进去再按比例分配剩余空间。层叠上下文也是个常考的知识点。z-index不是随便用就有效的它只会作用于同一层叠上下文的元素之间。当年考了一道题两个父元素分别创建了层叠上下文子元素的z-index不同问谁覆盖谁。这个题很能考察是否理解层叠上下文的嵌套结构。我当时总结的一句口诀是“先比父级上下文再比同级别内部的层叠顺序”简单且好记。盒模型那边比较基础主要考标准盒模型和IE怪异盒模型的区别以及box-sizing对width计算的影响。但有一道题比较刁钻一段内容设置了高度overflow为hidden问此时padding和border是否仍然会影响布局。答案是会影响因为overflow裁剪的是内容区域padding和border依然占据布局空间。这种细节如果没动手写过样式光靠背概念很难答对。3. 手写代码题阅卷人真正看的是工程素养3.1 必考的深拷贝边界比语法更重要手写题里出现过一道深拷贝要求实现一个函数能够复制一个包含嵌套对象、数组的JavaScript结构。这道题看起来不难但从阅卷角度来说非常能区分水平。很多人的第一版答案是这样的function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }这段代码放在笔试试卷里基本拿不到分或者只能拿一小半。JSON序列化的方式有几个硬伤函数、undefined、symbol、RegExp、Date会被丢失或变成字符串循环引用会直接报错。2017年那会儿如果能写出下面这个版本基本就能把这道题的分数拿满function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } if (map.has(target)) { return map.get(target); } const clone Array.isArray(target) ? [] : {}; map.set(target, clone); Object.keys(target).forEach((key) { clone[key] deepClone(target[key], map); }); return clone; }这段代码的核心在三个地方。第一用typeof判断基础类型直接返回第二用WeakMap记录已经克隆过的对象解决循环引用问题第三用Array.isArray区分数组和普通对象保证克隆后结构一致。这里选WeakMap而不是普通对象是因为WeakMap的键是弱引用不会造成内存泄漏在递归处理大型结构时更安全。当然这个版本还没有处理Date、RegExp、Map、Set这些特殊对象。我在复盘的时候把题目扩展了一下补上了类型判断分支。但笔试阅卷人通常不会苛求到这一步能写出基础结构并说明为什么用WeakMap已经超过了大多数人。如果时间充裕可以在函数开头加一段处理正则和日期的逻辑算是加分项。3.2 防抖与节流最常见的工具函数最容易被忽略的细节防抖和节流几乎是前端笔试和面试的“必考题”字节这份卷子也考了。有趣的是题目不是直接让写防抖函数而是给了一个场景用户输入关键词触发搜索请求要求减少请求频率并说明使用防抖还是节流为什么。正确的选择是防抖。搜索场景中用户停止输入一段时间后再发起请求这是防抖的典型用法节流适用于滚动、拖拽等持续触发且需要固定间隔执行的场景。前者关注“最后一次”后者关注“固定频率”。手写防抖时有几个细节容易被忽略。第一个是this的绑定——如果不做处理防抖函数内部的this会指向错误。第二个是参数透传要用rest参数接住并原样传给原函数。第三个是立即执行选项有些场景希望在第一次触发时立即执行而不是等待延迟。function debounce(fn, delay, immediate false) { let timer null; return function (...args) { const callNow immediate !timer; if (timer) clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) fn.apply(this, args); }, delay); if (callNow) fn.apply(this, args); }; }节流的实现有两种主流思路。时间戳版简单直接记录上次执行的时间两次调用间隔超过阈值才执行定时器版则在定时器为空时才重新设置定时器。两者各有优劣时间戳版第一次触发会立即执行但停止触发后不会再执行定时器版停止触发后还会执行最后一次。2020年之后很流行直接说“leading trailing”但在2017年能写出这两个版本的对比已经算很扎实了。3.3 Promise.all 与手动实现异步考察的常青树手写Promise.all是2017年笔试中很有区分度的一道题。ES6已经普及大家都会用Promise.all处理并发请求但真正要求手写一个时很多人会漏掉关键细节。先看我用在旁边写了注释的版本function promiseAll(promises) { return new Promise((resolve, reject) { const results []; let completed 0; const length promises.length; if (length 0) { resolve(results); return; } promises.forEach((promise, index) { Promise.resolve(promise).then((value) { results[index] value; completed 1; if (completed length) { resolve(results); } }).catch((error) { reject(error); }); }); }); }易错点有三个。第一结果数组的顺序必须和输入顺序一致不能因为谁先完成就排在前头所以赋值时要用results[index]而不是push。第二要求传入的每个元素不一定原生支持then方法所以要用Promise.resolve(promise)包裹一层确保所有元素都能进入then流程。第三处理空数组时要直接resolve否则promise会永远pending。另外还有个细节一旦某个promise reject整个Promise.all立即 reject且不会等待其他promise。类似的实现题还有Promise.race、手动实现new运算符、实现call/apply/bind。其中手动实现bind是当年的热门考题核心是返回一个新函数同时兼容作为构造函数调用的情况。很多人在实现bind时只记得绑定this忘了处理new的情形写出来的答案是残缺的。Function.prototype.myBind function (context, ...args) { const fn this; function bound(...innerArgs) { return fn.apply(this instanceof bound ? this : context, args.concat(innerArgs)); } bound.prototype Object.create(fn.prototype); return bound; };这里的核心逻辑是通过this instanceof bound判断函数是否被new调用。如果是new调用this指向实例应该优先使用这个this否则才使用传入的context。bound.prototype指向原函数原型才能让new出来的实例正确继承原型链。这个细节在手写题里很常见但大多数人写不出来。4. 算法与逻辑题字节笔试里真正决定生死的部分4.1 高频算法题类型与典型题的完整推导字节2017年秋招笔试的算法题难度放在今天看不算最难的但比很多公司的笔试明显高一档。印象里考过的类型集中在字符串、数组、链表和简单的动态规划上。如果平时刷过LeetCode热题100和剑指Offer大部分题都能找到影子。有两道题比较有代表性。一道是“两数之和”输入一个数组和一个目标值要求返回两个下标使这两个数的和等于目标值。这道题最直观的解法是暴力双重循环时间复杂度O(n^2)在数据量小的时候可以通过。但如果题意要求时间复杂度O(n)就需要用hash表记录已经遍历过的数function twoSum(nums, target) { const map new Map(); for (let i 0; i nums.length; i) { const diff target - nums[i]; if (map.has(diff)) { return [map.get(diff), i]; } map.set(nums[i], i); } return []; }这个版本空间换时间遍历一遍就能得到结果。我在笔试时先写了暴力解然后又优化成hash版本确保即使优化失败也有一个正确答案兜底。这种“先能跑通再谈优化”的策略在笔试场景中非常实用。另一道是“无重复字符的最长子串”这是典型的滑动窗口题。思路是维护一个窗口右指针不断向右扩展遇到重复字符时左指针跳到重复字符的下一个位置。用Map记录每个字符最后一次出现的下标可以在O(n)时间内完成function lengthOfLongestSubstring(s) { const map new Map(); let max 0; let left 0; for (let i 0; i s.length; i) { const char s[i]; if (map.has(char) map.get(char) left) { left map.get(char) 1; } map.set(char, i); max Math.max(max, i - left 1); } return max; }注意map.get(char) left这个条件它保证了我们只关心当前窗口内的重复字符如果重复字符已经滑出窗口不需要调整左指针。这个细节是很多同学写错的地方。链表题里反转链表是比较经典的可以用迭代也可以用递归。迭代写法的核心是三个指针的移动pre、cur、next。有效括号配对那道题就考栈的用法左括号入栈遇到右括号就出栈并检查是否匹配如果最后栈为空说明括号配对完整。爬楼梯问题则是初级的动态规划状态转移方程是dp[i] dp[i-1] dp[i-2]和斐波那契数列本质一样。4.2 现场做题的取舍策略与手感培养笔试题不是刷得越多越好但手感很重要。所谓手感是指看到一道题后能快速判断出题的类型、大概的时间复杂度、适合用什么数据结构。手感只有通过大量刷题和总结才能建立。我当时在笔试现场有个习惯先看数据范围判断暴力解能不能过。数据范围如果只有几百暴力解通常没问题如果到了一万甚至十万就应该直接往O(n log n)或O(n)的解法方向想。这种判断力能帮你在最短时间内锁定解法的方向。还有一个策略如果某道题真的做不出来哪怕只写出核心思路和伪代码也要尽量多写一些。有些阅卷会看思路分完全空着就一分没有写一部分至少能证明你有思考过程。字节的笔试平台会自动跑测试用例但人眼阅卷的部分也不会完全忽略代码逻辑。对于时间分配我的建议是算法题平均每道不超过25分钟。超过这个时间还在纠结说明方向可能不对果断先做其他题最后再回头补。不要在一道题上耗死影响后面的节奏。4.3 一道智力题引发的思考2017年那套卷子还考了一道接近智力题的题目具体细节我记不太清了大意是有25匹马5个赛道每次最多同时让5匹马比赛最少比多少次才能找出最快的3匹马。答案是7次但解题思路比较巧妙先分5组各比一次得到每组排名再让每组第一比一次假设结果依次是A1、B1、C1、D1、E1此时冠军一定是A1而第二、第三名只可能出现在A2、A3、B1、B2、C1这5匹马里再赛一场就能决出。所以一共是5117次。这种题本身和前端技术没有直接关系但它考察的是建模能力和逻辑推导能力。遇到这类题别慌先试着把问题简化为“已经知道了哪些信息”再思考如何利用这些信息减少比赛次数。这种思维方式在算法题里同样重要比如滑动窗口就是利用已有信息减少重复计算。5. 当时的准备过程复盘与对现在的启示5.1 我的备考节奏与踩过的坑那份记录里还留着当年的备考时间线算下来大概是秋季招聘前的五六周。我当时把备考分成了三个阶段第一阶段大概两周把JavaScript的基础知识整体过一遍重点看闭包、作用域、原型链、异步这几块第二阶段大概三周集中刷算法和手写题每天不贪多但保证每一道题都真正理解第三阶段是考前一周做整套的模拟题找到笔试的节奏感。现在回头看成败得失踩过的坑还挺明显的。第一个坑是刷题只刷不总结。刚开始我每天刷很多题但同一类型的题稍微换一个外壳就反应不过来。后来改成错题本每道题记录自己的错误原因以及同类题的通用解法效率才真正上来。第二个坑是手写题练得不够。因为平时都用编辑器有自动补全和代码提示一到笔试平台变成纯手写环境很多常用API都卡壳连函数声明都有些生疏。后来我恢复了“在白纸上写代码”的练习方式把每个手写题都反复默写几遍。第三个坑是忽视了输入输出格式。在线笔试平台的OJ和平时刷LeetCode不一样LeetCode是只写函数体而笔试平台经常需要自己处理输入输出用readline读取还要注意多行输入的坑。我印象中有一次就是因为不懂平台对多组输入的处理导致某个题一个用例都没过。5.2 从笔试题反推的考察方向与团队期待复盘这套卷子的时候我意识到它考察的不仅是知识点更是一整套工程习惯。选择题考的是对语言本身的熟悉程度手写代码题考的是编码规范、边界意识、内存安全意识算法题考的是问题建模和复杂度控制。这些能力放在真实工作中对应的是“能不能写出稳定可维护的代码”“出问题时能不能快速定位”“面对复杂场景能不能抽象拆解”。字节那几年招人的一个特点是非常看重逻辑清晰和基本功扎实的人。框架可以进来再学但语言基础和算法能力不会短期速成。这也解释了为什么笔试会把JS细节和手写题放在那么重要的位置。前端技术迭代越来越快今天的主流框架三五年后可能边缘化但扎实的JavaScript功底和解决未知问题的能力过多少年都不过时。5.3 给现在准备前端校招的几点实操建议这些年我带过不少人发现有些问题反复出现如果当年备考时有前辈告诉我可能会走得更顺。第一基础知识的复习不要靠看视频一定要靠输出。看视频的时候觉得自己都会了合上电脑就一片空白。更有效的方式是找一个话题比如事件循环自己写一篇完整笔记或者给同事讲一遍。讲得清楚才是真的懂。第二手写题不能只背答案。防抖节流、深拷贝、Promise.all这些题网上答案一大把但考试的时候题目一定会换条件。我的建议是把每个题自己推导一遍搞清楚每一行代码的意图再尝试实现几个变体。比如深拷贝先支持普通对象再加循环引用再加Date再加Map/Set每一步都是能力的提升。第三算法要坚持刷但不用追求题量。刷完一道题可以用LeetCode的题解区对比别人的解法看看有没有更优的思路。我更推荐按专题刷比如这周只做滑动窗口下周只做动态规划。专题式刷题比随机刷题更容易形成解题直觉。第四笔试模拟一定要做。找一套往年的真题严格计时用和真实考试一样的平台和输入输出方式完整走一遍流程。模拟的过程中你会发现很多平时不会遇到的问题比如系统崩溃、网络波动、时间不够用提前暴露总比正式考试时暴露好。5.4 如果让我重新准备一次我会做什么顺着这份旧笔记翻到底的时候我自己也在想如果能穿越回2017年重新准备一次字节的秋招笔试我会在哪些地方做得不一样。大概会有三件事。第一件我会更早开始写博客或者笔记把每道错题的解题思路、踩坑原因完整记录下来而不是等到面试前才开始整理。公开写出来还有一个好处——社区的反馈能帮你发现自己理解里的盲区。第二件我会用更多时间去研究“为什么”。当年很多知识点是背下来的比如缓存策略、事件循环顺序后来在实际开发中踩过坑才真正理解。如果备考时就把每个结论背后的原理挖透笔试里的变形题其实也都能cover住。第三件我会提前习惯在不同的在线笔试系统上做题而不是只在自己熟悉的编辑器或LeetCode环境中练习。字节的笔试平台只是其中一种还有很多公司用自己的系统提前熟悉各种平台的交互能省掉不少临场适应的精力。备考这件事说到底不是比谁刷的题多而是比谁对自己的认知更清晰。知道题目背后的考察逻辑知道自己的短板在哪里知道如何用有限的时间补上短板这套方法论放到今天依然适用。哪怕前端面试的形式以后变成项目考察、代码review、系统设计底层的东西依然是扎实的基本功和对问题本质的理解。希望这份复盘对正在准备笔试题的同学有点帮助。
返回列表