搜狗前端笔试精讲:大数相加与手写Promise核心解析
2026/8/29 22:21:03 网站建设 项目流程

搜狗2019秋招前端工程师编程题合集(第一场),我翻来覆去刷了很多遍。作为一个常年混迹在校招笔试题里的前端工程师,这套题给我最直观的感受是:它不像很多公司那样堆偏难怪题,而是把“搜索引擎场景下的数据处理”和“前端语言特性”揉在一起考,每一道题都能看出出题人真正想验证的能力点。无论你是准备投大厂校招,还是工作两三年想补基础,这套题都值得花时间过一遍,尤其是里面的大数相加和手写Promise,几乎就是前端笔试的“必修课”。

不少同学面对编程题有个误区,觉得前端笔试就考算法,拼命刷LeetCode,结果真上场时发现考的是手写一个防抖、解析一个URL参数,反而慌了。这篇博文我就把这套合集里最有代表性的几类题目拿出来,一道一道拆思路、写代码、讲边界,最后再聊聊我在笔试现场踩过的坑和复盘方法。内容比较多,但看下来你至少能少走一个月的弯路。

1. 搜狗第一场前端笔试题,到底在考什么

1.1 校招前端笔试的本质:用代码暴露你的思维短板

很多人把编程题等同于“算法题”,其实校招笔试的编程题考察的是三件事的叠加:基础知识是否扎实、代码风格是否规范、在有限时间内能否把思路转成可运行的程序。搜狗这套题尤其明显,它不会像ACM那样给一个纯数学背景的难题,而是把题目包装在很贴近业务的语言里,比如处理一段URL、解析一组数据、控制一段异步流程。

这种出题方式对我的启发很大。前端日常开发中,大量问题不是“这个算法不会”,而是“代码写出来能不能cover所有边界”。一个URL参数解析,看起来五分钟能写完,但等到跑测试用例才发现:“同样的key出现两次怎么办?编码后的中文要不要decode?这个参数没有值该怎么处理?”笔试真正筛选的,就是这种对完整性的把控能力。

1.2 从题型分布反推出题人的需求

搜狗本身是搜索引擎公司,前端团队每天面对的是大量文本、搜索词、页面渲染、异步请求,所以笔试题目倾向于三大方向:字符串和数据处理、JS语言机制的手写实现、场景化的性能优化。这三个方向几乎覆盖了前端笔试的主流考察面,刷完这套题,相当于把前端基础能力的关键节点都摸了一遍。

我把这套合集里出现频率较高、最具代表性的题目方向整理成了下面这个表格,方便大家快速定位哪类题必须重点准备。

题目方向典型考察点常见陷阱建议用时
字符串与数字处理大数相加、字符串变换数值精度丢失、进位遗漏15-20分钟
异步与状态管理手写Promise、事件循环输出微任务机制、then链返回25-30分钟
场景化数据处理URL参数解析、数组去重变体编码解码、同名参数、引用类型10-15分钟
性能优化手写防抖、节流、深拷贝this指向、定时器清理、循环引用15-20分钟

1.3 这套题适合哪些人刷

我的建议是,只要目标是前端方向,不管现在是大二还是已经工作,都应该拿这套题来自测一遍。没开始刷题的同学,可以先不设时间限制,把每道题当成一个“小项目”来做,先保证思路对,再追求代码能跑。已经有一定刷题量的同学,直接按笔试环境限时训练,做完之后重点看边界测试和代码简洁度。

这里有一个额外心得:这道题虽然已经是2019年的,但前端笔试的考察逻辑并没有发生太大变化。现在的大厂笔试依然在考手写Promise、实现防抖节流、处理URL参数,只是场景换了一层皮。所以这套题完全不过时,它代表的是前端岗位面试中最稳定的一类底层能力考核。

2. 四类核心考题精讲:从思路到实现

2.1 字符串处理和精度问题:大数相加

大数相加是这套题里很有代表性的一道,也是我强烈建议每个人都亲手写一遍的题目。它的业务背景非常直接:JS里Number类型能安全表示的最大整数是2的53次方减1,超过这个范围就会出现精度丢失。当后端返回一个超过安全范围内的ID或者金额时,前端不能直接参与运算,只能把数字当作字符串来处理。

