☰
前端笔试高频考点全解析:数组去重、深拷贝与事件循环
2026/10/31 2:41:47 网站建设 项目流程

1. 整体思路拆解:秋招前端笔试到底在考什么

搜狗2019秋招前端工程师这道题,放到今天来看依然很有代表性。我当年参加过这批笔试,说实话,题目难度不算变态,但它非常精准地踩中了前端基础能力的高频考点:数组去重、字符串处理、对象深拷贝、异步顺序、事件循环。说白了,搜狐、搜狗这类老牌互联网公司,前端笔试不跟你玩偏题怪题,他们更想通过一套题快速筛出“基础扎实、代码规范、能上生产环境干活”的人。

很多同学复习秋招笔试,特别喜欢钻LeetCode hard题,其实这是个大误区。我陪过不少学弟学妹模拟笔试,发现真正的拦路虎往往是最基础的几类题:第一,数组和字符串的内置方法用不熟;第二,手写代码时边界条件考虑不全;第三,Promise和setTimeout混在一起时输出的顺序总是理不清;第四,代码风格随意,变量命名不讲道理。这套编程题合集刚好把这些点全部覆盖到了,非常适合用来做一次系统性的查漏补缺。

我在梳理这套题的时候,会先把每道题背后的考点拆出来,再给出可直接运行的代码,然后重点讲清楚“为什么这么写”以及“哪些边界条件容易翻车”。这不是为了让你背答案,而是希望你通过这一套题,建立一套自己的笔试解题框架。直接开始拆题。

2. 核心高频题型拆解:数组、字符串与对象处理

2.1 数组去重的五种解法与复杂度分析

数组去重是搜狗这套题里最经典的送分题,但送分题也有高下之分。很多同学一上来就写两层循环,这虽然在笔试中能拿基础分,但在面试官眼里只能算及格。我建议你至少掌握五种写法,并且能说清楚每种的时间复杂度和适用场景。

第一种:利用Set去重。这是ES6之后最推荐的方式,代码简洁,时间复杂度O(n)。但要注意一个坑——Set的严格相等机制无法区分对象,比如两个内容相同的对象在Set里会被当作两个不同的元素,所以对于纯基本类型数组,Set是首选。

第二种:利用filter加indexOf。这种写法能保持原始数组中第一次出现的位置,也很直观。但它的问题在于indexOf每次都要从头遍历,时间复杂度是O(n²),数据量大的时候明显慢。

第三种:利用reduce加includes。它把去重逻辑收敛到一个累加器里,写出来很函数式,缺点是同样存在O(n²)的性能隐患。

第四种:利用Map记录标识。这适合需要去重对象数组的场景,你可以用对象的某个唯一字段(如id)作为Map的key来去重。这一招在实际项目里非常实用,比Set更灵活。

第五种:sort排序后相邻比较去重。先排序再去重,适合纯数字或字符串数组,优点是额外空间占用小,缺点是会改变元素原有的相对顺序。

笔试的时候如果没有明确要求,我一般建议优先写Set版本,然后备注一句“如果需要去重对象数组,可以改用Map”,这样既展示了你对ES6的熟悉,又体现对工程场景的思考。

2.2 字符串翻转:从API调用到双指针实现

字符串翻转看起来简单,但搜狗这道题其实在考察你对API底层原理的理解。最简单的做法是split、reverse、join三连,一行搞定。但这个答案太“标准”了,如果你只写出这个,基本拿不到亮点分。

真正能拉开差距的写法是双指针原地交换。用两个指针分别指向字符串数组的头和尾,然后不断交换两端元素并向中间移动,直到两个指针相遇。这样做的好处是不用额外创建新数组,空间复杂度从O(n)降为O(1)(如果允许原地操作的话)。

但这里有个细节新手经常忽略:JS里的字符串是原始类型,不可变。所以你直接对字符串做s[len - 1] = s[i]是无效的,必须先把字符串转换成数组,操作完再join回来。我在笔试现场就见过有人在这里卡住,写了个字符串版本的双指针,怎么调都不对,最后才发现连字符串能不能改都没搞清楚。

另外还要注意中英文字符混排的情况。如果字符串里包含表情符号或生僻字,split('')会把某些码点拆成两个字符,翻转后就乱码了。更稳妥的做法是使用Array.from(str)或展开运算符[...str],它会按码点正确分割字符。这个细节在常规答案里几乎不会提到,但实际生产环境特别容易踩。

