面接でよく出る手書きコード20選:デバウンス・スロットルからPromiseまで完全収集

コーディング面接著者: BeautyResume チーム

フロントエンド面接で最も頻出する手書きコード20問を網羅。デバウンス、スロットル、Promise、ディープコピー、LRUキャッシュ、カリー化、仮想DOMなどを収録。各問題に考察ポイントと解法アプローチ付き

面接でよく出る手書きコード20選:デバウンス・スロットルからPromiseまで完全収集

背景紹介

20回以上のフロントエンド面接に参加しましたが、企業規模を問わず、手書きコードの环节はほぼ100%出題されます。デバウンス・スロットル、Promise、ディープコピーなどは5回面接して4回聞かれるほどの頻出問題です。最初は手書きコードが本当に苦手でした。普段はIDEの自動補完に頼りきりで、ホワイトボードやオンラインエディタで書くと、スペルミスや境界条件の見落としが続出でした。その後、出題された手書き問題をすべて整理し、繰り返し練習して、問題を見たら暗記して書けるようになりました。この記事では、最も頻出する20問を収録し、各問題に考察ポイントと解法アプローチを付記しています。

面接プロセスの振り返り

手書きコードの环节は通常面接の中盤〜後半にあり、時間は15〜30分。大手はオンラインエディタ(HackerRank、CoderPadなど)を使用し、中小企業はホワイトボードで書かせることもあります。私の経験則:

1. まず入出力を確認する——多くの問題は説明が曖昧です。例えば「デバウンスを実装して」と言われても、即時実行か遅延実行か?戻り値は必要か?を確認する必要があります。

2. コアロジックを先に書く——最初から境界処理を書かず、まずコア機能を実装してから境界条件を追加。

3. 書きながら説明する——面接官は暗記コードを見ているのではなく、思考プロセスを聞いています。各行の意図を簡潔に説明しましょう。

4. 書き終わったら自己テスト——いくつかのテストケースを頭の中で実行。特に境界情况:空入力、null/undefined、極大値。

問題1:デバウンス(Debounce)

考察ポイント:クロージャ、タイマー、thisバインディング

解法アプローチ:デバウンスの核心は「遅延実行、遅延中に再トリガーされたらタイマーリセット」。クロージャでtimer参照を保持し、毎回clearTimeoutしてから再設定。

重要な詳細:1. thisとargumentsを保存し、apply/callでコンテキストをバインド;2. クロージャ関数を返す;3. キャンセル機能を提供。

追及質問:初回を即時実行にするには?——immediateパラメータを追加し、最初のトリガーで即時実行、以降はデバウンスロジック。

問題2:スロットル(Throttle)

考察ポイント:クロージャ、タイマー、タイムスタンプ

解法アプローチ:スロットルの核心は「固定時間間隔で1回のみ実行」。2つの実装方式:タイムスタンプ版(前回実行時刻で判定)とタイマー版(setTimeoutで制御)。タイムスタンプ版は初回即時実行、タイマー版は初回遅延実行。

重要な詳細:1. タイムスタンプ版はトリガー停止後に最後の1回を実行しない;2. タイマー版は停止後に最後の1回を実行する;3. 両者を組み合わせて「先頭も末尾も実行」するスロットルを実現。

追及質問:leadingとtrailingパラメータの実装は?——leadingは初回実行の有無、trailingは終了時の最後の実行の有無を制御。

問題3:Promise

考察ポイント:状態マシン、マイクロタスク、チェーン呼び出し

解法アプローチ:Promiseには3つの状態:pending、fulfilled、rejected。核心は状態がpendingからfulfilledまたはrejectedにのみ遷移し、不可逆であること。thenメソッドは新しいPromiseを返してチェーン呼び出しを実現。

重要な詳細:1. resolve/rejectはqueueMicrotaskでマイクロタスクの非同期実行をシミュレート;2. thenのコールバックは状態変更後に非同期実行;3. 値の貫通——thenにコールバックがない場合、値は次のthenに渡される。

追及質問:Promiseのthenがマイクロタスクなのはなぜ?——実行順序の確定性を保証するため。マイクロタスクは現在のマクロタスク終了後、次のマクロタスク開始前に実行される。

問題4:Promise.all

考察ポイント:並行制御、エラー処理、カウンター

解法アプローチ:Promise配列を受け取り、全成功時に順序通り結果配列を返す、いずれかが失敗したら即座にその失敗理由を返す。カウンターで完了したPromise数を記録し、全完了後にresolve。