解题思路就是模拟小学竖式加法:从两个字符串的末位开始逐位相加,逢十进一。注意几个关键点:两个字符串长度可能不一样;某一位相加之后可能产生进位;所有数字相加完后如果还有进位,要在结果最前面补一位。下面是完整的参考实现。

function addStrings(num1, num2) { let i = num1.length - 1; let j = num2.length - 1; let carry = 0; let result = ''; while (i >= 0 || j >= 0 || carry > 0) { const a = i >= 0 ? Number(num1[i]) : 0; const b = j >= 0 ? Number(num2[j]) : 0; const sum = a + b + carry; carry = Math.floor(sum / 10); result = (sum % 10) + result; i--; j--; } return result.replace(/^0+/, '') || '0'; }

这里我特意在最后做了一次前导零清理,因为题目如果输入"0001"和"0002",直接相加会得到"0003",很多实现没处理这种情况。如果真的在笔试中被隐藏用例卡住,大概率就是卡在这里。时间复杂度是O(n),只需要一次遍历,已经是最优解法。

我在实际调试中踩过的坑是:有同学会把Number(num1[i])换成parseInt(num1[i]),这两种写法本身没问题,但如果做字符到数字转换时忘记了字符可能不是数字,就会埋下隐患。另外,循环条件里carry > 0这个判断非常关键,不然两个数相加刚好得到整十数时,最高位的进位会丢掉。

2.2 异步和状态机:手写一个迷你Promise

手写Promise几乎是所有大厂前端笔试的必备题,搜狗这套题也不例外。这道题表面上考的是API实现,实际上考的是对Promise核心机制的理解:状态只能从pending变为fulfilled或rejected,且一旦改变就不可逆;then方法可以注册回调,并且必须返回一个新的Promise以支持链式调用;异步的resolve/reject需要等当前同步代码执行完再触发回调。

写一个能通过基础用例的迷你Promise,核心是把状态管理和回调队列管理好。我给出的参考实现如下,这个版本没有完全实现Promise/A+的每个细节,但作为笔试作答已经能覆盖大部分测试点。

class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.reason = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.state === 'pending') { this.state = 'fulfilled'; this.value = value; this.onFulfilledCallbacks.forEach(fn => fn()); } }; const reject = (reason) => { if (this.state === 'pending') { this.state = 'rejected'; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); } }; try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { const handleFulfilled = () => { try { const result = onFulfilled ? onFulfilled(this.value) : this.value; resolve(result); } catch (e) { reject(e); } }; const handleRejected = () => { try { if (onRejected) { resolve(onRejected(this.reason)); } else { reject(this.reason); } } catch (e) { reject(e); } }; if (this.state === 'fulfilled') { setTimeout(handleFulfilled, 0); } else if (this.state === 'rejected') { setTimeout(handleRejected, 0); } else { this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0)); this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0)); } }); } catch(onRejected) { return this.then(null, onRejected); } }

在笔试环境下,如果能写出这个程度,基本上可以拿到大部分分数。但有几个细节需要注意:第一,then里传入的回调如果返回的是一个Promise,规范的实现是需要等待这个Promise完成后再继续传递,这里简化成了直接resolve,严格来说不算完整;第二,setTimeout模拟异步只是权宜之计,真正规范中应该用微任务,但在笔试中这么写不会扣太多分,因为你已经展示了对异步时机的理解。

我建议大家写完这个版本后,自己跑三个测试用例:一个普通链式调用、一个reject后走catch、一个executor里抛异常。这三个用例都能通过,说明你对核心流程的理解是到位的。

2.3 场景化API设计:URL参数解析

搜狗做搜索产品,URL参数解析这种场景化题目几乎是量身定做。题目通常会要求:把一个queryString解析成对象,需要处理中文编码、同名参数、无值参数等情况。这道题的核心考点非常清晰:decodeURIComponent是否记得用、同名参数是否会合并成数组、没有等号的参数怎么取值。

参考实现如下:

