☰
Web前端小实训面试题01:拆解手写代码、底层原理与工程化应对
2026/10/15 14:12:48 网站建设 项目流程

很多人以为“小实训面试题”就是别人整理好的刷题库,背一背就能应付过去。真正面过几轮、带过几次新人的老手都清楚,这类题目考察的从来不是“标准答案”,而是你在没准备、没提示、甚至被故意打断的情况下,还能不能保持清晰的思路,把手上的代码一步步写出来。实训题和普通面试题最大的区别就在这儿:它要求你动手,要求你讲清楚,要求你在真实的工作场景里能直接落地点。这份“Web前端小实训面试题01”我前后带过几届同学,也算摸出了一些门道,今天把我自己的拆解和应对思路完整记录下来。

1. 这类实训题到底在筛什么人

1.1 面试官为什么会选择“小实训”而不出大项目

先搞清楚一个前提:为什么面试现场不直接让你写一个完整的后台管理框架?因为在有限时间内,完整的业务系统根本写不完,面试官也看不过来。所谓“小实训”,本质上是把真实工作里最常遇到、最难出错的几个切面单独拎出来,做成5到20分钟能完成的小任务。比如让你实现一个轮播图、写一个防抖函数、修一个样式塌陷的bug,或者用原生JS实现一个Tab切换。这些任务看起来都很基础,但恰恰能筛出几类人:

一类是只会复制粘贴的人。平时开发全靠搜索引擎和现成组件库,真要关掉网络、不依赖脚手架,让他手写原生实现,他可能连数据绑定的基本逻辑都理不清。另一类是只有理论不会动手的人,可以滔滔不绝讲闭包、原型链、事件循环,但让他真的写一个Promise.all,手会抖。还有一类是写代码飞快但解释不清的人,代码能跑,但问他为什么这样写、有没有考虑边界情况,一句“这样写就对了”就直接暴露了缺乏全局理解和沟通能力。

实训题的好处就是能够在同一批候选人里快速拉开这几个维度的差距。你不光要交出一段能运行的代码,还要向面试官解释这一段代码的设计思路,你做了哪些取舍,哪些边界条件之前踩过坑,上线后遇到问题怎么排查。

1.2 “面试题01”里通常隐含的知识分层

我看到的“01”这类题目,一般不会一上来就考框架源码,也不会上来就考算法题。它更大概率是:

  • 第一类:HTML/CSS/JavaScript的基础语法,比如变量提升、事件冒泡、盒模型、标准流与脱离文档流的区别
  • 第二类:常见手写题,比如数组去重、深浅拷贝、防抖节流、发布订阅
  • 第三类:框架方向的基础题,比如虚拟DOM是什么、Vue和React的响应式区别、key有什么作用
  • 第四类:开放场景题,比如“给你一个接口,返回一万条数据,你怎么渲染不卡顿”“两个组件需要共享状态,你有几种方案”

这个顺序其实是有讲究的,先考你的根基,再考你的工程习惯,最后考你的架构意识。所以准备“01”的时候,不要一上来就背框架源码,先把原生基础打牢,反而是最划算的投入。

2. HTML/CSS的高频考点,先把这几个软肋补上

2.1 盒模型和box-sizing的面试现场演示

面试官最常问的HTML/CSS题之一就是盒模型。这个知识点虽然基础,但很多人一紧张就会绕进去。标准盒模型里,width不包含padding和border;而IE盒模型里,width是包含padding和border的。平时开发里,我们一般会用box-sizing: border-box把宽度计算方式统一,尤其在移动端布局里,这几乎成了标配,因为像素计算更加吃紧,加padding不撑破父级。

面试的时候不要只背这两句话,最好现场画一个div,手动给一个宽高加padding和border,然后解释最终渲染出来的盒子实际占了多少空间。再补一句:“实际工作中,我会在全局样式里设置box-sizing,但要注意,如果第三方组件库内部样式的写法是基于content-box的,全局修改可能会造成布局错乱,所以更稳妥的做法是按需设定或者用CSS变量统一管理。”这种补充既能体现你用过真实项目,也能体现你考虑过全局副作用。

2.2 BFC到底解决什么问题

BFC,块级格式化上下文,是另一个高频考点。面试官问“说说你对BFC的理解”时,常见的回答是“BFC是一个独立的渲染区域”或者“它包含浮动元素,防止高度塌陷”。但实际上,面试官更希望听到一套完整的触发条件和应用场景。

