【ITニュース解説】Under the Hood of JavaScript: Demystifying Hoisting, Objects, 'this', and the 'new' Keyword
2026年09月29日に「Dev.to」が公開したITニュース「Under the Hood of JavaScript: Demystifying Hoisting, Objects, 'this', and the 'new' Keyword」について初心者にもわかりやすく解説しています。
ITニュース概要
JavaScriptの「不可解な挙動」は、Hoisting(宣言の事前処理)、Objectsの参照渡し、関数の呼び出し方で決まる「this」、`new`キーワードによるオブジェクト生成という4つの核心概念で理解できる。これらを把握すれば、JSの動作原理が明確になる。
ITニュース解説
JavaScriptのコードを書いていると、時折理解に苦しむ奇妙な挙動に出くわすことがある。例えば、変数を宣言する前にconsole.logするとエラーにならずundefinedが表示されたり、オブジェクトをコピーしたはずなのに元のオブジェクトまで変更されてしまったり、といった経験はないだろうか。これらの現象はJavaScriptが気まぐれに動いているわけではなく、特定のルールに基づいて発生している。本解説では、システムエンジニアを目指す初心者向けに、JavaScriptの深層にある「巻き上げ(Hoisting)」「オブジェクトと参照」「thisキーワード」「newキーワード」という4つの主要な概念を詳細に解説する。これらの概念を理解することで、JavaScriptの「魔法」に見えていたものが、論理的な挙動として腑に落ちるはずだ。
まず「巻き上げ(Hoisting)」について説明する。JavaScriptはコードを実行する前に、準備段階として「作成フェーズ」という処理を行う。このフェーズで、JavaScriptはコード全体をスキャンし、宣言された変数や関数をメモリに登録する。その後、「実行フェーズ」でコードが一行ずつ実行される。巻き上げとは、この作成フェーズで宣言がメモリに登録される副作用のことである。コードが物理的に移動するわけではなく、実行前に宣言情報が準備されるイメージだ。このとき、宣言の種類によって初期値や利用可能なタイミングが異なる。
関数宣言(function foo() {})は、関数全体が作成フェーズでメモリに格納されるため、宣言された位置よりも前で呼び出すことができる。
varキーワードで宣言された変数は、作成フェーズで変数名が登録され、初期値としてundefinedが設定される。そのため、宣言の前にconsole.logするとundefinedが表示される。変数への値の代入は、実行フェーズでそのコード行に到達したときに初めて行われる。
一方、letやconstで宣言された変数は、varと同様に作成フェーズでメモリに登録されるが、実行フェーズで宣言行に到達するまでは「一時的なデッドゾーン(Temporal Dead Zone: TDZ)」と呼ばれる利用不可能な状態になる。この期間中にアクセスしようとすると、「初期化前にアクセスできません(ReferenceError: Cannot access 'variable' before initialization)」というエラーが発生する。このエラーメッセージは、変数が存在することはJavaScriptが知っているが、まだ利用できない状態であることを示している。class宣言もlet/constと同様にTDZの対象となる。
巻き上げに関連する注意点として、関数式(var greet = function() {};)は、greetという変数のみがundefinedとして巻き上げられ、関数本体は巻き上げられないため、宣言前に呼び出すとTypeErrorになる。また、forループ内でvarを使うと、varは関数スコープであるため、ループ全体で変数が共有され、非同期処理などで意図しない結果を招くことがある。letを使用すると、各イテレーションごとに新しい変数が作成されるため、この問題は解決される。さらに、関数宣言はvar変数の宣言よりも優先されるため、同じ名前の場合、関数として初期化された後にvarによる値の代入が行われる。これらの複雑さを避けるため、原則としてconstを使用し、再代入が必要な場合にのみletを使用することが推奨される。varは避けるべきである。
次に「オブジェクトと参照」について解説する。JavaScriptの値は大きく二つのカテゴリに分けられる。一つはstring、number、boolean、null、undefined、symbol、bigintといった「プリミティブ値」である。もう一つは、配列や関数を含む「オブジェクト」である。これらはコピーされる際の挙動が大きく異なる。プリミティブ値は「値渡し」でコピーされ、新しい独立したコピーが作られるため、コピー元の値に影響を与えない。しかし、オブジェクトは「参照渡し」でコピーされる。これは、オブジェクトそのものではなく、オブジェクトがメモリ上のどこにあるかを示す「アドレス(参照)」がコピーされることを意味する。結果として、複数の変数が同じオブジェクトを指し示すことになり、いずれかの変数を通じてオブジェクトの内容を変更すると、その変更は他のすべての変数が指し示すオブジェクトにも反映される。
この参照の特性は、いくつかの重要な挙動を説明する。例えば、constキーワードで宣言されたオブジェクト変数は、その変数が指し示すオブジェクトのアドレスを変更することはできないが、オブジェクト自体のプロパティを変更することは可能である。これはconstが変数の「バインディング(参照先)」をロックするのであって、オブジェクトの「内容」を不変にするわけではないためだ。また、オブジェクト同士の比較(===)は、内容が同じであっても参照が異なればfalseとなる。同じ参照を持つオブジェクト同士のみtrueとなる。オブジェクトを完全にコピーして独立させたい場合は、スプレッド構文(浅いコピー)やstructuredClone関数(深いコピー)を用いる必要がある。
三つ目の概念は「thisキーワード」である。thisはJavaScriptで最も混乱しやすい概念の一つだが、その挙動は「関数がどこで書かれたか」ではなく、「関数がどのように呼び出されたか」、つまり「呼び出しサイト」によって決定される。thisの値を決定する主要なルールは四つあり、それに加えてアロー関数という例外が存在する。
一つ目の「デフォルトバインディング」は、関数が単独で呼び出される場合、厳格モードではthisがundefinedになる。非厳格モードではグローバルオブジェクト(ブラウザではwindow、Node.jsではglobalThis)を指す。
二つ目の「暗黙的バインディング」は、関数がオブジェクトのメソッドとして呼び出される場合、thisはそのメソッドを所有するオブジェクトを指す。例えば、obj.method()と呼び出した場合、thisはobjとなる。メソッドをオブジェクトから分離して単独で呼び出すと、デフォルトバインディングが適用され、thisの値が失われる「thisの紛失」という問題が発生しやすい。
三つ目の「明示的バインディング」は、call、apply、bindメソッドを使って、開発者が明示的にthisの値を指定できるルールである。callは引数をカンマ区切りで渡し、applyは引数を配列で渡して関数を即座に実行する。bindはthisの値が固定された新しい関数を生成し、その関数は後で実行できる。
四つ目の「newバインディング」は、関数がnewキーワードと共に呼び出された場合に適用される。これについては次のセクションで詳しく説明する。
これらのルールには優先順位があり、newバインディングが最も強く、次に明示的バインディング、暗黙的バインディング、そしてデフォルトバインディングが最も弱い。
「アロー関数」はthisのバインディングルールにおいて例外となる。アロー関数は自身のthisを持たず、それが定義された「外側のスコープ」からthisの値を継承する(レキシカルthisと呼ばれる)。これは、コールバック関数内でthisの紛失を防ぐのに非常に役立つが、オブジェクトのメソッドとして使用すると意図しないthisの値になるため注意が必要である。また、アロー関数はcall、apply、bindでthisを明示的に変更することもできず、newキーワードと一緒に使うこともできない。
最後に「コンストラクタ関数とnewキーワード」について解説する。ES6のclass構文が登場する以前は、同じ構造を持つ多数のオブジェクトを作成する際に、オブジェクトリテラルをコピーし続けるのは非効率だった。この問題を解決するために、「コンストラクタ関数」が用いられた。コンストラクタ関数は通常の関数だが、慣習的に大文字で始まり、newキーワードと共に呼び出すことで、オブジェクトの設計図として機能する。
newキーワードでコンストラクタ関数を呼び出すと、JavaScriptは以下の4つのステップを実行する。
- まず、全く新しい空のオブジェクトが作成される。
- その新しいオブジェクトのプロトタイプが、コンストラクタ関数の
prototypeプロパティにリンクされる。これにより、オブジェクトはコンストラクタのプロトタイプに定義されたメソッドやプロパティを利用できるようになる。 - コンストラクタ関数が実行され、このとき、関数の内部で使われる
thisは、ステップ1で作成された新しいオブジェクトにバインドされる(これが前述のnewバインディングルールである)。これにより、this.propertyName = valueという形で新しいオブジェクトにプロパティを設定できる。 - コンストラクタ関数が明示的に別のオブジェクトを返さない限り、ステップ1で作成された新しいオブジェクトが返される。
コンストラクタ関数でメソッドを定義する際、各オブジェクトインスタンスが独自のメソッドのコピーを持つのではなく、プロトタイプにメソッドを配置することが重要である。これにより、すべてのインスタンスが一つの共有されたメソッドを参照し、メモリ効率が向上する。
newキーワードを忘れてコンストラクタ関数を呼び出すと、thisはデフォルトバインディングに従うため、厳格モードではエラーになり、非厳格モードではグローバルオブジェクトを汚染してしまう可能性がある。このため、class構文の使用が推奨される。classを使うとnewキーワードを忘れた場合にエラーが発生し、安全性が高まるためである。
まとめとして、JavaScriptの不可解な挙動は、以下の主要なルールによって説明できる。巻き上げは、コード実行前の作成フェーズにおける宣言の登録であり、functionは完全に、varはundefinedとして、let/constはTDZとして扱われる。オブジェクトは参照渡しであり、変数のコピーはメモリ上のアドレスをコピーするため、constは参照先を固定するがオブジェクトの内容は変更可能である。thisは関数が呼び出される場所で決定され、new、明示的、暗黙的、デフォルトの順に優先される。アロー関数は例外的に自身ではthisを持たず、外側のスコープから継承する。newキーワードは、新しいオブジェクトの作成、プロトタイプリンク、thisのバインディング、オブジェクトの返却という4つのステップを自動的に実行する、オブジェクト生成のための仕組みである。これらの概念を深く理解することで、JavaScriptのコードがどのように動作するのか、より明確に把握できるようになるだろう。