前端面试手写题30道全收录:从Promise到虚拟DOM一网打尽

前端手写题作者: 美历团队

涵盖JS基础、异步编程、设计模式、DOM/浏览器、算法5大类30道前端手写题,每题含核心思路与关键点,助你攻克手写代码关。

前端面试手写题30道全收录:从Promise到虚拟DOM一网打尽

背景介绍

我面了十几家前端岗,从字节到拼多多,从快手到小红书,手写题是绕不过去的坎。有些人八股文背得溜,但一到手写就慌了——因为手写题考的不是记忆,是编码能力和对原理的理解深度。我把面过的30道最高频手写题全整理出来了,分成JS基础、异步编程、设计模式、DOM/浏览器、算法五大类,每道题都附上核心思路和关键点。建议你每道题都自己敲一遍,光看是学不会的。

一、JS基础(10题)

1. 防抖(debounce)

核心思路:延迟执行,在延迟期间再次触发则重新计时。关键点:用闭包保存timer,每次调用clearTimeout再重新setTimeout。面试常问:要不要首次立即执行?加个immediate参数,为true时第一次立即执行。还要注意this和参数的绑定,用apply或...args。

2. 节流(throttle)

核心思路:固定时间间隔内只执行一次。两种实现:时间戳版(首次立即执行,最后不执行)和定时器版(首次不立即执行,最后会执行)。面试官会问:能不能两者结合?用时间戳+定时器,首次立即执行,最后一次也执行。我在字节二面被要求手写这个组合版。

3. 深拷贝(deepClone)

基础版:递归+判断类型。进阶要处理:循环引用(用WeakMap缓存)、特殊对象(Date、RegExp、Map、Set)、Symbol作为key、函数(直接引用不拷贝)。面试官最爱追问循环引用怎么处理——在递归时把已拷贝的对象存到WeakMap里,遇到已存在的直接返回。

4. call/apply/bind

call:在context上临时挂载函数,执行后删除。apply:类似call,但参数是数组。bind:返回新函数,支持柯里化。bind的关键点:1)绑定this;2)保存预设参数;3)new调用时this指向实例而非绑定的context;4)原型链继承。我在美团一面被要求完整实现bind,new的情况一定要处理。

5. new操作符

四个步骤:1)创建空对象;2)对象的__proto__指向构造函数的prototype;3)构造函数的this绑定到新对象并执行;4)如果构造函数返回对象则返回该对象,否则返回新对象。注意:返回值判断——只有返回的是对象才用返回值,基本类型忽略。

6. instanceof

核心思路:沿着左边对象的原型链往上找,看能不能找到右边构造函数的prototype。用while循环,leftProto = leftProto.__proto__,直到为null。面试追问:能不能判断基本类型?不能,instanceof只对引用类型有效。怎么判断基本类型?用typeof。

7. Object.create

核心思路:创建一个新对象,其__proto__指向传入的proto参数。用临时构造函数实现:function F(){},F.prototype = proto,return new F()。注意:proto必须为对象或null,否则抛TypeError。

8. 柯里化(curry)

核心思路:把多参数函数变成一系列单参数函数。用递归收集参数,当参数数量足够时执行原函数。关键点:用fn.length获取原函数参数个数,用闭包保存已收集的参数。面试常考:add(1)(2)(3)和add(1,2)(3)都要支持——参数收集用...args拼接。

9. 数组扁平化(flatten)

三种实现:1)递归reduce+concat;2)用栈模拟(迭代,性能更好);3)ES6的flat(Infinity)。面试官会追问:不用递归怎么实现?用栈:每次pop一个元素,如果是数组则展开push回去,否则放到结果里。还要注意能指定扁平化深度。

10. 数组去重

方案:1)Set(最简单);2)filter+indexOf;3)reduce+includes;4)Map。面试追问:NaN怎么去重?Set可以(NaN===NaN为false但Set认为相等),indexOf不行(NaN的indexOf是-1)。对象去重呢?用Map或JSON.stringify(有顺序问题)。

二、异步编程(6题)

11. Promise