2.3 深拷贝:不止是JSON.parse(JSON.stringify())

对象深拷贝几乎是搜狗前端笔试的“钉子户”题目。大部分同学会写JSON.parse(JSON.stringify(obj)),但这个问题至少有四个坑:函数和undefined会被直接丢弃;Date对象会被转成字符串而不是保留Date类型;RegExp、Map、Set等特殊对象会变成普通对象或直接丢数据;循环引用会直接抛错。

真正的深拷贝应该用递归加WeakMap来手动实现。核心思路是:遍历源对象的每个键,如果值还是对象就递归处理,同时用WeakMap记录已经复制过的对象,解决循环引用问题;对数组、Date、RegExp、Map、Set各自做独立的复制分支;对symbol做特殊处理,因为Object.keys不会返回symbol键。

这道题的进阶考点是“性能与安全”。如果对象层级特别深,递归可能爆栈,可以考虑用栈改写成迭代版本;如果用for...in遍历,需要注意它会连原型链上的可枚举属性一起拷贝,通常要配合hasOwnProperty来过滤。我面过一些候选人,深拷贝能写出来,但一问到“你如何区分数组和对象”“Symbol属性要不要拷贝”就答不上来。这些细节才是面试官真正想看的深浅。

2.4 模板字符串渲染:一道综合性大题

搜狗这套题里有一道模板字符串渲染的题,它比其他题更“综合”,非常像实际业务里的低代码配置场景。题目大致是给你一个模板字符串和一组变量,需要把模板里的占位符替换成真实的变量值。

这道题如果允许用正则,直接replace加分组搞定。但搜狗这道题比较狠的是要求不能用正则,只能手写解析。那你就得自己写一个状态机:遍历字符串,遇到{就记录开始位置,遇到}就截取中间的变量名,然后去对象里取值并替换。

写这个解析器的关键点有三个:第一,变量名里可能包含点号,比如data.user.name,你要能处理嵌套属性的读取;第二,占位符可能出现转义,比如前面有反斜杠时不应该替换;第三,如果变量不存在,你要决定是保留原样还是替换成空字符串,这要跟面试官确认需求。

整体来说,这道题完美模拟了一个简化版的模板引擎,能帮你理解Vue插值表达式和mustache模板的实现原理,建议好好写一遍运行通过,收获会非常大。

3. 异步编程与事件循环:搜狗必考的JS核心机制

3.1 Promise、async和setTimeout的顺序输出题

搜狗笔试里几乎必出这样一道题:给你一段混合了setTimeout、Promise实例、async函数和普通console.log的代码,问控制台输出的顺序。这题看似简单,实则是全网答错率最高的JS基础题之一,因为它考察的是你对事件循环机制的全链路理解。

我的解题习惯分四步走。第一步,先扫一遍代码,把所有普通同步任务标记出来,它们在当前宏任务里按顺序立刻执行。第二步,找出所有Promise的then和catch回调、async函数中await后面的部分,这些都属于微任务,会在当前宏任务结束前、下一个宏任务开始前按入队顺序执行。第三步,找出所有setTimeout、setInterval、I/O回调,它们属于宏任务,至少等当前宏任务和全部微任务都清空后才执行。第四步,也是很多人忽略的,await关键字会先同步执行右侧表达式,再把后面的代码作为微任务继续执行;async函数内部没有await的部分会完全同步跑完。

举个具体例子,代码里有setTimeout(0)、Promise.resolve().then(fn1)、async function里await后面跟了console.log(fn2)。那执行顺序一定是:先跑完所有同步代码,然后微任务队列里fn2大概率排在fn1之后(取决于await的位置和你调用的先后),最后才轮到setTimeout里的回调。

这道题我最想提醒的一点是:不要背答案。每次做这类题,都用我说的“四步拆解法”从头走一遍逻辑,练多了之后,你在考场上甚至不用写代码,看一遍就能排出顺序。如果你能顺手在代码里跑一遍验证答案,笔试时就更有把握。

3.2 手写Promise.all:一场对细节的硬核考验

除了顺序输出题,搜狗还喜欢让你手写Promise.all的实现。这道题考察的不是API记不记得,而是两个关键细节:并发执行与聚合同步返回。

