フロントエンド面接の手書きコード30選:Promiseから仮想DOMまで完全網羅
JS基礎、非同期プログラミング、デザインパターン、DOM/ブラウザ、アルゴリズムの5カテゴリ30のフロントエンド手書き問題を網羅。各問題に核心的アプローチとポイント付き。
フロントエンド面接の手書きコード30選:Promiseから仮想DOMまで完全網羅
背景紹介
私は10社以上のフロントエンドポジションの面接を受けました。ByteDanceからPinduoduo、KuaishouからXiaohongshuまで、手書きコード問題は避けられない壁です。理論は得意でもコードを書く段階で慌てる人は多い——手書き問題がテストするのは記憶ではなく、コーディング能力と原理への理解の深さだからです。面接で出題された30の高頻度手書き問題を全てまとめました。JS基礎、非同期プログラミング、デザインパターン、DOM/ブラウザ、アルゴリズムの5つのカテゴリに分け、各問題に核心的なアプローチとポイントを付けています。各問題を自分でタイプすることをお勧めします——読むだけでは身につきません。
一、JS基礎(10問)
1. デバウンス(debounce)
核心アプローチ:遅延実行、遅延期間中に再度トリガーされるとタイマーをリセット。ポイント:クロージャでtimerを保存、毎回clearTimeoutしてからsetTimeout。面接でよく聞かれる:最初の呼び出しを即時実行するか?immediateパラメータを追加、trueなら最初は即時実行。thisと引数のバインドも忘れずに、applyまたは...argsを使用。
2. スロットル(throttle)
核心アプローチ:固定時間間隔内で1回のみ実行。2つの実装:タイムスタンプ版(最初は即時実行、最後は実行しない)とタイマー版(最初は即時実行しない、最後は実行する)。面接官が聞く:両方を組み合わせられるか?タイムスタンプ+タイマーで、最初も最後も実行。ByteDanceの2次面接でこの組み合わせ版を手書きするよう求められました。
3. ディープコピー(deepClone)
基本版:再帰+型判定。高度な処理:循環参照(WeakMapでキャッシュ)、特殊オブジェクト(Date、RegExp、Map、Set)、Symbolをキーとして使用、関数(参照のみコピーしない)。面接官が最も追及するのは循環参照——再帰時にコピー済みオブジェクトをWeakMapに保存、既存の場合は直接返す。
4. call/apply/bind
call:contextに一時的に関数をマウント、実行後に削除。apply:callと同様だが引数が配列。bind:新しい関数を返す、カリー化をサポート。bindのポイント:1)thisのバインド;2)事前設定引数の保存;3)newで呼び出された場合、thisはバインドされたcontextではなくインスタンスを指す;4)プロトタイプチェーンの継承。Meituanの1次面接でbindの完全実装を求められました——newの場合は必ず処理すること。
5. new演算子
4つのステップ: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)
3つの実装: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
核心:3つの状態(pending/fulfilled/rejected)、状態は不可逆。thenメソッドが新しいPromiseを返してチェーンを実現。ポイント:1)resolve/rejectはsetTimeoutで非同期実行を保証;2)thenのonFulfilled/onRejectedはマイクロタスク(queueMicrotaskを使用);3)値のパススルー——thenに引数がない場合、値は下に渡される;4)then内の例外は次のrejectへ。Alibabaの1次面接で完全なPromiseの実装を求められました——少なくともthenとcatchを実装。
12. Promise.all
核心アプローチ:全てのPromiseがfulfilledになったらfulfilled、1つでもrejectedになったらrejected。カウンターで完了数を記録、配列で順序通りに結果を保存。注意:Promiseでない入力はラップ、空配列は即resolve。フォローアップ:結果の順序をどう保証?pushではなくインデックスを使用。
13. Promise.race
核心アプローチ:最初に状態が変化したPromiseの結果を返す。実装はシンプル——全てのPromiseを反復、最初にresolve/rejectしたものを返す。面接官が聞く:Promise.anyとの違い?raceは最初に完了したもの(成功/失敗問わず)、anyは最初に成功したもの。
14. Promise並行制限
核心アプローチ:実行キューを維持、同時実行タスク数はlimit以下。1つ完了したら次を開始。実装:カウンターまたはPromise+再帰。超高頻度——ByteDance、Kuaishou、Pinduoduoで出題されました。ポイント:1)完了コールバックで再帰的に次のタスクを開始;2)全タスク完了時に全体のPromiseをresolve;3)エラー処理——1つの失敗が他のタスクに影響しない。
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はラッパー関数で包み、実行後に自動off;2)emitはイベントが存在しない場合を処理;3)引数は...argsで渡す。最もよく出題されるデザインパターン——5社で出題されました。
18. オブザーバーパターン
発行購読との違い:オブザーバーパターンではSubjectが直接Observerに通知、中間のイベントバスなし;発行購読はEventBusで疎結合。手書き:Subjectがobserverリストを維持、notifyで反復してupdateを呼び出し。面接官が聞く:Vueのリアクティブはどちら?オブザーバーパターン——Dep(Subject)がWatcher(Observer)に通知。
19. シングルトンパターン
核心:クラスのインスタンスが1つだけであることを保証。実装:1)クロージャ+静的メソッド;2)ES6 classのstaticプロパティ;3)プロキシパターン。面接でよく出る:グローバルStorageシングルトンの実装。ポイント:getInstanceメソッドで既に作成済みか確認、既存なら直接返す。Pinduoduoで聞かれた:遅延初期化と即時初期化の違い?遅延は使用時に作成、即時はクラスロード時に作成。
20. ストラテジーパターン
核心:一連のアルゴリズムを定義、それぞれを独立した戦略クラスにカプセル化、相互に置き換え可能。典型的シナリオ:フォームバリデーション——異なるルールが異なる戦略、Validatorクラスが呼び出し。手書き:戦略オブジェクト+コンテキストクラス。戦略オブジェクトの各メソッドが1つのルールに対応、コンテキストクラスが戦略名を受け取って対応メソッドを呼び出し。利点:大量のif-elseを回避、新しい戦略の追加が既存コードに影響しない。
21. プロキシパターン
核心:オブジェクトの代理を提供してアクセスを制御。一般的なプロキシタイプ:仮想プロキシ(画像遅延読み込み)、キャッシュプロキシ(計算結果のキャッシュ)、保護プロキシ(アクセス制御)。キャッシュプロキシの手書き:プロキシ関数を作成、内部でMap/オブジェクトで結果をキャッシュ、同じ引数ならキャッシュを返す。フォローアップ:ES6 Proxyを知っているか?メタプログラミングレベルのプロキシで、より強力。
四、DOM/ブラウザ(5問)
22. イベント委譲
核心アプローチ:イベントバブリングを利用、親要素でイベントをリッスン、event.targetで実際にトリガーされた子要素を判定。手書き:ulにclickリスナーを追加、target.tagNameがLIか確認。フォローアップ:LIに子要素がある場合は?closestメソッドで上方検索。Kuaishouの1次面接でイベント委譲を手書きするよう求められ、e.targetとe.currentTargetの違いも聞かれました。
23. 画像遅延読み込み
方法1:scrollイベント+getBoundingClientRectで要素がビューポート内にあるか判定。方法2:IntersectionObserver(推奨)。IntersectionObserver版の手書き:new IntersectionObserver、isIntersectingがtrueの場合にdata-srcをsrcに代入して観察解除。面接官が聞く:scroll版の最適化は?スロットル+requestAnimationFrameを追加。
24. テンプレートエンジン
核心アプローチ:テンプレート文字列の<%=xxx%>をデータに置換。正規/<%=(.*?)%>/gでマッチ、replace時にdataから値を取得。高度版:if/forなどのロジックをサポート——Functionコンストラクタでレンダリング関数を動的生成。面接官が聞く:XSS対策は?出力をHTMLエスケープ、<>&"'等の文字を変換。
25. ルーター(ハッシュルーター)
核心:hashchangeイベントをリッスン、ハッシュに基づいて対応コンポーネントをレンダリング。手書き:1)Routeクラスがpathとcomponentを保存;2)Routerクラスがroutesを管理、add/go/initメソッドを持つ;3)initでhashchangeをリッスン、routeにマッチしてコンポーネントをレンダリング。フォローアップ:historyルーターの実装は?pushState+popstateイベント、バックエンドのサポートが必要。
26. JSONP
核心:scriptタグが同一生成元ポリシーの制限を受けないことを利用。手書き:1)scriptタグを作成、srcはAPI URL+callbackパラメータ;2)windowにcallback関数をマウント;3)サーバーがcallback(data)を返す;4)フロントエンドのcallback関数がデータを受信;5)scriptとcallbackをクリーンアップ。注意:GETのみサポート、XSSリスクあり。
五、アルゴリズム(4問)
27. LRUキャッシュ
核心データ構造:Map(挿入順序を維持)または双方向リスト+HashMap。Map版が最もシンプル:get時にdeleteしてset(末尾に移動)、put時に容量超過なら最初のkeyをdelete。双方向リスト版がより古典的:get/put時にノードを先頭に移動、末尾ノードを削除。ByteDanceの3次面接で双方向リスト版を手書きするよう求められ、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. ソートアルゴリズム(クイックソート/マージソート)
クイックソート:ピボットを選択、小さいものを左に大きいものを右に、再帰。インプレースクイックソートがよく出題:両端からスキャンする2ポインタ、位置を交換。マージソート:分割統治、再帰的に半分に分割してからマージ。どちらもO(nlogn)だが、クイックソートは最悪O(n²)、マージソートは安定。面接官が聞く:マージソートを使う場面は?データ量が多く安定ソートが必要な場合。Meituanの2次面接でインプレースクイックソートを手書きするよう求められました。
実問題まとめ
上記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:まずアプローチを説明——コードが不完全でも、アプローチが正しければ得点になる。沈黙は避け——考えながら話し、思考プロセスを面接官に見せる。