function parseQuery(queryString) { const result = {}; if (!queryString) return result; const str = queryString.charAt(0) === '?' ? queryString.slice(1) : queryString; if (!str) return result; const pairs = str.split('&'); for (const pair of pairs) { if (!pair) continue; const idx = pair.indexOf('='); const key = idx >= 0 ? decodeURIComponent(pair.slice(0, idx)) : decodeURIComponent(pair); const value = idx >= 0 ? decodeURIComponent(pair.slice(idx + 1)) : ''; if (Object.prototype.hasOwnProperty.call(result, key)) { if (Array.isArray(result[key])) { result[key].push(value); } else { result[key] = [result[key], value]; } } else { result[key] = value; } } return result; }

有人可能会问:直接用split('=')不是更简单吗?确实,split('=')在处理简单场景时更省事,但一旦value本身包含等号,比如?link=https://example.com?a=1&b=2split会把链接切碎。用indexOf('=')找第一个等号,就能正确处理这种场景。这是实际开发里非常常见的坑,笔试中能想到这里,说明你写代码不是照搬模板。

另一个容易被忽略的细节是:解析出的key可能以数组形式存在,但只在第二次遇到同名参数时才转为数组。很多同学一上来就固定result[key] = [],导致只有一个参数时变成了数组,这和实际需求不符。所以我在代码里保留了hasOwnProperty的判断,这个写法在业务开发中也一样适用。

2.4 高频事件性能优化:防抖和节流的实现与选型

前端笔试考防抖节流,搜狗这套题里的考法通常是:给一个场景,比如输入框实时搜索、页面滚动加载更多,让你实现对应的性能优化函数,并解释为什么用防抖而不是节流。这两个函数看似简单,但能写对的人比例并不高,原因在于很多人没有真正理解它们的视角差异。

防抖的核心是“延迟执行,重新触发则重新计时”,适合输入框搜索这种“停止输入后才真正发起请求”的场景;节流的核心是“固定时间间隔内最多执行一次”,适合滚动加载这种不能太频繁触发但也不能一直不执行的场景。参考代码如下:

function debounce(fn, delay) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; } function throttle(fn, interval) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }

这里有一个很容易被忽略的点:setTimeout回调里的fn.apply(this, args),为什么要保留this?因为防抖节流通常用在对象方法上,如果直接调用fn(args),this会变成undefined,这在严格模式下直接报错。实际开发中,我见过太多人写防抖时掉进this陷阱,所以笔试时一定要体现出来。

节流还可以用定时器实现一个带尾调用的版本,不过笔试中给出时间戳版本已经足够,因为它的核心逻辑很干净。如果你时间充裕,可以再补一个带trailing参数的进阶版本,这样能明显拉开和普通人的差距。

3. 实操过程与边界控制:我就这么一步步调通的

3.1 本地调试环境的搭建思路

笔试不同于日常开发,没有IDE提示、没有调试器,很多代码写出来不跑一遍根本不知道哪里有问题。所以我强烈建议平时刷题时就在本地搭一个最小调试环境。不需要装任何复杂框架,只需要Node环境,写一个简单的test.js,把函数和测试用例放在同一个文件里,用node test.js直接跑。

比如写大数相加时,我会在代码下方手动加几组测试:

console.log(addStrings("123", "456")); // 579 console.log(addStrings("999", "1")); // 1000 console.log(addStrings("0", "0")); // 0 console.log(addStrings("001", "002")); // 3 console.log(addStrings("50", "50")); // 100

调试环境搭建的核心价值是:让错误快速暴露,逼着你把边界补全。我第一次写大数相加时以为写完就完了,结果测试"999" + "1"直接输出"990",就是进位没处理对。这种从失败到修正的过程,是刷题最有价值的部分。

3.2 边界用例清单:笔试前必测的10个输入

我在刷题复盘时给自己总结了一份“必测清单”,每次写完一道题都会按清单过一遍。现在分享出来,这份清单基本可以覆盖大多数字符串处理类编程题的隐藏用例。

  • 输入为空字符串或null的情况。
  • 输入长度差很大的情况,比如一个一位数、一个一万位数。
  • 结果产生最高位进位的情况,比如"999"加"1"。
  • 输入包含大量前导零的情况。
  • 数字字符和特殊字符混合的情况。
  • URL参数中value包含=号或&号的情况。
  • 同名参数连续出现多次的情况。
  • 参数没有=号,只有key的情况。
  • 参数值已经是编码状态的情况。
  • 参数顺序需要保持原始顺序的情况。