Promise.all接收一个可迭代对象,返回一个Promise,当所有传入的Promise都成功时resolve一个数组,数组顺序和传入顺序一致;任何一个Promise失败就立刻reject。写法的核心是维护一个results数组和计数器,每个Promise都注册then,成功时把结果存到对应下标,计数器加一,当计数器等于总长度时统一resolve。

但实现里至少有三个容易出错的点。第一,传入的数组可能包含非Promise值,需要先用Promise.resolve包一层;第二,要处理空数组的情况,此时应该直接resolve空数组;第三,假如某个Promise先完成,它对应下标的回调要先执行,但结果数组要放在正确的位置,不能因为先完成就把后面的值排到前面。很多候选人这里会写错,结果顺序全乱。

如果面试官让你进阶实现Promise.allSettled,那就意味着你要忽略失败,等待所有Promise都结束,最后返回每个Promise的结果和状态。理解了all的实现,allSettled也就多一个失败分支的事。建议你在本地把两个版本都写透,这是一种性价比极高的复习方式。

3.3 防抖与节流:笔试里最容易“一看就会、一写就错”的题

防抖和节流几乎不会缺席搜狗这类公司的前端笔试。题目往往给你一段输入框监听代码,要求你给请求接口加上防抖,或者给你一个滚动事件要节流。看似简单,但真正能一次性写对的人不多。

防抖的核心是“在事件停止触发后的delay毫秒后才执行一次”。实现原理很简单:每次触发事件时先clearTimeout上次的定时器,再setTimeout一个新的。关键点在于确保this指向和event对象能正确传递给回调,所以必须用function普通函数获取arguments。

节流的实现我分两种:时间戳版和定时器版。时间戳版的特点是第一次触发会立刻执行,但停止触发后不会再有补救执行;定时器版的特点是第一次触发会等delay毫秒后才执行,但停止触发后还会再补执行一次。两者都有各自的应用场景,而且回答时如果能主动说明“我选择定时器版是因为它能让最后一次操作也被处理”会给面试官留下好印象。

另外,很多笔试环境要求你手写一个带cancel方法的debounce,用来取消防抖等待。这个需求在生产中其实非常常见,比如用户点击提交后要立即取消后续防抖。写的时候一定要把timer这个变量保留在外部作用域,这样cancel才能访问到。

4. 实战演练:完整解析一套搜狗风格的模拟笔试题

4.1 模拟题一:数组乱序

题目:实现一个函数,将数组顺序随机打乱。

先说为什么不能直接用Math.random()减0.5然后sort。原因很简单:它不满足真正意义上的均匀分布,而且sort在V8引擎里对长度小于等于10的数组使用的是插入排序,大于10使用快速排序,不同元素移动的概率分布是严重偏斜的。如果面试官追问“怎么把乱序做得更均匀”,答不上来会减分。

推荐的方案是Fisher-Yates洗牌算法。从数组最后一个元素开始,随机选一个比它小的下标交换,然后往前移。每经过一次循环,已经处理过的位置就不会再被交换,这样就保证每个排列出现的概率是1/n!,真正均匀。

实现时的注意事项:一定要从后往前遍历,否则原理会乱;随机下标要用Math.floor(Math.random() * (i + 1)),我见过有人写成Math.random() * (i + 1)没取整,结果访问了下标的小数位,返回undefined;此外,如果原数组不允许被修改,记得先slice一份。

这道题还有一个加分答法:在不修改原数组的前提下返回新数组。你可以先把原数组复制一份,再对新数组做洗牌。虽然原理一样,但这个细节能体现你写代码的工程素养。

4.2 模拟题二:大数相加

题目:输入两个字符串形式的正整数,返回它们相加后的字符串结果,不能直接用BigInt。

很多前端岗看到这题第一反应是转Number相加,但Number能安全表示的最大整数只有2的53次方减1,超过就会丢失精度。所以正确做法是模仿小学数学的竖式加法:从低位往高位逐位相加,用一个变量记录进位。

实现时先让两个字符串按末位对齐,从字符数组的末尾往前逐个取出数字字符转成数字,相加后加上进位carry,结果模10放在当前位,商记录为新的进位。两个字符串都遍历完后,如果最后的carry不为0,要记得在前面补一个1。最后的输出记得反转数组再join。