核心:三个状态(pending/fulfilled/rejected),状态不可逆。then方法返回新Promise实现链式调用。关键点:1)resolve/reject用setTimeout保证异步执行;2)then的onFulfilled/onRejected是微任务(用queueMicrotask);3)值穿透——then不传参数时值往下传;4)then中抛异常走下一个reject。我在阿里一面被要求手写完整Promise,至少要实现then和catch。

12. Promise.all

核心思路:等所有Promise都fulfilled才fulfilled,有一个rejected就rejected。用计数器记录完成数量,用数组按顺序保存结果。注意:传入的不是Promise要先包装,空数组直接resolve。面试追问:结果顺序怎么保证?用索引而不是push。

13. Promise.race

核心思路:返回最先改变状态的Promise结果。实现很简单——遍历所有Promise,谁先resolve/reject就返回谁。面试官会问:和Promise.any的区别?race是第一个完成(不管成功失败),any是第一个成功。

14. Promise并发限制

核心思路:维护一个执行队列,同时运行的任务不超过limit个。完成一个就补一个。实现方式:用计数器或用Promise+递归。这是高频中的高频——我在字节、快手、拼多多都被考过。关键点:1)任务完成回调里要递归启动下一个;2)所有任务完成要resolve总Promise;3)错误处理——某个失败不影响其他任务。

15. async/await原理

本质是Generator+自动执行器的语法糖。Generator函数yield交出控制权,执行器调用next()恢复执行。手写核心:用递归调用gen.next(),如果done为true则resolve,否则对value(Promise)调用then再递归。面试官会问:为什么async函数返回Promise?因为执行器包装了一层Promise。

16. 红绿灯交替亮

题目:红灯3秒→绿灯2秒→黄灯1秒,循环。用async/await最优雅:while(true){ await red(); await green(); await yellow(); }。每个灯用Promise+setTimeout实现。面试追问:不用async/await怎么实现?用回调嵌套或Promise链式调用。

三、设计模式(5题)

17. 发布订阅模式(EventEmitter)

核心:on注册事件、emit触发事件、off移除事件、once注册一次性事件。用对象存储事件名到回调数组的映射。关键点:1)once用wrapper函数包装,执行后自动off;2)emit要处理没有该事件的情况;3)参数传递用...args。这是最常考的设计模式手写题,我在5家公司的面试中都遇到了。

18. 观察者模式

和发布订阅的区别:观察者模式中Subject直接通知Observer,没有中间事件总线;发布订阅有EventBus解耦。手写:Subject维护observer列表,notify时遍历调用update方法。面试官会问:Vue的响应式是哪种?是观察者模式——Dep(Subject)通知Watcher(Observer)。

19. 单例模式

核心:保证一个类只有一个实例。实现:1)闭包+静态方法;2)ES6 class的static属性;3)代理模式。面试常考:实现一个全局的Storage单例。关键点:getInstance方法判断是否已创建,已创建直接返回。我在拼多多被问:懒汉式和饿汉式的区别?懒汉式是使用时才创建,饿汉式是类加载时就创建。

20. 策略模式

核心:定义一系列算法,把它们封装成独立的策略类,可以互相替换。经典场景:表单验证——不同规则是不同策略,Validator类负责调用。手写:策略对象+环境类。策略对象的每个方法对应一种规则,环境类接收策略名调用对应方法。好处:避免大量if-else,新增策略不影响已有代码。

21. 代理模式

核心:为对象提供代理以控制访问。常见代理类型:虚拟代理(图片懒加载)、缓存代理(计算结果缓存)、保护代理(权限控制)。手写缓存代理:创建代理函数,内部用Map/对象缓存结果,相同的参数直接返回缓存。面试官会追问:ES6 Proxy了解吗?那是元编程层面的代理,更强大。

四、DOM/浏览器(5题)

22. 事件委托

核心思路:利用事件冒泡,在父元素上监听事件,通过event.target判断实际触发的子元素。手写:给ul添加click监听,判断target.tagName是否为LI。面试追问:如果LI里有子元素怎么办?用closest方法向上查找。我在快手一面被要求手写一个事件委托,还问了e.target和e.currentTarget的区别。

23. 图片懒加载

