面试中手写代码的20个高频题:防抖节流到Promise全收录
汇总前端面试最高频的20道手写代码题,包括防抖、节流、Promise、深拷贝、LRU缓存、柯里化、虚拟DOM等,每题含考察点与解题思路
面试中手写代码的20个高频题:防抖节流到Promise全收录
背景介绍
我前后参加了不下20场前端面试,发现不管大厂小厂,手写代码环节几乎100%会考。有些题目我面了5次被问了4次,比如防抖节流、Promise、深拷贝这些,真的是逢面必考。我一开始特别怕手写代码,因为平时都是IDE自动补全,突然让你在白板上或者在线编辑器里手写,各种拼写错误、边界条件遗漏,惨不忍睹。后来我把所有被考过的手写题整理了一遍,反复练习,到后面基本能做到看到题目就能默写出来。这篇文章收录了20道最高频的手写题,每道都标注了考察点和解题思路,希望帮你少走弯路。
面试流程复盘
手写代码环节通常在面试的中后段,时间15-30分钟不等。大厂一般用在线编辑器(如牛客、CodePad),中小厂可能让你在白板上写。我的经验是:
1. 先确认输入输出——很多题目描述比较模糊,比如"实现防抖",你得问清楚:是立即执行还是延迟执行?要不要带返回值?
2. 先写核心逻辑——不要一上来就写边界处理,先把核心功能实现,再补充边界条件。
3. 边写边说——面试官不是在看你默写代码,而是在听你思考。写每一行的时候简单解释一下你的思路。
4. 写完要自测——用几个测试用例在脑子里跑一遍,特别是边界情况:空输入、null/undefined、超大数值。
真题一:防抖(Debounce)
考察点:闭包、定时器、this绑定
解题思路:防抖的核心是"延迟执行,如果在延迟期间再次触发则重新计时"。用闭包保存timer引用,每次调用先clearTimeout再重新设置。
关键细节:1. 保存this和arguments,用apply/call绑定上下文;2. 返回闭包函数;3. 提供取消防抖的能力。
面试追问:如果需要首次立即执行怎么办?——加一个immediate参数,第一次触发时立即执行,后续触发走防抖逻辑。
真题二:节流(Throttle)
考察点:闭包、定时器、时间戳
解题思路:节流的核心是"固定时间间隔内只执行一次"。两种实现方式:时间戳版(用上次执行时间判断)和定时器版(用setTimeout控制)。时间戳版首次立即执行,定时器版首次延迟执行。
关键细节:1. 时间戳版在停止触发后不会再执行最后一次;2. 定时器版在停止触发后会执行最后一次;3. 两者结合可以实现"有头有尾"的节流。
面试追问:leading和trailing参数怎么实现?——leading控制首次是否执行,trailing控制结束是否执行最后一次。
真题三:Promise
考察点:状态机、微任务、链式调用
解题思路:Promise有三个状态:pending、fulfilled、rejected。核心是状态只能从pending变到fulfilled或rejected,且不可逆。then方法返回新的Promise实现链式调用。
关键细节:1. resolve/reject用queueMicrotask模拟微任务异步执行;2. then的回调要在状态变更后异步执行;3. 值穿透——then没有传回调时,值要传递到下一个then。
面试追问:Promise的then为什么是微任务?——为了保证执行顺序的确定性,微任务在当前宏任务结束后、下一个宏任务开始前执行。
真题四:Promise.all
考察点:并发控制、错误处理、计数器
解题思路:接收Promise数组,全部成功时按顺序返回结果数组,任一失败则立即返回该失败原因。用计数器记录完成的Promise数量,全部完成后resolve。
关键细节:1. 空数组直接resolve([]);2. 结果要按原始顺序排列,不能按完成顺序;3. 用索引而非push保证顺序。
面试追问:Promise.allSettled怎么实现?——不管成功失败都收集结果,用status字段区分。
真题五:call/apply/bind
考察点:this绑定、函数作为对象方法
解题思路:call和apply的核心是把函数作为对象的方法调用,利用"对象方法调用时this指向对象"的特性。bind返回一个新函数,预先绑定this。
关键细节:1. call传参列表,apply传参数数组;2. this为null/undefined时指向全局对象(非严格模式);3. bind返回的函数可以new,此时this指向实例而非绑定的对象。
面试追问:bind后的函数new调用时怎么处理?——判断this instanceof boundFunction,如果是new调用则忽略绑定的this。
真题六:深拷贝
考察点:递归、类型判断、循环引用
解题思路:递归遍历对象的所有属性,对每个属性值进行判断:基本类型直接复制,引用类型递归拷贝。循环引用用WeakMap记录已拷贝的对象。
关键细节:1. 类型判断用Object.prototype.toString.call;2. 数组用[]初始化,对象用{}初始化;3. 循环引用用WeakMap避免无限递归;4. 特殊对象(Date、RegExp、Map、Set)需要特殊处理。
面试追问:为什么用WeakMap而不是Map?——WeakMap的key是弱引用,不会阻止垃圾回收,避免内存泄漏。
真题七:EventEmitter
考察点:发布订阅模式、事件管理
解题思路:维护一个事件映射表{eventName: [callback1, callback2, ...]},on注册事件,emit触发事件,off移除事件,once注册一次性事件。
关键细节:1. 同一事件可以注册多个监听器;2. emit时按注册顺序执行;3. once监听器执行后自动移除;4. off不传回调则移除该事件所有监听器。
真题八:LRU缓存
考察点:哈希表+双向链表、O(1)读写
解题思路:哈希表存key到节点的映射(O(1)查找),双向链表维护访问顺序(O(1)插入删除)。get时把节点移到链表头部,put时如果超出容量则删除链表尾部节点。
关键细节:1. 哨兵节点(dummy head/tail)简化边界处理;2. 删除和移动节点时注意指针顺序;3. Map的size属性可以判断容量。
真题九:虚拟DOM
考察点:树结构、diff算法、渲染函数
解题思路:虚拟DOM本质是用JS对象描述DOM结构。包含tag、props、children三个核心属性。render方法递归创建真实DOM节点。
关键细节:1. props中的事件属性需要用addEventListener绑定;2. children可能是字符串或虚拟节点;3. 简化版diff只比较同层节点。
真题十:柯里化(Curry)
考察点:闭包、函数式编程、参数收集
解题思路:将多参数函数转换为一系列单参数函数。用闭包收集参数,当收集的参数数量等于原函数参数数量时执行,否则返回新函数继续收集。
关键细节:1. 用Function.prototype.length获取原函数参数数量;2. 每次调用可以传0到多个参数;3. 占位符支持(如_表示该位置参数待填充)。
真题十一:数组扁平化
考察点:递归、reduce、迭代
解题思路:递归版用reduce+concat,遇到数组则递归展开。迭代版用栈,逐个弹出元素,是数组则推入栈中继续处理。
关键细节:1. 控制扁平化深度depth参数;2. 空数组元素要跳过;3. 原生flat方法只扁平一层,flat(Infinity)完全扁平。
真题十二:发布订阅
考察点:设计模式、解耦
解题思路:与EventEmitter类似,但更强调发布者和订阅者的解耦。增加事件中心(EventCenter)作为中间人,发布者只管发布,订阅者只管订阅。
关键细节:1. 支持命名空间(如"user:login");2. 支持通配符订阅;3. 错误隔离——一个监听器报错不影响其他监听器执行。
真题十三:AJAX
考察点:XMLHttpRequest、Promise封装
解题思路:创建XHR对象→open设置请求→设置header→send发送→监听onreadystatechange→用Promise封装异步结果。
关键细节:1. readyState为4且status为200-299表示成功;2. GET请求参数拼在URL上;3. 超时处理用xhr.timeout+ontimeout;4. 现代方案用fetch替代。
真题十四:模板引擎
考察点:正则替换、with语句、new Function
解题思路:将模板字符串中的{{expression}}替换为对应的数据值。用正则匹配{{}},用with(data)创建数据作用域,用new Function动态生成渲染函数。
关键细节:1. 转义HTML特殊字符防XSS;2. 支持条件判断和循环(如{{if}}、{{each}});3. 编译缓存避免重复编译。
真题十五:前端路由
考察点:History API、hashchange、路由匹配
解题思路:Hash路由监听hashchange事件,History路由用pushState/replaceState+popstate事件。维护路由表,匹配当前路径到对应组件。
关键细节:1. History路由需要服务端配合(所有路径返回index.html);2. 路由参数解析(如/user/:id);3. 嵌套路由的实现。
真题十六:new操作符
考察点:原型链、构造函数、this绑定
解题思路:1. 创建空对象;2. 将对象的__proto__指向构造函数的prototype;3. 将构造函数的this绑定到新对象并执行;4. 如果构造函数返回对象则返回该对象,否则返回新对象。
关键细节:1. 构造函数显式返回对象时的处理;2. 原型链的建立过程;3. 与Object.create的区别。
真题十七:instanceof
考察点:原型链查找
解题思路:沿着对象的原型链向上查找,看是否能找到构造函数的prototype。用while循环遍历__proto__,直到null为止。
关键细节:1. 基本类型instanceof始终返回false;2. null和undefined的处理;3. Symbol.hasInstance自定义行为。
真题十八:Object.create
考察点:原型继承、属性描述符
解题思路:创建新对象,将其__proto__指向传入的原型对象。如果第二个参数存在,用Object.defineProperties添加属性。
关键细节:1. 原型为null时创建的对象没有原型;2. 第二个参数的属性描述符格式;3. 与new的区别——不执行构造函数。
真题十九:异步并发限制
考察点:Promise、并发控制、队列
解题思路:维护一个执行队列和正在执行的计数器。当正在执行数小于并发限制时,从队列取出任务执行;执行完成后计数器减1,继续取任务。
关键细节:1. 任务完成后的回调处理;2. 错误不影响其他任务;3. 支持动态添加任务。
真题二十:二叉树遍历
考察点:递归、栈、Morris遍历
解题思路:前中后序遍历的递归版最简单,面试官通常要求写非递归版。前序和中序用栈模拟递归,后序用双栈法或标记法。层序用队列BFS。
关键细节:1. 非递归后序遍历是最容易出错的,注意访问时机;2. Morris遍历O(1)空间但会修改树结构;3. 面试常考变体:判断是否BST、求最大深度、最近公共祖先。
心得建议
1. 分类练习:把20道题分成闭包类(防抖节流、柯里化)、异步类(Promise、AJAX)、数据结构类(LRU、二叉树)、工具类(深拷贝、call/apply/bind),每类集中突破。
2. 不要背代码:理解每道题的核心思路,然后自己写出来。背代码面试一紧张就容易忘,理解了思路就算忘了细节也能现场推导。
3. 注意边界条件:面试官最看重的不是你能不能写出核心逻辑,而是你能不能处理边界情况。每道题写完后,主动说"我考虑一下边界情况"。
4. 手写练习:平时在纸上或白板上写代码,不要只依赖IDE。面试时没有自动补全,拼写错误会扣分。
5. 时间分配:简单题5分钟,中等题10分钟,难题15分钟。超时要主动跟面试官沟通,不要一直卡在一道题上。
FAQ
Q:手写代码面试能用ES6+语法吗?
A:大部分公司可以,但有些面试官会要求用ES5实现。建议两种写法都准备,面试时先问清楚。
Q:写不出来怎么办?
A:不要沉默。把你的思路说出来,面试官可能会给你提示。部分完成比完全空白好得多。
Q:需要写测试用例吗?
A:不要求写完整的测试代码,但写完后用口头方式过几个测试用例,展示你的验证意识。