我当时做这题踩过一个坑:直接把两个字符串转成数组后,用reverse方法翻转再遍历,固然可以,但reverse会改变原数组,后续如果再对齐长度就麻烦了。现在更推荐直接通过下标从后往前处理,避免多余操作。

还有一个细节是传入的字符串可能是"000123"这种前导零。笔试时最好先做一次去前导零处理,否则结果会出现多余的前导零,与标准答案不一致。

4.3 模拟题三:二叉树的层序遍历(按层输出)

搜狗前端笔试偶尔会混入一两道基础算法题,层序遍历就是典型代表。它考的不是JS API,而是数据结构基本功和队列的使用。

实现层序遍历用BFS,维护一个队列,初始时把根节点入队。每一轮先取出当前队列的长度size,这个size就是当前层的节点个数。然后循环size次,逐个出队节点,记录值,并把它的左右子节点入队。这轮结束,就把当前层的结果push进result数组。

处理时要重点注意的是:一定要先记录size再for循环,不能直接在循环里用queue.length,因为循环过程中新入队的子节点会不断改变长度,最终把下一层的节点也混进来,导致层级错乱。这个坑我在带实习生写代码时经常看到。

另外,如果二叉树是空树,应该返回空数组而不是[null],这个边界条件在书写时容易忽略。写完后建议用一棵三层满二叉树手动走一遍,就能确认输出顺序是否正确。

4.4 模拟题四:两数之和的双指针优化

两数之和的朴素解法是两层循环,时间复杂度O(n²)。搜狗这类题目如果你想拿高分,至少要写成哈希表版本,时间复杂度O(n)。但哈希表版本有一定空间消耗,如果面试官要求不能使用额外空间,那就得用“排序+双指针”的方案。

双指针方案的思路是:先对数组排序,然后用left和right分别指向头尾,计算当前和。如果和等于target,记录结果,移动双指针;如果和小于target,left右移;如果大于target,right左移。这个方案的复杂度是排序的O(n log n)加双指针的O(n),总体比哈希表更省空间。

这里要特别提醒:排序会改变原数组的顺序,如果题目要求返回原数组下标,排序后下标就乱了。通常的解法是保存一个带原下标的对象数组再排序。这类细节面试官非常喜欢追问,建议提前准备好标准的双下标方案。

4.5 模拟题五:实现一个简单的事件总线

搜狗面试里有一道“手写一个EventEmitter”题,它在业务开发中对应的就是事件发布订阅模式。前端老项目里经常用它来做组件间通信,虽然现在Vue和React各有状态管理,但事件总线仍是基础能力。

核心结构很简单:一个对象作为事件表,key是事件名,value是回调函数数组。on方法负责往对应事件名的数组里push回调;once方法需要包装一层,使回调执行一次后自动off;emit方法负责遍历回调数组逐个调用;off方法负责从数组里把指定的回调过滤掉。

写这个题最容易忽略的有两点:第一,emit的时候,回调执行过程中如果又被注册了同类事件,绝对不能影响当前这次遍历——所以要提前把回调数组复制一份再遍历;第二,off移除回调时,如果传入的callback是once包装过的,原callback会因为不是同一个引用而相等判断失败。解决思路是在包装函数上挂一个属性指向原回调。

写完EventEmitter之后,建议你顺手想一下它和原生EventTarget的区别,以及它和Redux的状态管理有什么差异。能把这个捋清楚,面试官会觉得你不是在背API,而是真的理解事件驱动的代码架构。

5. 常见问题与考场避坑日记

5.1 考场上最容易翻车的几个瞬间

我结合自己刷题和面试候选人的经验,总结出前端笔试里最常出现的错误类型,做成一张清单放在这里,考前过一遍会非常有用。

第一,API记忆错误。很多同学在写完代码后运行报错,查了半天发现是splice和slice用反了,把会改变原数组的方法当成不改变的用。这类问题最好的规避方式是平时就注意总结,推荐自己整理一份“改变原数组方法”和“不改变原数组方法”的对照表。

第二,边界值处理不当。数组为空、字符串长度为1、数字为0、嵌套对象为null,这些看似不起眼的场景,恰恰是扣分最狠的地方。每次写完代码,我都会逼自己至少检查三个边界:空输入、单元素输入、极端输入(比如大数相加里的999+1)。