BFC的触发条件包括:根元素、浮动 (float不为none)、绝对定位 (position: absolute/fixed)、行内块 (display: inline-block)、表格单元格 (display: table-cell)、overflow 不为visible的块级容器,以及display: flow-root和 flex/grid 容器。实际工作里我遇到最多的两个场景:一是父元素包含浮动子元素,高度塌陷,给父级加 overflow hidden 或者 display flow-root 能触发 BFC,使它包裹浮动元素;二是避免两个兄弟元素边距折叠,把它们分别放进独立BFC容器里。

如果时间允许,建议现场演示一个小例子:一个盒子里面三个浮动块,然后父盒没设置高度,问你怎么让父盒包住子元素。不要急着背方案,先分析为什么会塌陷,再给两到三种修复方式,最后说明在实际项目中优先选哪一种,以及为什么。这样答下来,效果远胜于背概念。

2.3 flex布局和grid布局谁更常用

Flex和Grid是现在布局的两大主力,面试里也经常被拿来对比。我的切身体会是:一维布局用Flex,二维布局用Grid。Flex擅长处理主轴方向上的排列和换行,比如导航栏、按钮组、卡片横向排布;Grid擅长实现栅格系统、整体页面骨架、报表区域这种需要行列同时控制的场景。

但面试里光说这个不够。最好能顺手画出一个小例子:三栏布局,左右固定宽,中间自适应。用Flex怎么写,用Grid怎么写,各自的代码量大概是多少,以及哪个方案更稳。我通常推荐Grid,因为代码更简洁,但也要提到Grid兼容性在老旧浏览器上有问题,如果项目有特殊要求,会优先考虑Flex加百分比方案。这种对比式的回答,比单纯列出API更有说服力。

2.4 样式覆盖和优先级,是排查线上问题的地基

样式优先级也是个高频考点,而且很多人在项目里吃过亏。规规矩矩的说法是:内联样式 > ID选择器 > 类选择器 > 标签选择器,加上 !important 可以进一步提高权重。但真正重要的是让面试官看到你在实际场景里怎么定位样式覆盖问题。

我自己的常规排查路径是:打开DevTools找到元素,先看一下覆盖的样式来源,哪些规则被划掉了,哪些规则生效,然后往前追优先级和选择器,再看是不是工程上用了某种CSS方案(比如CSS Modules、Tailwind)导致类名冲突。如果遇到情况比较难查,我会检查父级是否设置了position或z-index,因为层叠上下文也会影响视觉而不是单纯的优先级。说这些细节,会明显让人觉得你不是死记硬背,而是真的处理过线上的诡异样式。

3. JavaScript核心题,重在理解运行机制

3.1 闭包不会被问“什么是闭包”,而是“这里为什么会泄漏”

闭包是JavaScript面试里逃不开的话题。但现在的面试官很少直接问“什么是闭包”,更常见的问法是:“我这里有一个循环,给每个按钮绑定了点击事件,为什么点每个按钮都显示同一个数字,该怎么修?”

这时候你要从闭包保存了外部函数变量的引用说起。经典的经典就是var循环里用setTimeout或监听事件,每次循环都共享同一个i,打印输出全是最后一个值。修复方式之一是改成let,因为let是块级作用域,每次循环都会创建新的词法环境;另一个方式是立即执行函数传参,把每次的i作为实参绑定到内部函数;更工程化的解法是事件委托,直接在外层容器上监听事件,用事件对象的target来判断具体是哪一项。

这里还有一个容易被忽略的加分点:闭包和内存泄漏的关系。如果闭包里长期持有一个大对象,或者你无意中把事件函数挂到了全局对象,就会导致该释放的资源无法释放。面试的时候如果能顺带提一句,在组件卸载时一定要记得解绑事件,清掉定时器,避免闭包把整个依赖链牵制住,说明你已经把知识点转化为工程经验了。

3.2 this指向问题,别靠死记硬背

this是前端面试里最容易翻车的点,因为它的变化规则太多。语法上,普通函数调用时this指向全局对象,严格模式下是undefined;对象方法里this指向调用者;构造函数里this指向新创建的实例;箭头函数没有自己的this,它的this来自定义时外层词法环境。

但面试官更愿意给你一段代码,问你实际输出。比如:

const obj = { name: 'demo', getName: function() { return this.name; } }; const func = obj.getName; console.log(func());

这段里,单独把方法拿出来调用func()时,this已经不再指向obj,在浏览器里指向window,严格模式下是undefined,所以输出会报错。如果改成箭头函数:

const obj = { name: 'demo', getName: () => { return this.name; } }; console.log(obj.getName());

看似obj调用,但箭头函数的this在定义时已经确定,指向的是外部词法环境的this,而不是obj。这个点讲清楚,再结合bind、call、apply的区别,基本就能镇住一波追问。

3.3 事件循环,一张图能撑起半小时

事件循环是JavaScript的基石,也是小实训题里经常掺在手写题后面的一问。先记住几个关键词:调用栈、任务队列、微任务、宏任务。浏览器里JS是单线程的,同步代码先执行,遇到异步操作会挂到对应的队列里。宏任务包括setTimeout、setInterval、I/O、事件回调、script整体;微任务包括Promise.then、MutationObserver、queueMicrotask。

最经典的预测输出题:

console.log('1'); setTimeout(() => console.log('2'), 0); Promise.resolve().then(() => console.log('3')); console.log('4');

输出顺序是1、4、3、2。原因是同步代码1和4先执行,微任务3在本次循环的末尾清空,宏任务2在下一轮宏任务队列里才执行。这里要记住一个细节:Promise构造函数里的代码是同步执行的,Promise.then里才是异步微任务。很多人就是栽在这个差别上。

如果面试官继续追问async/await的时机,可以补一句:await后面的代码会在Promise决议后以微任务形式继续执行,所以await之后的内容总体上会排在同步代码后面、setTimeout宏任务前面。

3.4 var / let / const 和暂时性死区

这个题目看起来简单,但也能拉开差距。var有变量提升,声明会被提升到作用域顶部,初始化不会;let和const也有提升,但存在暂时性死区,在声明之前访问变量会报ReferenceError。const声明的是绑定的不可变,并非对象内部属性不可变。

举个我常用的例子:

console.log(a); // undefined var a = 1; console.log(b); // ReferenceError let b = 2;

第一个输出的原因是var提升后默认初始化为undefined;第二个是因为let虽然提升了声明,但在词法绑定完成前访问,会触发暂时性死区。面试时再补一句:这也就是为什么我们在项目里优先用const,实在需要重新赋值才用let,var基本不用的原因,它能从编码阶段规避掉一部分奇怪的状态污染问题。

4. 框架与工程化问题的答题思路

4.1 Vue和React的响应式差异,重点讲设计取舍

现在的实训题经常二选一或都考,让你对比Vue和React的响应式差异。如果只讲“Vue是双向绑定,React是单向数据流”,基本没有加分,因为太浅。

我会从数据变更的粒度入手。Vue 3的响应式基于Proxy,可以在运行时拦截对象属性的读取和赋值,依赖收集和触发更新都发生在语言层面,所以能够做到精确到属性的更新。React则不同,它没有拦截读写的能力,而是通过状态触发整个组件render,再把新旧虚拟DOM做diff,只把真实变化更新到页面。React说“单向数据流”,指的是从状态到视图到事件再到状态的单向循环,这让数据流变得好追踪,但也意味着某些场景下不必要的重新渲染需要额外手段优化。

面试官如果追问Key的作用,我能给的答案是:Key对React和Vue的虚拟DOM diff都至关重要,它帮助算法识别哪些节点可以复用,什么顺序发生变化,避免对同一位置节点做无意义的卸载和重建。不加Key或者滥用数组下标做Key,在列表重新排序时经常出现状态错位的诡异问题。

4.2 虚拟DOM到底解决什么问题

很多人背了“虚拟DOM比真实DOM快”这个结论,但这需要小心,因为它不完全准确。虚拟DOM的核心价值是把UI描述和真实DOM操作解耦,你可以先对一份轻量级的对象做计算,集中、批量地更新真实DOM,减少不必要的重排和重绘;同时它让跨平台成为可能,同样的虚拟DOM结构可以渲染到浏览器、小程序、原生组件等多个目标端。

但真实操作里,偶尔直接手写命令式DOM更新,在极小规模场景下比虚拟DOM更快,因为渲染成本足够小。所以面试时我会把话说严谨:虚拟DOM的优势不体现在单一操作的速度,而是体现在复杂状态变更下的可控性和批处理能力,同时破坏了手写命令式操作的链路复杂度。

4.3 Webpack工程化问题:从构建到性能优化

前端实训题里常有一道开放式问题:“你们项目的构建流程是怎么优化的”。这考察的是工程能力。我的回答套路会固定一条线:

分析打包产物 -> 定位体积占比 -> 拆分chunk -> 配置CDN和缓存 -> 优化开发体验。

具体操作包括:用打包分析工具看哪些包占了大头;把第三方库单独拆分,利用长期缓存;按需加载路由组件,用动态import切片;开启多进程压缩;对图片资源做压缩和转WebP;配置alias缩短模块解析路径;开发环境开启热更新和预编译。没有固定答案,但关键在于解释每一个优化点背后的投入产出比。比如拆分chunk会让首屏更快,但会增加请求数量,在HTTP2之前可能是反向优化,所以要根据实际项目评估。

面试官真正想听的,是你有没有对线上的性能数据负责的意识和手段。

4.4 状态管理方案,别只知道全局store

现在问状态管理也越来越实际了。如果只是两三个组件共享一个小状态,直接放到公共父组件,或者用子组件回调往上传就行。如果跨了多层又不想引入太重的东西,可以用Context或依赖注入。如果业务复杂度变大、多人协作,再考虑全局状态库。

我的一个重要心得是:引入全局状态管理必须慎重。一旦你把状态放到全局store里,排查问题的路径就多了很多条,你必须给每个字段找到清晰的所有权和变更入口。所以面试中遇到这类题目,我会答“先看状态的作用域,再看更新的频率,最后看使用的复杂度,然后再选方案”,而不是一上来就全上全家桶。

5. 手写代码题,把套路练成肌肉记忆

5.1 防抖和节流,写完一定要说应用场景

手写题里最高频的就是防抖和节流。防抖表示事件触发后在某段时间内如果又触发,就重新计时,只有停止触发后才会执行。节流表示固定时间间隔内最多执行一次,无论事件触发多频繁。

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

写完后要解释为什么用了fn.apply(this, args)。因为返回的包装函数是普通函数,调用时this应该透传给原函数,否则在对象方法场景下this会丢失。再补一句防抖的首次立即执行版本,使用leading参数控制是否先执行,这是加分项。

5.2 数组去重,至少讲出三种思路

数组去重是入门级手写题,但一定要表现出“我会权衡多种方案”。第一种是利用Set的特性和展开运算符,Array.from(new Set(arr));第二种是使用filter加indexOf判断第一次出现的位置;第三种是用哈希表记录,适合需要复杂对象精确比较的场景;第四种是原地去重,不创建新数组,适合内存受限的场景。

你以为面试到这里就完了?面试官可能会接着问:如果数组里有对象,去重规则是什么,用Set和JSON.stringify有什么坑。这时你需要答:JSON.stringify要求对象的属性顺序一致才能做到一致序列化,否则相同内容因为键顺序不同就重复计了,还要考虑循环引用会抛错。这题考的就是你有没有真的在项目里处理过复杂去重。

5.3 Promise.all、race、any到底记住什么

手写Promise相关题目在实训里也经常出现。Promise.all接收一个可迭代对象,全部成功才返回所有结果的数组,任何一个reject就整体reject。Promise.race是返回最先状态改变的结果,不管成功失败。Promise.any是只要有一个成功就返回该成功结果,如果全部失败才返回AggregateError。

手写Promise.all时,关键点在于结果数组的顺序要和入参顺序一致,不能因为哪个先resolve就先用哪个顺位。我用计数器记录完成数量,每完成一个就存到对应索引,等计数达到总长度时统一resolve,这样既保证了顺序,也能处理空数组直接resolve的情况。

5.4 深拷贝,永远不要只写一层JSON

面试官经常直接说“写一个深拷贝”,很多人的反映是用JSON.parse(JSON.stringify(obj))。这个确实能应付简单场景,但也只在这个场景。JSON序列化无法处理undefined、函数、Symbol、Date、RegExp、Map、Set以及循环引用,遇到这些都会被悄然丢弃或报错。

我一贯的建议是,面试现场写出递归版本,并且补充解决循环引用,比如用WeakMap记录已经拷贝过的对象,如果再次遇到直接返回之前的结果。同时要手动处理数组和其他特殊类型。

function deepClone(obj, map = new WeakMap()) { if (typeof obj !== 'object' || obj === null) return obj; if (map.has(obj)) return map.get(obj); const target = Array.isArray(obj) ? [] : {}; map.set(obj, target); for (const key of Object.keys(obj)) { target[key] = deepClone(obj[key], map); } return target; }

