Skip to main content

コンテキスト、スコープ、クロージャーについて

学習の目的

この単元を完了すると、次のことができるようになります。

  • JavaScript で変数の通用範囲を指定する方法を特定する。
  • 関数がコールされた場所によって this がどのように変化するかを説明する。
  • クロージャーを使用して、関数の変数への参照を捕捉する。

どのようなプログラミング言語でもそれを理解するために重要な点は、変数の可用性、状態の維持方法、その状態へのアクセス方法を把握することです。 

JavaScript では、変数の可用性と可視性を通用範囲といいます。通用範囲は、変数が宣言された場所によって決まります。 

コンテキストは、コードの現在の実行状態です。この情報には this ポインターを使ってアクセスします。 

変数の通用範囲

JavaScript の変数は、varletconst のいずれかのキーワードを使用して宣言されます。キーワードをコールする場所によって、作成される変数の通用範囲が決まります。 

これらの 3 つの違いを理解するということは、突き詰めれば、代入の変更可能性と (補足的に) 非関数ブロック通用範囲の 2 つの要因を認識することです。代入の変更可能性については、このモジュールの最初の単元で説明しました。ここでは通用範囲について解説します。 

通用範囲の面白さ

通用範囲は、変数または引数がコードのどのブロックで宣言されたかによって決まります。ただし、var はコードの非関数ブロックを認識しません。つまり、if ブロックまたはループブロックで var をコールすると、変数が直近の囲み関数の通用範囲に代入されます。この機能を宣言の巻き上げといいます。

let または const を使用する場合は、宣言された実際のブロックが常に引数または変数の通用範囲になります。この点を示すおなじみの思考演習があります。

function countToThree() {

// i is in the scope of the countToThree function
for (var i = 0; i < 3; i++){
console.log(i); // iteration 1: 0
// iteration 2: 1
// iteration 3: 2
}
console.log(i); // What is this?
}

for ループの内側での console.log の出力は驚くことではなく、反復ごとに i の値が出力されます。ここで驚くであろう点は、最後の console.log ステートメントで、3 が出力されます。ifor ループの通用範囲と思われるものの内側で宣言されているため、エラーと思ったかもしれません。けれども、巻き上げにより、i は実際には countToThree の通用範囲に属します。 

巻き上げは必ずしも悪いものではありませんが、誤解されることが多く、変数のリークが発生することや、変数がコードブロックで再宣言された場合に誤って上書きされることがあります。こうした誤解に対処するために、この言語に letconst が追加され、ブロックレベルの通用範囲を使用して変数を作成できるようになっています。思考演習をもう一度見てみましょう。

for (let j = 0; j < 3; j++){

console.log(j); // 0
// 1
// 2
}
console.log(j); // error

var の代わりに let を使用すると、変数が for ループのコンテキスト内にのみ存在するようになります。ループが閉じた後でこの変数にアクセスしようとするとエラーが発生します。 

コンテキストと this

これまで解説してきたとおり、JavaScript はオブジェクトを中心に展開します。関数が呼び出されるときは、常にその関数の周辺にオブジェクトコンテナが存在します。このオブジェクトコンテナがそのコンテキストで、this キーワードはそのコンテキストをポイントします。つまり、コンテキストは関数が宣言されたときに設定されるのではなく、関数が呼び出されたときに設定されます。 

関数はオブジェクト間で受け渡しができるため、this のポイント先が変わることがあります。 

たとえば、これが JavaScript のオブジェクトであるとします。

var obj = {

aValue: 0,
increment: function(incrementBy) {
this.aValue = this.aValue + incrementBy;
}
}

インクリメント関数にアクセスすると、予想どおり機能します。

obj.increment(2);

console.log(obj.aValue); // 2

では、この関数を別の変数に代入した場合に、どのように機能するか見てみましょう。 

//assign function to variable

var newIncrement = obj.increment;
//now invoke through the new pointer
newIncrement(2);
console.log(obj.aValue); // still 2 not 4

変数を newIncrement に代入すると、関数が異なるコンテキストで実行されます。この場合は、具体的にグローバルコンテキストです。 

メモ

Function.apply()Function.call()Function.bind() 関数は、関数を異なるオブジェクトコンテキストに明示的にバインドしながら、その関数を呼び出す手段になります。

グローバルオブジェクト

あなたが開発者として記述した包含オブジェクトが 1 つもない JavaScript を実行すると、グローバルオブジェクトで実行されます。このため、こうした状態で呼び出された関数は、グローバルコンテキストで実行されていると考えられ、this にアクセスするとこのグローバルコンテキストをポイントします。 

