
商汤科技2018校招Web前端开发工程师笔试第一场这个话题放到现在来看依然很有的聊。虽然那是五年前的老题了但每一年都有准备校招的同学翻出来研究原因很简单商汤作为AI公司里的技术派它的前端笔试考的东西不一定难但考得很杂、很细尤其看重基本功和手写代码能力。我当初也刷过很多AI公司旷视、依图、商汤的笔试题型发现它们的出题套路高度一致就是“基础题手写题少量算法”的组合拳。这篇文章我把这场笔试的题型结构、高频考点、典型题目和解题思路完整复盘一遍也结合我后来实际带人和面试的经验补充了一些真实的踩坑记录。不管是正在准备秋招的应届生还是工作两三年想跳槽去AI公司的前端这套内容都值得花一个下午认真过一遍。1. 笔试整体结构与考点分布1.1 这场笔试到底考什么商汤这场笔试的题量不算大但覆盖范围非常广整体来看分为三大块选择题/填空题、手写代码题、简答题。选择题和填空题集中在JavaScript基础、浏览器机制、网络协议、CSS布局这些方向上手写代码题是重头戏占比最高基本每题都需要在编辑器里独立实现一个功能简答题则考察对工程化、框架原理的理解。我根据参加过的同学回忆和网上流传的题目还原整理了一个考点分布表考点方向大致占比常见出题形式JavaScript基础闭包、原型、this、异步30%选择题、手写题浏览器与网络缓存、事件循环、渲染20%选择题、简答题CSS布局与样式15%选择题、手写题框架与工程化20%选择题、简答题算法与逻辑15%手写题看到这个分布就能发现一个规律JS基础是绝对的核心。这其实代表了2018年前后AI公司前端招聘的主流审美不考花哨的框架API而是疯狂试探你对语言本身的理解。原因也合理AI公司的前端工程师往往要做数据可视化、标注平台、内部工具这类复杂业务框架可以快速上手但语言功底决定了能不能写出稳定、高效、可维护的代码。1.2 为什么AI公司的笔试这么考很多同学会困惑商汤不是搞人工智能的吗怎么前端笔试全是JS底层、手写机制这里需要理解AI公司前端的真实工作场景。AI公司的前端跟互联网公司不一样业务往往集中在三块算法效果展示平台、数据标注工具、模型训练可视化看板。这三类系统的共同特点是交互复杂、数据量大、需要处理大量异步逻辑和高频渲染。拿数据标注工具举例一张图片上的标注框要支持拖拽、缩放、撤销还要保持canvas和DOM的状态同步这种场景下闭包、事件循环、内存管理能力直接决定功能能不能做出来。再比如训练看板需要实时拉取训练日志并用图表展示这考验网络请求管理和渲染性能优化的能力。笔试考的那些东西表面上像是“八股文”实际上每一道题都能映射到这类业务场景中。还有一个很实际的原因2018年正是前端工程化快速演进的时期Vue和React已经普及但市场上大量前端开发者停留在“会用框架”的阶段对原理一知半解。商汤这类公司需要通过笔试快速筛掉“只会写页面”的候选人考查语言基础是最高效的方式。所以这不是为了难为人而是为了过滤。2. JavaScript基础怎么考都不为过2.1 原型链与继承一道题看出功底原型链是JavaScript笔试的“钉子户”商汤这场也不例外。我当时根据同学的回忆整理出来的典型题目是这样的function Parent() { this.name parent; this.say function() { console.log(I am this.name); }; } Parent.prototype.sayName function() { console.log(this.name); }; function Child() {} Child.prototype new Parent(); const child new Child(); child.sayName(); child.say();题目多半会问上面的代码输出什么child.sayName()是否能正常执行child.say()和child.sayName()有什么区别拆解一下考点。Child.prototype new Parent()这种写法是经典的原型式继承这样Child.prototype就指向了一个Parent实例因此child实例通过原型链可以访问到sayName方法。而say是Parent构造函数内部的属性它的值是在创建Parent实例时赋值给实例自身的所以Child.prototype这个Parent实例身上有say属性child访问say时通过原型链也能拿到。但是这里有一个隐藏的坑如果代码改成Parent.prototype.say function() {...}那child.say()和child.sayName()的行为是一样的因为它们都定义在原型上。区别在于构造函数内定义的属性在每个实例上都会复制一份原型上定义的属性是所有实例共享的。笔试考这种题就是为了看你能不能准确区分“实例属性”和“原型属性”。还有一个高频变体是手写new的过程这道题我会建议每个前端都背下来因为它综合考察了原型链、this指向和构造函数的返回值处理。一个成熟的答案大致长这样function myNew(Constructor, ...args) { // 1. 创建一个新对象原型指向构造函数的prototype const obj Object.create(Constructor.prototype); // 2. 将构造函数的this指向这个新对象并执行 const result Constructor.apply(obj, args); // 3. 如果构造函数返回了引用类型则返回该结果否则返回新对象 return (result ! null (typeof result object || typeof result function)) ? result : obj; }注意第三步的返回值判断很容易被忽略。如果构造函数内显式返回了一个对象那么new出来的结果就是这个对象而不是你创建的那个实例。这个边界条件在阿里、字节、商汤的笔试里都出现过属于典型的“看着简单写全了不容易”。2.2 this指向和闭包选择题的重灾区this指向是选择题里最能拉分的点考法千变万化。商汤这场出现过一道很经典的综合题代码我整理成了这样var name window; var obj { name: obj, fn: function() { console.log(this.name); } }; var fn1 obj.fn; fn1(); obj.fn(); var fn2 obj.fn; fn2.call(obj);这道题分别输出什么答案是window、obj、obj。第一行fn1()是普通函数调用this指向全局对象第二行obj.fn()是方法调用this指向obj第三行call(obj)显式指定this。这里最容易出错的是第一问很多同学会和第二问混淆核心判断标准就是函数调用时.前面是谁this就指向谁在严格模式和箭头函数之外。不过真正有区分度的题目是闭包结合定时器的这个经典场景for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 1000); }运行结果是连续输出5个5。原因是var声明的i是函数作用域的循环结束后i已经变成5而定时器的回调函数需要等到循环结束才执行此时访问的i是同一个变量。如果改成let声明则输出0到4因为let每次循环都会创建一个新的绑定。手写这个问题的终极解法是使用闭包for (var i 0; i 5; i) { (function(j) { setTimeout(function() { console.log(j); }, 1000); })(i); }这里闭包的作用是把每次循环的i值“冻结”在函数参数j中。我在面试别人的时候经常用这道题考察候选人能准确讲出为什么用闭包、闭包到底捕获了什么的人JS基础基本是扎实的。特别要注意的是闭包捕获的是变量本身而不是变量的值这是很多人理解上的最大误区。2.3 深拷贝与数组去重高频手写题手写代码题里深拷贝和数组去重出现的频率非常高商汤这场把它们放在一起考而且明确要求不能用JSON.parse(JSON.stringify())。这其实是个提示出题人希望你理解更深层的原理。数组去重有多重写法笔试时推荐写出利用Set的一行版本同时额外写一个复杂度更优的版本// 利用Set简单高效 function unique(arr) { return [...new Set(arr)]; } // 利用object作为哈希表O(n)复杂度兼容性好 function unique(arr) { const map {}; const result []; for (let i 0; i arr.length; i) { const key arr[i]; if (!map[key]) { map[key] true; result.push(key); } } return result; }第二种写法要注意如果数组元素存在数字1和字符串1它们会被当作同一个key导致去重出错更严谨的方案是用Map但笔试时如果没特殊要求能写出思路并说明局限即可。深拷贝这道题核心考点是基本类型直接复制、引用类型递归处理、数组和对象的区分、循环引用的处理。我整理了一个能应对大部分笔试的版本function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } // 处理循环引用避免递归死循环 if (map.has(target)) { return map.get(target); } const result Array.isArray(target) ? [] : {}; map.set(target, result); for (let key in target) { if (target.hasOwnProperty(key)) { result[key] deepClone(target[key], map); } } return result; }WeakMap用来记录已经拷贝过的对象解决循环引用问题。严格来说这个版本还有改进空间比如没有处理Date、RegExp、函数等特殊类型但笔试时能写出递归、能指出循环引用问题、能说明JSON.parse(JSON.stringify())的局限函数丢失、Date变成字符串、正则变成空对象已经能拿到绝大部分分数了。不要在笔试中追求完美版本老师看的是你的思路完整度和核心边界条件的处理能力。3. 事件循环与浏览器机制答错率最高的部分3.1 事件循环输出题先画栈再下笔浏览器事件循环的题目几乎每次笔试都会出现商汤这场也不例外。我的建议是遇到这种题先在草稿纸上画出任务队列的调度过程再写下输出。一道典型的题目是这样的console.log(1); setTimeout(function() { console.log(2); new Promise(function(resolve) { console.log(3); resolve(); }).then(function() { console.log(4); }); }, 0); new Promise(function(resolve) { console.log(5); resolve(); }).then(function() { console.log(6); }); console.log(7);正确答案是1、5、7、6、2、3、4。很多人会在第三部犯错误。需要注意new Promise的构造函数是同步执行的所以console.log(3)会立即执行而.then注册的回调是微任务要等当前宏任务执行完后才进入微任务队列。setTimeout的回调是一个宏任务而Promise的then是两个微任务所以顺序是“宏任务1同步代码微任务→ 宏任务2setTimeout回调它的微任务”。其中最容易答错的点是4为什么在3后面很多同学以为resolve()后then马上执行其实then中的回调是在当前宏任务代码执行完后统一调度的。我当时复习的时候总结了一句话同步代码永远最先微任务清空后才会执行下一个宏任务宏任务内部又会嵌套新的微任务。3.2 浏览器缓存与页面渲染浏览器缓存也是选择题的常客。核心考点是强缓存和协商缓存的区别2018年时Expires和Cache-Control并存现在基本只用Cache-Control了。我整理了这道题的高频考法缓存类型关键字状态码触发条件强缓存Cache-Control / Expires200 (from memory/disk cache)未过期不发起请求协商缓存Last-Modified / ETag304缓存过期/失效需要服务端确认回答这类问题时除了把定义讲清楚还应该补充一句Cache-Control的优先级高于ExpiresETag的优先级高于Last-Modified。这些细节点是拉开分数的关键。页面渲染流程那部分商汤笔试问到过“从输入URL到页面渲染完成发生了什么”这是一个经典的综合性问题。完整的回答链条是DNS解析 → TCP连接 → HTTP请求 → 服务端响应 → 浏览器解析HTML构建DOM树 → 解析CSS构建CSSOM树 → 合成渲染树 → 布局 → 绘制 → 合成。这里要特别强调DOM树和CSSOM树是并行解析的JavaScript的加载和执行会阻塞DOM树的构建所以script标签应该放在body底部或者使用defer、async属性。4. 框架与工程化检验实战能力4.1 Vue/React那些必问的点2018年笔试时Vue 2和React 15/16是主流两道方向都有公司考。商汤的题里框架部分不算深但有一道关于Vue双向绑定原理的题几乎每年都有人讨论考点是Object.defineProperty。需要能说清楚Vue 2通过Object.defineProperty劫持数据的getter和setter在getter中收集依赖订阅者在setter中触发依赖更新发布者。一个常考点是为什么Vue 2无法检测到对象属性的新增和删除原因是Object.defineProperty只能劫持已经存在的属性所以Vue 2提供Vue.set和Vue.delete来手动处理。这道题只要能把“依赖收集”和“视图更新”的关系讲清楚面试官就基本认可了。React方向如果考到大概率是这些key的作用是什么为什么不能用index作为key虚拟DOM是什么解决了什么问题setState是同步还是异步核心就是key用于高效的diff复用index作为key在列表顺序变化或插入元素时会导致组件状态错乱虚拟DOM的核心价值是让跨平台渲染成为可能并通过diff减少真实DOM操作。这里我建议也了解一下setState在React 18里自动批处理的变化虽然笔试考的是老版本但你能主动提到新特性会显得一直在跟进技术演进。4.2 模块化与构建工具从使用到原理工程化相关题目更多出现在简答题或者选择题里考法比较固定。CommonJS和ES Module的区别是一个极高频考点。简单来说require是运行时加载、同步加载import是编译时加载、静态分析基础支持tree-shaking。另一个常考点是webpack中loader和plugin的区别我当时的记忆方法是loader是“翻译官”负责把文件从一种语言/格式转换成另一种plugin是“调味料”负责在打包的各个生命周期里做额外的事情比如压缩、注入环境变量、拆分代码。笔试中如果遇到“如何优化webpack打包速度”这类问题可以从这几个角度答thread-loader做多线程打包、cache-loader做持久化缓存、DllPlugin预编译公共依赖库、合理配置resolve.modules减少搜索范围、使用mini-css-extract-plugin拆分CSS。能答出两三个点并说明它们的作用原理这道题就过关了。5. CSS与页面布局容易被忽视的送分题5.1 水平垂直居中五种姿势CSS布局题在整个前端笔试中属于“简单却不简单”的板块。说简单是因为方法多且固定说不简单是因为很多人只会用flex问“还有没有别的方法”就卡住了。商汤笔试中有一道题是“实现一个元素在容器中水平垂直居中至少写出两种方法”。最常见的三种答案分别是/* 方法一flex */ .parent { display: flex; justify-content: center; align-items: center; } /* 方法二绝对定位 transform */ .parent { position: relative; } .child { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } /* 方法三absolute margin auto */ .parent { position: relative; } .child { position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: auto; }两种方法都各有适用场景。transform方案适合子元素尺寸未知的情况但需要浏览器支持transformmargin auto方案需要子元素有明确的宽高或内容是块级元素。面试时如果能够补充一句“如果容器高度由内容撑开flex的align-items会失效需要显式设置高度”会显得很有实战经验。5.2 BFC布局题的隐藏主角BFC块级格式化上下文是CSS部分最有区分度的考点。常见问法是什么是BFC如何创建解决了什么问题我当时回答的思路是这样BFC可以理解成一个独立的渲染容器容器内部的元素布局不会影响外部外部也不会影响内部。创建BFC的常见方式有overflow: hidden/auto、display: flow-root、position: absolute/fixed、display: inline-block、float等。BFC能解决的主要问题包括清除浮动带来的父元素高度塌陷、避免margin垂直方向重叠、阻止元素被浮动元素覆盖。笔试考BFC时不要只背概念最好能配合一个例子说明。比如经典的“父元素高度塌陷”问题子元素全部浮动后父元素没有撑开解决方法是给父元素设置overflow: hidden或display: flow-root。能现场画一个简图并写代码演示这道题基本就稳了。这里也提示一下现在display: flow-root已经很好用了创建BFC的标准方式应该优先考虑它而不是依赖有副作用的overflow。6. 算法与逻辑题前端笔试的分水岭6.1 字符串与数组操作类题目AI公司的笔试里算法题比重比普通互联网公司略高但难度基本在leetcode easy到medium之间。商汤这场出现过几道比较典型的题我挑一道有代表性的说说思路实现一个函数判断一个字符串是否为合法的括号序列。比如输入()[]{}返回true输入(]返回false。解题思路是使用栈function isValid(str) { const stack []; const map { ): (, ]: [, }: { }; for (let i 0; i str.length; i) { const char str[i]; if (char ( || char [ || char {) { stack.push(char); } else { if (stack.pop() ! map[char]) { return false; } } } return stack.length 0; }这道题考察的核心是“栈”这个数据结构的理解和应用。如果不给语言限制前端笔试中很常见的题目还包括数组扁平化、字符串去重、找最大连续子串等。这些题目难度不高但能看出候选人的编码习惯和边界处理能力。我建议遇到这类题先花30秒想清楚输入输出和边界条件空字符串、全匹配、单括号再开始写代码能有效减少低级错误。6.2 算法题的答题节奏算法题忌讳上手就写尤其是笔试时间有限的时候方法不对会让你卡在一个细节上浪费大量时间。我给自己定的节奏是“三步走”第一步读题并划出关键条件时间限制、空间限制、输入规模第二步先用暴力法想清楚逻辑再考虑优化第三步写出代码并在脑中跑几个测试用例。举个典型的优化题例子两数之和。暴力法是双重循环O(n²)优化是使用哈希表把时间复杂度降到O(n)function twoSum(nums, target) { const map new Map(); for (let i 0; i nums.length; i) { const complement target - nums[i]; if (map.has(complement)) { return [map.get(complement), i]; } map.set(nums[i], i); } return []; }笔试中如果能写出优化版并主动解释“用空间换时间”的思路会比只写暴力解的同学拿到明显更高的分数。在商汤这类AI公司算法能力代表的可迁移的逻辑思维能力他们宁可要一个算法基础好但框架经验少的人也不想要只会写业务逻辑的人。7. 复盘后的避坑心得与备考建议7.1 笔试中最容易丢分的地方复盘这场笔试也复盘了周围很多同学的答题过程我发现丢分点其实集中在几件“小事”上。第一是手写代码不处理边界条件。很多同学写数组去重主逻辑写完了没考虑空数组写深拷贝主递归写完了没考虑null写防抖节流没考虑初始调用。这些边界条件在笔试中占的分值不小甚至会被单独标注为考察点。我的建议是写完之后自己用实际输入在脑中跑一遍空值、单元素、极端值都要过一遍。第二是变量命名和代码注释。笔试不是只给机器看更给面试官看。变量名用a、b、temp不会减分但也不会加分但如果函数名和变量名能清晰表达含义并在关键逻辑处加一行注释说明思路阅卷人会更容易抓到你的得分点。尤其是手写题面试官通常要在很短时间内批改大量试卷清爽的代码天然带有好感度。第三是只会在浏览器里跑代码不考虑Node环境。笔试现场通常要求提交纯JS代码有些同学的代码里混用了window、alert、document在浏览器里能跑在Node或评测系统里直接报错。平时练习时就应该在两个环境里都试一下尤其是那些涉及模块导出、require/import的题目。7.2 针对这类笔试的备考策略如果你正在准备前端校招或跳槽去AI公司怎么复习才能在短期内拿到这套题的合格分我整理了三条我自己验证过有效的策略。第一做知识树而不是刷题海。先把考点列出树状结构JS基础、浏览器、CSS、框架、算法、工程化六大分支每个分支下再细化到具体知识点。逐个节点检查自己“能说出来”和“能写出来”是两个层面笔试考的是后者所以每个知识点都要用代码验证过。第二手写代码要像背单词一样每天练。闭包应用、深拷贝、防抖节流、数组扁平化、事件循环输出题这五类题我建议每天花30分钟各写一遍连续两周效果非常明显。这些题出现频率高而且思路相通练熟之后能形成肌肉记忆考场上紧张也不会完全写不出来。第三用真题做模拟考。找一套完整的笔试题目限时90分钟屏蔽所有干扰模拟真实考试的节奏。做完之后对照答案复盘重点看是“不会”还是“会但没时间写”。如果是后者说明时间分配有问题选择题耗费太久导致后面的手写题没时间建议先扫一遍全部题目把有把握的题放在前面做。另外要特别提醒的是笔试中如果遇到不会的题一定不要留白。手写题即使只写出大体思路或部分代码也能拿到步骤分。我在批改笔试的时候最怕的不是候选人写得不对而是答得一片空白因为你根本找不到依据给他分。哪怕只写一个关键函数的签名或者写两行伪代码都比空着强。7.3 跨年份真题还能不能刷很多人会问2018年的题放到现在还有参考价值吗我的回答是考点层面非常有价值但需要注意几个变化。框架部分变化最大Vue 2的Object.defineProperty已经被Vue 3的Proxy取代key和diff的原理也发生了演进。答Vue原理相关题目时如果还在讲老版本的实现面试官会觉得你的技术栈陈旧。建议把Vue 2和Vue 3的响应式原理都学一遍能说清楚两者的差异和各自的局限性这反而是加分的亮点。CSS部分变化不大Flex和Grid依然是主流注意新增的aspect-ratio、gap等属性的使用即可。JS基础部分基本没有变化闭包、原型链、事件循环这些语言层面的东西不会过时反而因为面试官自己也在用新框架更依赖这些基础题来区分候选人的真实水平。所以我的建议是这套题要刷但不要背答案而是用“为什么”的视角去思考每个考点背后的原理然后对照当前主流技术栈更新你的知识库。这样你掌握的不是某一年的真题答案而是一套能够应对各种前端笔试的底层能力。打个小广告式的建议如果你是首次接触这类笔试建议从JS基础的手写题开始练尤其是防抖节流、深拷贝、数组去重这三件套覆盖面广、难度适中、效果立竿见影。我当年就是先把这三道题练到闭着眼能写出来再去攻克事件循环和算法题整体的备考效率高了不少。