面試中手寫代碼的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:不要求寫完整的測試代碼,但寫完後用口頭方式過幾個測試用例,展示你的驗證意識。

#手寫代碼#前端面試#防抖节流#Promise#深拷贝#LRU緩存#柯里化#虚拟DOM