概要
JavaScriptを学習、理解を深めるため「JavaScript Primer 迷わないための入門書」を読み、
理解した内容等を記載していく。
「【JavaScript】JavaScript入門一覧」に他の記事をまとめています。
この記事で理解できること
- 巻き上げとは
- 関数における巻き上げの挙動
- クロージャとクロージャ内部の仕組み
関数宣言と巻き上げ
-
巻き上げ
とは、宣言部分がもっとも近い関数またはグローバルスコープの先頭に移動しているように見える動作のこと。 - 代表的な例として、
functionキーワードを使用した関数宣言より前に関数を呼び出しをしてもエラーにならない
挙動がある。
// この時点では関数は定義されていないのに関数を呼び出せる
hello(); // => hello
// この位置で関数を定義している
function hello() {
console.log("hello");
}
巻き上げによって以下のように定義されているのと同じ
// グローバルスコープに移動しているかのような挙動となる
function hello() {
console.log("hello");
}
hello(); // => hello
クロージャ
- 「
静的スコープ
」「メモリ管理の仕組み
」を利用し、関数内で外側のスコープにある変数への参照を保持できる
という仕組みのこと。
静的スコープ
- JavaScriptのスコープには、
どの識別子がどの変数を参照するかが静的に決定される
という性質がある。 - 以下の例では、下記の流れで静的に参照先が決定される。
- 関数printStr()の関数スコープには変数strが定義されていない
- 外側のスコープ(グローバルスコープ)を探す
- グローバルスコープのstrを参照する(静的に参照が決定する)
const str = "グローバルスコープのstr";
// printStr関数内で定義されたstrは、スコープチェーンの仕組みによりグローバルスコープの変数strを参照する
function printStr() {
console.log(str);
}
function excePrintStrMethod() {
// この変数strはprintStr関数から参照されない
const str = "関数スコープのstr";
// printStr関数はグローバルスコープの変数strを参照することが決定している(静的スコープ)
printStr();
}
excePrintStrMethod(); // => グローバルスコープのstr
excePrintStrMethod(); // => グローバルスコープのstr
メモリ管理の仕組み
- プログラミング言語には、使わなくなった変数やデータを
メモリから解放する仕組み
を持っている。 - 言語によっては、
手動でメモリを解放するコード
を書く必要がある。 - JavaScriptでは、自動でメモリを解放してくれる
ガベージコレクション
という仕組みを採用している。
メモリから不要なデータが解放されるタイミング例
// ①"first comment"という文字列データがメモリ上に確保され、変数strがメモリ上のデータを参照する
let str = "first comment";
// ②再代入により変数strは、"second comment"という文字列データを参照するようになる
str = "second comment";
// ③"first comment"は、どこからも参照されなくなりガベレージコレクションの回収対象になる
// ④任意のタイミングでガベレージコレクションが回収し、メモリから解放される
関数の実行が終了した際に解放される場合
// 関数内に定義した変数は関数呼び出しのたびに定義され、呼び出し完了時に不要となり解放される
function func() {
const str = "関数内で定義";
console.log(str);
}
func(); // => 関数内で定義
関数の実行が終了しても解放されない場合
// 関数内で定義した変数を返り値としている
function func() {
const str = "関数内で定義";
return str;
}
// 関数funcの返り値を変数resultに代入
const result = func();
console.log(result); // => 関数内で定義(変数resultはデータを参照し続けている)
静的スコープとメモリ管理の仕組みを使用したクロージャの例
const baseCounter = () => {
let count = 0;
return function increment() { // *1
return count = count + 1;
}
}
// counterAに代入したタイミングでbaseCounter()内の変数や関数が定義される
// counterAは"*1"のincrement()を参照している
// "*1"のincrement()はcountを参照しているため、結果的にcounterAも参照し続けている事になる
const counterA = baseCounter();
console.log(counterA()); // => 1
console.log(counterA()); // => 2
console.log(counterA()); // => 3
console.log(counterA()); // => 4
// counterBに代入したタイミングでbaseCounter()内の変数や関数が新たに定義される
// counterBを呼び出すと、結果は1からとなっており、新たな参照を持っていることが分かる
const counterB = baseCounter();
console.log(counterB()); // => 1
console.log(counterB()); // => 2
console.log(counterB()); // => 3
console.log(counterB()); // => 4
console.log(counterA()); // => 5
console.log(counterA()); // => 6
console.log(counterB()); // => 5