第三,循环里使用delete或splice遍历数组。这在去重、删除元素类题目里非常常见。删除元素会让数组长度变化,数组元素会前移,索引就错位了。解决方案有几种:反向遍历、过滤后生成新数组、或者在下标上做适当调整。

第四,把null和undefined混为一谈。判断一个对象是否存在时,很多人习惯写if (obj),但实际上空字符串、数字0、NaN都会被这个判断排除掉。在做字符串或数字处理题时,请务必想清楚到底要不要排除这些“假值”。

5.2 如何高效复习这套题:三天复现计划

如果你现在离笔试还有三到五天,我给你推荐一个非常高效的三天复现计划。第一天,重点攻克手写深拷贝、防抖节流、模板渲染这三道综合题,每道题写完之后手动构造测试用例,覆盖边界情况。第二天,集中刷数组和字符串相关题目,不只是会写一种思路,而是要能随时说出两种以上的解法并分析时间和空间复杂度。第三天,把异步顺序题和Promise.all这类题全部过一遍,然后限时做一套完整的模拟题,务必模拟真实考场的书写环境,不开联想补全、不查资料。

很多人复习的一大毛病是“看懂了就行”,结果一到考场上手就生。相信我,编程笔试没有捷径,亲手敲一遍跟看十遍答案的差距,比你想的要大得多。每道题建议至少手写两遍:第一遍对照解析写出可运行代码,第二遍关掉参考答案,在10到15分钟内独立完成。

5.3 从笔试代码到面试高光:三个进阶思考

笔试通过后通常紧接着一轮技术面,面试官大概率会针对你笔试里的代码追问。所以在笔试结束后的当天,建议利用记忆还热的时候,重新审视自己写的代码,提前准备几个进阶思路。

第一个进阶思考是“如何把这段代码写得更健壮”。比如你写的是数组去重,可以想想如果数组里出现NaN和Infinity会怎么样;如果你写的是深拷贝,可以想想嵌套层数极其深、数据量极大时会不会卡死页面,需不需要用Worker。

第二个进阶思考是“这段代码在框架里怎么用”。比如你手写了模板字符串渲染,那么Vue的插值表达式跟你的实现相比有什么差别,为什么Vue要设计成响应式的而非一次性渲染;你手写了事件总线,那么Vue3为什么改成了mitt,它的设计好处是什么。这些问题一旦答上来,面试官对你的评价会非常高。

第三个进阶思考是“测试用例怎么设计”。面试官常会问你怎么证明你的深拷贝是对的。这个问题的标准答案是“构造一个包含普通对象、数组、嵌套对象、Date、Map、Set、循环引用和Symbol字段的复杂对象,然后运行你的拷贝函数,再做深度相等判断”。你能主动提出来,就说明你的测试意识比一般候选人强不少。

5.4 时间分配:笔试时90分钟怎么安排

搜狗这类公司的前端笔试时间通常在60到90分钟,题目数量在5题左右,有时候还会加几道选择题。我见过很多同学死磕第一道题,结果后面简单的大分题没时间做,非常可惜。

我的建议是拿到试卷先花2分钟快速浏览全部题目,按“熟练程度高、代码量小”优先的原则排序。第一轮先做能快速拿分的题,比如数组去重、字符串翻转,一共控制在25分钟以内。第二轮集中精力做深拷贝、模板渲染这类综合题,每题控制在15分钟上下。第三轮才处理异步顺序题和算法题。最后留10分钟检查边界条件和代码格式。

前端的代码题通常不需要你对格式扣得太细,但至少变量名不能是a、b、c这种无意义命名,函数名要能看出用途,循环和条件判断的缩进要一致。阅卷人看代码的速度比你想象得快,清晰整洁的代码往往能带来隐性的印象加分。

我个人在实际操作中的体会是——笔试说到底拼的不是谁运气好碰到了原题,而是谁在平时把底层原理和边界条件掌握得更扎实。不要迷信题海战术,更不要死记硬背答案,把搜狗这套题里的每一类解法都吃透,它能帮你应对的绝不止这一家公司的笔试。如果你能把上面这些题的多种解法和背后的复杂度、边界条件、工程场景串成体系,秋招笔试至少稳稳过一大半。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询