【ITニュース解説】5 Javascript coding interview questions - Part 7
2025年09月27日に「Dev.to」が公開したITニュース「5 Javascript coding interview questions - Part 7」について初心者にもわかりやすく解説しています。
ITニュース概要
JavaScriptのコーディング面接で頻出する5つの問題を紹介。ネストしたオブジェクトのフラット化、デバウンス、スロットル、アナグラムのグループ化、カリー化といったテーマを具体的なコード例とともに解説する。SE志望の初心者がJavaScriptの実践力を高めるのに役立つだろう。
ITニュース解説
この解説では、JavaScriptのコーディング面接でよく出される5つの問題と、その解答コードについて、システムエンジニアを目指す初心者にもわかるように、それぞれの機能が何をするのか、そしてどのような場面で役立つのかを説明する。
Q1. Flatten a Nested Object(ネストされたオブジェクトのフラット化)
この問題は、オブジェクトの中にさらにオブジェクトがあるような深く入れ子になったデータ構造を、すべて平らな(単一レベルの)オブジェクトに変換する方法を求めるものだ。例えば、{ a: 1, b: { c: 2, d: { e: 3 } } } のような複雑なオブジェクトを、{ a: 1, "b.c": 2, "b.d.e": 3 } のように、キーをドット(.)で区切って階層を示し、すべての値を最上位に配置する形に変換する。これは、データを扱いやすくしたり、特定のデータ形式(例えばデータベースのテーブルのカラム名)に合わせたりする際に非常に便利なテクニックだ。
提供された解答コードでは、flattenObjectという関数を使ってこの処理を実現している。この関数は「再帰」というプログラミング手法を使っており、関数が自分自身を呼び出すことで、オブジェクトの深い階層まで繰り返し処理を進めていく。具体的には、引数として現在のオブジェクトobj、現在のキーのパスを保持するparent(最初は空)、そして最終的な結果を格納するresというオブジェクトを受け取る。関数内では、for...inループでobjの各キーと値を処理する。newKeyとして、parentがあればそれに現在のキーをドットでつなげ、なければ現在のキーそのものを使う。もしobj[key]がオブジェクト(ただしnullや配列ではない)であれば、それはさらに深く潜る必要がある階層なので、flattenObject関数をobj[key]を新しいオブジェクトとして、newKeyを次のparentとして、そしてresを渡して再度呼び出す。もしobj[key]がオブジェクトでなければ、それは最終的な値なので、res[newKey]にその値を格納する。すべてのキーが処理されると、resオブジェクトが返され、これがフラット化された結果となる。この再帰的なアプローチにより、どれほど深い階層のオブジェクトでも適切に処理できる。
Q2. Debounce Function(デバウンス関数)
デバウンス関数は、特定の関数が短時間で何度も呼び出された場合に、最後の呼び出しから一定の時間が経過するまでその関数の実行を遅らせる機能を提供する。これは、頻繁に発生するイベント(例: ユーザーが検索窓に文字を入力する、ウィンドウのリサイズ、スクロールなど)の処理を最適化する際に非常に役立つ。例えば、検索窓に文字を入力するたびにサーバーに検索リクエストを送ると、無駄なリクエストが増える可能性があるが、デバウンスを使えば、ユーザーがしばらく入力を止めた後(例えば500ミリ秒後)に一度だけ検索処理を実行するといったことができる。これにより、システムの負荷を軽減し、よりスムーズなユーザー体験を提供できる。
提供された解答コードのdebounce関数は、遅延させたい関数fnと、遅延時間delay(ミリ秒単位)を引数として受け取り、新しい関数を返す。この返された関数が実際にイベントリスナーなどに登録されて使われる。内部では、timeoutという変数がsetTimeout関数が返すタイマーIDを保持する。返された関数が呼び出されるたびに、まずclearTimeout(timeout)が実行される。これは、もし以前に設定されたタイマーがあればそれをキャンセルするという意味だ。つまり、新しい呼び出しがあった瞬間に、以前の遅延実行はリセットされる。その後、timeout = setTimeout(() => fn.apply(this, args), delay)で、delayミリ秒後にfn関数を実行する新しいタイマーを設定する。fn.apply(this, args)は、元の関数fnを、デバウンスされた関数が呼び出された際のthis(コンテキスト)とargs(引数)を使って実行するためのものだ。この仕組みにより、delayミリ秒以内に何度もデバウンスされた関数が呼び出されても、タイマーが繰り返しリセットされるため、実際のfnの実行は最後の呼び出しからdelayミリ秒後に一度だけとなる。
Q3. Throttle Function(スロットル関数)
スロットル関数は、デバウンスと似ているが、異なる目的で使用される。スロットルは、関数が短時間のうちに何度も呼び出されても、指定された一定の間隔(limit)で最大一度しか実行されないように制限するものだ。デバウンスが「最後の操作後に一度だけ」なのに対し、スロットルは「指定された間隔内で最大一度」実行する。例えば、ゲームでのボタン連打や、ウェブサイトのスクロールイベントで特定の処理をトリガーする際に、処理が頻繁に起こりすぎてブラウザのパフォーマンスが悪化するのを防ぎたい場合に非常に有効だ。これにより、アプリケーションが過剰な処理で重くなるのを避けつつ、ユーザーの操作にリアルタイム性を持たせることができる。
提供された解答コードのthrottle関数は、制限したい関数fnと、実行間隔limit(ミリ秒単位)を引数として受け取り、新しい関数を返す。この返された関数が実際にイベントリスナーなどに登録されて使われる。内部ではlastCallという変数が定義されており、これは最後にfnが実行された時刻を記録するために使われる。返された関数が呼び出されるたびに、まずDate.now()で現在の時刻(ミリ秒単位)を取得する。次に、if (now - lastCall >= limit)という条件文で、現在の時刻と最後にfnが実行された時刻lastCallの差が、設定されたlimit時間以上であるかどうかをチェックする。もしこの条件が真であれば、つまりlimit時間以上が経過していれば、lastCallを現在の時刻に更新し、fn.apply(this, args)で元の関数fnを実行する。fn.apply(this, args)は、元の関数fnを、スロットルされた関数が呼び出された際のthis(コンテキスト)とargs(引数)を使って実行するためのものだ。もし条件が偽であれば、つまりlimit時間以内であれば、関数fnは実行されず、次の呼び出しを待つことになる。このメカニズムにより、fnの実行頻度がlimitで指定された間隔より短くなることはない。
Q4. Group Anagrams(アナグラムのグループ化)
この問題は、与えられた文字列の配列の中から、互いにアナグラムである(つまり、文字を並び替えることで互いに生成できる)単語をグループ化するものだ。例えば、「eat」「tea」「ate」はすべて文字「a」「e」「t」で構成されており、互いにアナグラムの関係にある。この問題の目的は、これらの単語を一つのグループとしてまとめることだ。これは、テキスト処理や言語関連のアプリケーションで、単語の類似性に基づいてデータを分類したり、検索機能を強化したりする際に応用できる。
提供された解答コードのgroupAnagrams関数は、wordsという文字列の配列を受け取る。この関数は、mapという名前の空のオブジェクト(ハッシュマップや連想配列と呼ばれることもあり、キーと値を関連付けてデータを管理するのに使う)を初期化する。このmapは、ソートされた文字列をキーとして、そのアナグラムである単語の配列を値として保持するために使う。関数はfor...ofループを使って、words配列の各wordを順に処理していく。各wordに対して、まずword.split('')で単語を文字の配列に分解し、sort()でその文字の配列をアルファベット順にソートし、最後にjoin('')でソートされた文字の配列を再び文字列に結合する。この結果がkeyとなる。アナグラムである単語は、構成文字が同じなので、この処理によって必ず同じkeyが生成される。次に、mapにkeyがまだ存在しなければ、map[key]に空の配列を割り当てる。これは、そのアナグラムグループの入れ物を準備する意味だ。その後、現在のwordをmap[key]の配列に追加する。すべての単語の処理が終わると、mapオブジェクトには、ソートされた文字列をキーとして、アナグラムのグループを値とするデータ構造が完成する。最後に、Object.values(map)を使ってmapオブジェクトの「値」の部分(つまり、アナグラムのグループの配列)だけを抽出し、それらを配列として返す。
Q5. Currying Function(カリー化関数)
カリー化は、複数の引数を取る関数を、一つずつ引数を受け取る関数が連続して返される形に変換する関数型プログラミングのテクニックだ。例えば、add(a, b, c)のように3つの引数を一度に受け取る関数があったとして、これをカリー化すると、add(a)(b)(c)のように、引数を一つずつ順番に渡せるようになる。途中で引数をすべて渡しきらない場合、部分的に適用された引数を持つ新しい関数が返され、残りの引数を受け取ることができる状態となる。これは、特定の引数を固定した新しい関数を簡単に作成したり、関数を組み合わせやすくしたりする際に便利で、より柔軟な関数設計が可能になる。
提供された解答コードのcurry関数は、カリー化したい元の関数fnを引数として受け取り、curriedという名前の内部関数を返す。このcurried関数が、実際にカリー化された関数として振る舞う。curried関数は、...argsという構文で任意の数の引数を受け取ることができる。まず、if (args.length >= fn.length)という条件文で、現在curried関数に渡された引数の数args.lengthが、元の関数fnが期待する引数の数fn.length以上であるかをチェックする。fn.lengthは、関数の定義時に宣言された仮引数の数を自動的に取得するプロパティだ。もし条件が真であれば、つまり必要なすべての引数、あるいはそれ以上の引数が揃っている場合、fn.apply(this, args)を実行して、元の関数fnを、現在までに収集されたargsを使って呼び出し、その結果を返す。applyを使うことで、関数が呼び出されたときのthisコンテキストを適切に引き継ぐ。もし条件が偽であれば、つまりまだ必要な引数がすべて揃っていない場合、新しい関数を返す。この新しい関数は、さらに...nextArgsという引数を受け取ることができ、呼び出された際には、以前にcurried関数に渡された引数argsと、新たに渡された引数nextArgsをconcat(結合)し、その結合された引数セットを使ってcurried関数を再度呼び出す。この再帰的なプロセスにより、必要な引数がすべて揃うまで新しい関数を返し続け、引数が揃った時点で元の関数fnが実行されるという仕組みが実現される。