ブラウザーでは、グローバルコンテキストは window オブジェクトです。この点は、ブラウザー開発者ツールで次のとおり実行すれば、簡単にテストできます。 

this === window; // true

increment の例で、increment 関数を newIncrement 変数に代入すると、コンテキストが呼び出された場所、つまりグローバルオブジェクトに移ります。この動作は簡単に示すことができます。

console.log(this.aValue); // NaN

console.log(window.aValue); // NaN
console.log(typeof window.aValue); // number

新しいコンテキストを使用して this.aValue を代入しようとすると、JavaScript オブジェクトの変更可能性が発揮されます。初期化されていない新しい aValue プロパティが this に追加されます。初期化されていない変数での算術処理に失敗し、値が NaN になります。けれども、windowaValue が存在することを確認でき、その値は実際には数値です。 

オブジェクトのあるコンテキスト

increment の例で、increment 関数がドット表記の obj を使用して呼び出される限り、thisobj をポイントします。あるいは、一般的に関数を someObject.function() としてコールすると、ドットの左側にあるものが、その関数が呼び出されるコンテキストになります。 

Bike の例を考えてみます。Bike コンストラクターは、this 参照を使用していくつかのプロパティを定義します。また、this を参照するそのプロトタイプに代入された関数があります。 

const Bike = function(frontIndex, rearIndex){

this.frontGearIndex = frontIndex || 0;
this.rearGearIndex = rearIndex || 0;
...
}
...
Bike.prototype.calculateGearRatio = function(){
let front = this.transmission.frontGearTeeth[this.frontGearIndex],
rear = this.transmission.rearGearTeeth[this.rearGearIndex];
if (front && rear) {
return (front / rear) ;
} else {
return 0;
}
};

その後、new キーワードを使用して Bike をコールします。 

const bike = new Bike(1,2);

console.log(bike.frontGearIndex); // 1
console.log(bike.rearGearIndex); // 2

一見すると、Bike コンストラクターをグローバルコンテキストで呼び出しているように見えます。けれども、この new キーワードがコンテキスト (と this ポインター) を、代入の左側にある新しいオブジェクトに移しています。 

いずれかの関数を呼び出すと、この時点では関数が bike オブジェクトのメンバーであるため、そのオブジェクトを包含コンテキストとして使用します。 

let gearRatio = bike.calculateGearRatio();

console.log(gearRatio); // 3

コンストラクターを間違った方法で呼び出してしまうことがよくあります。以下は、うまくいかない可能性がある場合を示しています。

const badBike = Bike(1,2);

console.log(badBike.frontGearIndex); // error
console.log(window.frontGearIndex); // 1

new を使い忘れると、Bike が他の関数のようにコールされ、thiswindow から新規作成されたオブジェクトへの重要な移行に失敗します。ここでオブジェクトの変更可能性が発揮され、代わりに frontGearIndex プロパティが window に追加されます。 

メモ

JavaScript の class 構文は、new キーワードを使用してコンストラクターを呼び出すことを強制するため、コンテキストを誤った方向に導くことがありません。

クロージャー

関数は、宣言されると、その中で宣言される変数または引数への参照と、それが該当する通用範囲内で参照する変数をすべて保持します。関数の変数や引数と、その関数が該当する通用範囲からのローカル変数や引数をすべてまとめたものをクロージャーといいます。 

この関数と、この関数が返す関数を考えてみます。 

const greetingMaker = function(greeting){

return function(whoGreeting){
return greeting + ", " + whoGreeting + "!";
}
}
const greetingHello = greetingMaker("Hello");
const greetingBonjour = greetingMaker("Bonjour");
const greetingCiao = greetingMaker("Ciao");
console.log(greetingHello("Gemma")); // Hello, Gemma!
console.log(greetingBonjour("Fabien")); // Bonjour, Fabien!
console.log(greetingCiao("Emanuela")); // Ciao, Emanuela!

greetingMaker が呼び出された場合、通常ならばその greeting 引数は、コールの対象のライフサイクルの間しか持続しないと思うでしょう。 

けれども、返された関数は、greetingMaker の通用範囲で greeting 引数を参照し続けます。そのため、最終的に greetingHello/Bonjour/Ciao を介して呼び出された時点でもアクセスできます。 

この言語を理解し使用するためには、クロージャーについても把握することが欠かせません。 

リソース

無料で学習を続けましょう!
続けるにはアカウントにサインアップしてください。
サインアップすると次のような機能が利用できるようになります。
  • 各自のキャリア目標に合わせてパーソナライズされたおすすめが表示される
  • ハンズオン Challenge やテストでスキルを練習できる
  • 進捗状況を追跡して上司と共有できる
  • メンターやキャリアチャンスと繋がることができる