重要な詳細:1. 空配列は即座にresolve([]);2. 結果は完了順ではなく元の順序で並べる;3. pushではなくインデックスで順序を保証。

追及質問:Promise.allSettledの実装は?——成功/失敗に関わらず全結果を収集し、statusフィールドで区別。

問題5:call/apply/bind

考察ポイント:thisバインディング、関数をオブジェクトメソッドとして呼び出す

解法アプローチ:call/applyの核心は、関数をターゲットオブジェクトのメソッドとして呼び出すこと。「オブジェクトのメソッドとして呼び出すとthisがそのオブジェクトを指す」特性を利用。bindはthisを事前にバインドした新関数を返す。

重要な詳細:1. callは引数リスト、applyは引数配列;2. thisがnull/undefinedの場合、グローバルオブジェクトを指す(非strictモード);3. bindされた関数はnewで呼び出せる——その場合thisはバインド先ではなくインスタンスを指す。

追及質問:bind後の関数をnewで呼び出した場合の処理は?——this instanceof boundFunctionで判定し、new呼び出しの場合はバインドされたthisを無視。

問題6:ディープコピー

考察ポイント:再帰、型判定、循環参照

解法アプローチ:オブジェクトの全プロパティを再帰的に走査。各値について:プリミティブ型は直接コピー、参照型は再帰的にクローン。循環参照はWeakMapで既にコピー済みのオブジェクトを記録。

重要な詳細:1. 型判定はObject.prototype.toString.callを使用;2. 配列は[]、オブジェクトは{}で初期化;3. 循環参照はWeakMapで無限再帰を回避;4. 特殊オブジェクト(Date、RegExp、Map、Set)は個別処理が必要。

追及質問:MapではなくWeakMapを使う理由は?——WeakMapのキーは弱参照で、ガベージコレクションを妨げず、メモリリークを防げる。

問題7:EventEmitter

考察ポイント:Pub/Subパターン、イベント管理

解法アプローチ:イベントマップ{eventName: [callback1, callback2, ...]}を維持。onでイベント登録、emitでイベント発火、offでイベント削除、onceで一回限りのイベント登録。

重要な詳細:1. 同一イベントに複数リスナーを登録可能;2. emitは登録順に実行;3. onceリスナーは実行後に自動削除;4. offにコールバックを渡さないとそのイベントの全リスナーを削除。

問題8:LRUキャッシュ

考察ポイント:ハッシュマップ+双方向リンクリスト、O(1)読み書き

解法アプローチ:ハッシュマップにkey→ノードのマッピングを保存(O(1)検索)、双方向リンクリストでアクセス順序を維持(O(1)挿入・削除)。get時にノードをリストの先頭に移動、put時に容量超過なら末尾ノードを削除。

重要な詳細:1. 番兵ノード(dummy head/tail)で境界処理を簡略化;2. ノードの削除・移動時にポインタの順序に注意;3. Mapのsizeプロパティで容量を判定。

問題9:仮想DOM

考察ポイント:ツリー構造、diffアルゴリズム、レンダリング関数

解法アプローチ:仮想DOMは本質的にJSオブジェクトでDOM構造を記述するもの。tag、props、childrenの3つのコアプロパティを持つ。renderメソッドで再帰的にリアルDOMノードを作成。

重要な詳細:1. propsのイベントプロパティはaddEventListenerでバインド;2. childrenは文字列または仮想ノードの可能性;3. 簡略版diffは同階層のノードのみ比較。

問題10:カリー化(Curry)

考察ポイント:クロージャ、関数型プログラミング、引数収集

解法アプローチ:多引数関数を一連の単引数関数に変換。クロージャで引数を収集し、収集数が元関数の引数数に達したら実行、そうでなければ新関数を返して収集を継続。

重要な詳細:1. Function.prototype.lengthで元関数の引数数を取得;2. 各呼び出しで0〜複数の引数を渡せる;3. プレースホルダー対応(例:_で保留位置を示す)。

問題11:配列のフラット化

考察ポイント:再帰、reduce、イテレーション

解法アプローチ:再帰版はreduce+concatを使用、配列に遭遇したら再帰的に展開。反復版はスタックを使用、要素を順にポップし、配列ならスタックにプッシュして処理を継続。