方案一:监听scroll事件+getBoundingClientRect判断元素是否在视口内。方案二:IntersectionObserver(推荐)。手写IntersectionObserver版:new IntersectionObserver,当isIntersecting为true时把data-src赋值给src并取消观察。面试官会问:scroll版怎么优化?加节流+requestAnimationFrame。

24. 模板引擎

核心思路:把模板字符串中的<%=xxx%>替换为数据。用正则/<%=(.*?)%>/g匹配,replace时从data中取值。进阶:支持if/for等逻辑——用Function构造函数动态生成渲染函数。面试官会问:XSS怎么防?对输出做HTML转义,转义<>&"'等字符。

25. 路由(hash路由)

核心:监听hashchange事件,根据hash渲染对应组件。手写:1)Route类存path和component;2)Router类管理routes,有add/go/init方法;3)init时监听hashchange,匹配route渲染组件。面试追问:history路由怎么实现?用pushState+popstate事件,需要后端配合。

26. JSONP

核心:利用script标签不受同源策略限制的特点。手写:1)创建script标签,src为接口地址+callback参数;2)在window上挂载callback函数;3)服务端返回callback(data);4)前端callback函数接收数据;5)清理script和callback。注意:只支持GET,有XSS风险。

五、算法(4题)

27. LRU缓存

核心数据结构:Map(保持插入顺序)或双向链表+HashMap。Map版最简洁:get时先delete再set(移到末尾),put时如果超容量就delete第一个key。双向链表版更经典:get/put时把节点移到头部,淘汰尾部节点。我在字节三面被要求手写双向链表版,要定义Node类和LRUCache类。

28. 数组扁平化(递归/迭代)

这个和第9题的区别是面试官可能要求指定深度。递归版:reduce+concat,每层递归depth-1。迭代版:用栈,每个元素标记当前深度。面试官会追问:如果数组里有稀疏项(empty)怎么处理?flat方法会跳过稀疏项。

29. 数组去重(多种方案)

这个和第10题的区别是面试官可能要求手写多种方案并比较。Set版:[...new Set(arr)]。filter版:arr.filter((item, index) => arr.indexOf(item) === index)。Map版:用Map存已见过的值。追问:对NaN的处理?Set认为NaN===NaN,indexOf找不到NaN。

30. 排序算法(快排/归并)

快排:选基准值,小的放左边大的放右边,递归。面试常考原地快排:双指针从两端扫描,交换位置。归并:分治,递归拆半再合并。时间复杂度都是O(nlogn),但快排最坏O(n²),归并稳定。面试官会问:什么时候用归并?数据量大且要求稳定排序时。我在美团二面被要求手写原地快排。

真题汇总

以上30题按出现频率排序:Top 10高频是防抖节流、深拷贝、call/apply/bind、Promise、Promise.all、并发限制、EventEmitter、LRU缓存、new操作符、instanceof。其中Promise相关和EventEmitter几乎每场面试必考,一定要滚瓜烂熟。

心得建议

1. 每道题必须手敲,看懂和写出来差了十万八千里。建议在LeetCode或CodePen上练习。

2. 注意边界条件,面试官最爱在这些地方挖坑:null/undefined参数、空数组、循环引用、NaN处理等。

3. 说出思路再写代码,先和面试官确认理解是否正确,避免写完发现理解偏了。

4. 写完要测试,自己构造测试用例跑一遍。面试官很看重你有没有验证意识。

5. 了解原理,不只是能写出来,还要能解释为什么这么写。比如Promise为什么用微任务,LRU为什么用双向链表。

FAQ

Q:手写题要写到什么程度?

A:核心逻辑必须完整,边界条件尽量覆盖。不用追求100%完美,但主要功能要能跑。面试官更看重思路和编码习惯。

Q:时间不够怎么取舍?

A:优先准备Top 10高频题,特别是Promise系列、防抖节流、深拷贝、EventEmitter。这些覆盖了80%的考点。

Q:手写题用什么语言?

A:前端岗用JavaScript/TypeScript。如果面试官允许用TS,类型标注会加分。

Q:写不出来怎么办?

A:先说思路,哪怕代码写不完整,思路对了也有分。千万别沉默——边想边说,让面试官看到你的思考过程。

#前端#手写题#Promise#JavaScript#设计模式#算法#面试