每一条都对应一种实际场景。比如URL参数中的?a=1&a=2&a=3,如果解析结果不是数组,后续业务比如多选筛选就全炸了。这些细节在笔试中往往就是区分通过与否的关键。

3.3 时间复杂度和代码规范上的隐性加分项

编程题不是“能跑就行”,尤其是大厂笔试,代码质量直接反映你的工程习惯。我习惯在写完代码后做三件事:第一,检查循环次数是否可以优化;第二,检查是否有不必要的变量和分支;第三,给核心函数加上清晰的命名和适当注释。

比如大数相加,如果我看到有人直接写成Array.from(num1).reverse()再循环,虽然也能过,但多了一次O(n)的反转和一次O(n)的数组创建。直接用下标从末位往前遍历,代码更短,内存占用更少。这种细节在面试官眼里就是加分项。

另外一个常见问题是:尽量别用递归处理深度未知的数据。比如深拷贝,用递归实现看起来简洁,但如果对象层级很深,直接栈溢出。笔试时可以先用递归拿分,但复盘时一定要补一版用栈模拟递归的迭代实现。

4. 常见丢分点与排查技巧实录

4.1 笔试现场最容易翻车的三个习惯

第一个习惯是拿到题目就写代码,不先理清边界。我见过很多同学在URL参数解析这道题上翻车,就是因为没想清楚“同名参数要合并成数组”这个需求,直接按普通键值对处理,结果隐藏用例一跑就挂。正确的做法是先用两分钟在草稿纸上写出输入输出的映射关系,把所有分支列出来再动手。

第二个习惯是过度依赖内置API。数组去重题你用new Set()当然可以,但如果题目明确要求不借助Set,或者数据量极大且要求保持顺序,直接Array.from(new Set(arr))就不合适。刷题时多想想内置API的原理,比背一百个API用法有用得多。

第三个习惯是只写函数不写验证代码。笔试平台的判题系统确实会自动跑测试用例,但你在本地练习时不写测试,等于闭着眼睛开车。手动跑几个用例,能在五分钟内找到逻辑漏洞,这个时间花得非常值。

4.2 遇到不会的题,怎么保住基本盘

笔试场上肯定会遇到没思路的题,这时候最重要的不是蒙一个答案,而是展示你现有的思考过程。很多在线笔试系统支持打印日志,你可以先写一个朴素版本、分析它的时间复杂度,再把优化思路写在注释里。面试官看到的是“这个人知道暴力解法,也知道瓶颈在哪”,这比空着不写要好太多。

以手写Promise这道题为例,如果一上来就写完整版,很多人会卡在then链返回新Promise这个逻辑上。我的建议是分步走:第一版只写状态机和resolve/reject;第二版加then回调注册;第三版再处理链式调用和异常捕获。每写一个版本都跑一下测试,确保当前步骤没有引入新的bug。

4.3 复盘方法:从一道题反推出完整知识体系

刷题最有价值的环节不是“做出来”,而是“复盘”。我每次做完一道题都会问自己三个问题:这道题考的是哪个底层知识点?我为什么一开始没想到最优解?如果把题目变一个场景,我能不能套用同样的思路?

比如大数相加,复盘时我会顺着它梳理JS精度问题的完整体系:Number的存储结构、最大安全整数、浮点数运算误差、BigInt的出现时间和使用限制。这样一道题就不再是一道题,而是一个知识簇。URL参数解析也可以延伸出URLSearchParams的兼容性、encodeURIComponentencodeURI的区别、query与hash之间的边界问题。

我当时复盘这套题时给自己列了一张知识补全表,每个题目对应了三到五个延伸知识点,然后利用一周时间把表格里不熟的部分逐个攻破。学完之后再回头看这些题,会发现它们比想象中简单,因为真正难的是知识体系的完整程度,而不是单道题的解法。

最后再说一个实用技巧:刷题时准备一个文件,专门记录自己犯过的低级错误,比如“忘记判断空输入”“用了严格相等却没考虑类型转换”“在两个长度不同的字符串上直接把下标对齐”。每次笔试前翻一遍这个文件,比临时看十篇面经都管用。这套搜狗编程题我已经刷过好几遍,每次都能从里面翻出新的收获,希望这篇拆解也能帮你把每一道题吃透。

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

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

立即咨询