重要な詳細:1. フラット化の深さをdepthパラメータで制御;2. 空の配列要素はスキップ;3. ネイティブflat()は1レベルのみフラット化、flat(Infinity)で完全フラット化。

問題12:Pub/Subパターン

考察ポイント:デザインパターン、疎結合

解法アプローチ:EventEmitterと似ているが、パブリッシャーとサブスクライバーの疎結合を強調。EventCenterを仲介者として追加し、パブリッシャーは発行のみ、サブスクライバーは購読のみを行う。

重要な詳細:1. 名前空間のサポート(例:"user:login");2. ワイルドカード購読のサポート;3. エラー分離——1つのリスナーのエラーが他のリスナーに影響しない。

問題13:AJAX

考察ポイント:XMLHttpRequest、Promiseラッパー

解法アプローチ:XHRオブジェクト作成→openでリクエスト設定→ヘッダー設定→sendで送信→onreadystatechangeを監視→Promiseで非同期結果をラップ。

重要な詳細:1. readyStateが4でstatusが200-299なら成功;2. GETリクエストのパラメータはURLに付加;3. タイムアウト処理はxhr.timeout+ontimeout;4. モダンな代替手段はfetch。

問題14:テンプレートエンジン

考察ポイント:正規表現置換、with文、new Function

解法アプローチ:テンプレート文字列の{{expression}}を対応するデータ値で置換。正規表現で{{}}をマッチング、with(data)でデータスコープを作成、new Functionでレンダリング関数を動的生成。

重要な詳細:1. HTML特殊文字のエスケープでXSS防止;2. 条件分岐やループのサポート({{if}}、{{each}}など);3. コンパイルキャッシュで再コンパイルを回避。

問題15:フロントエンドルーター

考察ポイント:History API、hashchange、ルートマッチング

解法アプローチ:Hashルーティングはhashchangeイベントを監視、HistoryルーティングはpushState/replaceState+popstateイベントを使用。ルートテーブルを維持し、現在のパスを対応コンポーネントにマッチング。

重要な詳細:1. Historyルーティングはサーバー側の協力が必要(全パスでindex.htmlを返す);2. ルートパラメータの解析(例:/user/:id);3. ネストされたルートの実装。

問題16:new演算子

考察ポイント:プロトタイプチェーン、コンストラクタ、thisバインディング

解法アプローチ:1. 空オブジェクトを作成;2. オブジェクトの__proto__をコンストラクタのprototypeに設定;3. コンストラクタのthisを新オブジェクトにバインドして実行;4. コンストラクタがオブジェクトを返した場合はそれを返す、そうでなければ新オブジェクトを返す。

重要な詳細:1. コンストラクタが明示的にオブジェクトを返す場合の処理;2. プロトタイプチェーンの確立プロセス;3. Object.createとの違い。

問題17:instanceof

考察ポイント:プロトタイプチェーン検索

解法アプローチ:オブジェクトのプロトタイプチェーンを上方向に検索し、コンストラクタのprototypeが見つかるかを確認。whileループで__proto__を走査し、nullに到達するまで続ける。

重要な詳細:1. プリミティブ型のinstanceofは常にfalse;2. nullとundefinedの処理;3. Symbol.hasInstanceによるカスタム動作。

問題18:Object.create

考察ポイント:プロトタイプ継承、プロパティ記述子

解法アプローチ:新オブジェクトを作成し、__proto__を渡されたプロトタイプオブジェクトに設定。第2引数が存在する場合、Object.definePropertiesでプロパティを追加。

重要な詳細:1. プロトタイプがnullの場合、作成されたオブジェクトにはプロトタイプがない;2. 第2引数のプロパティ記述子の形式;3. newとの違い——コンストラクタを実行しない。

問題19:非同期並行制限

考察ポイント:Promise、並行制御、キュー

解法アプローチ:実行キューと実行中カウンターを維持。実行中数が並行制限未満の場合、キューからタスクを取り出して実行;タスク完了後にカウンターを減らし、次のタスクを取り出す。

重要な詳細:1. タスク完了後のコールバック処理;2. エラーが他のタスクに影響しないこと;3. タスクの動的追加のサポート。

問題20:二分木の走査

考察ポイント:再帰、スタック、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:完全なテストコードの記述は求められませんが、書き終えた後、いくつかのテストケースを口頭で確認し、検証意識を示しましょう。

#ライブコーディング#フロントエンド面接#Debounce & Throttle#Promise#Deep Clone#LRU Cache#Currying#Virtual DOM