進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

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

学習の目的

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

  • 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 をポイントします。あるいは、一般的に、関数を object.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 を介して呼び出された時点でもアクセスできます。 

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

リソース

JavaScript のクロージャ

Scope and Closures (スコープとクロージャ)