这段代码仍然不是终极完整版,但已经展现了核心的处理思路。你可以跟面试官说,严格的深拷贝要考虑原型、属性描述符、Symbol键、各种内置类型,工作里通常会结合生产环境需要决定拷贝深度,而不是一味追求完美。

6. 现场答题的节奏与沟通技巧

6.1 先说结论,再讲过程,最后给方案

我发现很多人现场答题时不自觉地变成“默写模式”,想到哪写到哪,结果面试官越听越迷糊。正确的节奏是“先说结论,再拆解过程,最后给出可落地方案”。

比如问“为什么页面首屏渲染慢”。不要一上来罗列十几条可能原因,先说结论:“我认为最常见的前三名是资源体积过大、请求过多与阻塞渲染”,然后把每一项展开讲证据链,最后给出优先推荐的处理方案。这样的结构符合工程沟通习惯,面试官也可以随时切进任何一块追问。

6.2 不会的题,怎么把损失降到最低

面试现场一定会遇到不会的题。我的经验是,别僵硬地说“我不会”,也别瞎猜乱编。比较体面的应对是:“我对这个点的底层细节记不太清了,但我理解它解决的问题是什么,通常我会先去查文档验证,因为这类细节很容易被版本更新改变。”然后思考10秒,再把相关的一部分说出来,表示你的知识是有边界的,但你有定位问题的能力。

其实面试官也明白没人能全部记住。关键是掉进盲区后的处理策略,这本身就是一种职业素养。哪怕只能说出源码文件里大概的位置和排查步骤,也已经比空白强太多了。

6.3 让代码保持扩展性,多数人忽略的自检清单

最后一个小习惯:手写代码时,尽量别让代码只能应对当前这个用例。我的自检顺序是:这个函数有没有副作用,有没有考虑空值边界,会不会修改输入的原始数据,命名是否表达意图,能否方便后续加参数而不用推翻重写。前端实训题不要求你像刷LeetCode那样极致优化,它要求的是代码能在真实项目里存活下来,能被新人接手而不崩溃,能经得起产品改需求而不返工。

7. 一次模拟实训的完整复盘

我后来在带人时,经常组织一场模拟面试。就出一道小实训题:不限框架,实现一个搜索列表,要求输入框防抖搜索,接口返回后渲染列表,点击某项可以选中高亮,再次点击取消。这题目看起来不难,却能暴露出大量问题。

第一个暴露点是候选人拿到需求就直接写代码,不确认接口格式、不分页、不处理Loading和空状态,占了一堆时间。第二个暴露点是防抖函数写好后忘了取消上一次的pending请求,结果一个快速输入的场景里,前面请求比后面请求慢,返回后把新状态覆盖污染了。第三个暴露点是列表渲染后没有给每项正确设置key,删除或重新排序时高亮错位。

我自己的处理习惯是:先写一个最小可运行版本,跑通搜索和渲染;第二次迭代加上选中态和键盘上下键操作;第三次再考虑防抖请求竞态和取消机制。注意顺序问题,先核心后细节,这恰恰是面试官希望你展示的推进方式。

从那次模拟之后,我得出了一个结论:大多数“小实训面试题”其实不是在考察算法的难度,而是在模拟项目迭代的真实路径。你永远不要想着开局就写出一个完美无缺的成品,而是先跑起来,再逐步加固边界。

8. 备考和日常积累,比刷题重要得多

到了最后,我是真的想劝一句:面试题可以刷,但刷题必须建立在自己有真实代码量的基础上。前端这个行业变化很快,今天背熟的框架API,可能在下一个大版本里就被替换掉了。真正留在你手里的,是你对浏览器运行机制的理解,是你排查问题的经验,是你设计小功能时对可维护性的敏感度。

如果这个“实训面试题01”你是在入职前看到,我建议你花一周时间把里面每一道题都亲手写一遍,分类整理出自己的回答模板和代码片段。如果是在职准备跳槽,那就把它当成一次查漏补缺:哪个题让你停顿了,哪个现场写不出来,哪个解释了但对方还是不理解,直接标记出来,后面的两周重点就补哪里。

面试时最稳的状态,不是脑子里装着所有标准答案,而是无论面试官从哪个角落提问,你都能平静地拆解问题、调用自己的经验库、组织出层次分明的回答。代码会更新,框架会换代,但这一套拆解问题的能力不会过时。我自己面试别人和被人面试都经历过,那些最后通过考核的人,共同点从来不是背题背得最全,而是真的见过、修过、思考过问题。

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

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

立即咨询