tacamy--blog

JavaScriptを勉強中の人のブログです。

JavaScript のスコープチェーンとクロージャを理解する

前回JavaScript のスコープの基本がわかったので、今回はスコープチェーンとクロージャを勉強してみました。 Call オブジェクトとクロージャの理解がかなり大変でした・・。

変数オブジェクト

JavaScript で変数の宣言と参照をするということは、変数オブジェクトを読み書きするということです。

  • 変数オブジェクトというのは、key と value による変数管理専用のハッシュテーブルのこと
  • key が変数名、value が値のセットになっているテーブルで、変数の数だけレコードができるイメージ
  • 変数オブジェクトはプログラマが意識することのない、便宜的なオブジェクト

グローバルオブジェクト

JavaScript は、ブラウザが新しいページを読み込んだとき、内部的に新しいグローバルオブジェクトを生成して初期化します。 グローバルオブジェクトとは、グローバル変数やグローバル関数を管理するための、変数オブジェクトです。

つまり、グローバル変数を定義するということは、実際にはグローバルオブジェクトのプロパティを定義するということです。 ブラウザ上では window オブジェクトがグローバルオブジェクトなので、トップレベルで宣言した変数が window オブジェクトのプロパティとして扱えるようになるのも納得できます。

var n = 10;
console.log(window.n); // > 10

Call オブジェクト

ローカル変数もグローバルオブジェクトと同じように、関数が実行されたタイミングで変数オブジェクトができます。 それを Call オブジェクトといいます。目に見えないオブジェクトで、プログラマが直接操作することはできません。

関数が実行されたタイミングで、var で宣言された変数と実行時に渡された引数などが、Call オブジェクトに格納されます。

Call オブジェクトは関数実行が終了すると消えます。ローカル変数の記憶領域が関数実行終了とともに破棄されるのは、これが関係しているのかもしれません。

Call オブジェクトに含まれる情報

  • その関数内のローカル変数の値
  • 関数に渡された引数名とその値
  • 引数情報を管理するオブジェクト(arguments オブジェクト)
  • this
  • 親の Call オブジェクトの場所(アドレス情報)

スコープチェーン

変数 x がグローバル変数とローカル変数で名前がぶつかっている場合、値を調べる手順は次のとおりです(これを名前解決という)。

  1. チェーン先頭の Call オブジェクトから x というプロパティを探す。
  2. あればその値を返し、なければチェーンの次の Call オブジェクトを探す。
  3. あればその値を返し、なければチェーンの最後まで順に Call オブジェクトを探していく。
  4. グローバルオブジェクトにも見つからなければ、x はこのスコープ内に存在しないので、エラーになる。

チェーンの先頭は一番内側にある Call オブジェクトで、外側に行くにつれて優先度が下がります。この繋がりをスコープチェーンといい、Call オブジェクトに含まれる「親の Call オブジェクトの場所」の情報をたどっていくことによって、スコープチェーンが成り立ちます。

スコープチェーンを理解することによって、次のようなコードで名前解決がどのように行われるかをイメージできるようになりました。

var a = "Global";
var b = "Global";

function outer() {
  var a = "Outer";

  function inner() {
    var c = "Inner";

    console.log(c); // > "Inner" (自身の Call Obj で発見)
    console.log(b); // > "Global" (inner なし → outer なし → Global Obj で発見)
    console.log(a); // > "Outer" (inner なし → outer の Call Obj で発見)
  }
  inner(); // 呼び出し
}
outer();   // 呼び出し

クロージャ

クロージャとは、ローカル変数の状態を保持できる関数のことです。 通常ローカル変数は関数の呼び出しが終わると破棄されますが、クロージャはローカル変数を参照し続けられます。

クロージャのつくりかた

ローカル変数を参照している関数内の関数のことをクロージャといい、次のようにつくります。

  1. 関数の中にさらに関数を作る
  2. 外側の関数のスコープ内で変数を定義する
  3. 関数の中で入れ子になった関数を戻り値として返す
  4. 内側の関数から、外側の変数を参照する

クロージャでカウンタをつくる

function fn(n) {
    var cnt = n;
    return function() { // この無名関数がクロージャ
        return ++cnt;   // スコープチェーンにより、外側のローカル変数 cnt を参照
    }
}
var f = fn(0);    // fn 関数の実行
console.log(f()); // > 1
console.log(f()); // > 2
console.log(f()); // > 3

上記のコードでは、次のようなことが内部で行われています。

  1. var f = fn(0); で fn 関数を実行
  2. fn 関数の戻り値は内側の無名関数なので、f には無名関数のアドレスが代入される
  3. このとき、無名関数が内部的に参照しているローカル変数 cnt も一緒に引っ張ってくる
  4. f(); とは、無名関数を実行すること(ここでは return ++cnt; )なので、cnt に渡した 0 がインクリメントされて 1 として返ってくる
  5. 次に f(); を実行したときも、匿名関数が cnt を参照し続けているため、先ほどの 1 をさらにインクリメントした 2 が返ってくる

内側の関数は、スコープチェーンによって、外側の関数の Call オブジェクトを参照し続けることができます。

独立したスコープチェーン

先ほどのカウンタで、fn 関数の実行を複数行った場合の挙動は次のとおりです。

function fn(n) {
    var cnt = n;
    return function() {
        return ++cnt;
    }
}
var f1 = fn(0);    // fn 関数の実行 1
var f2 = fn(10);   // fn 関数の実行 2
console.log(f1()); // > 1
console.log(f2()); // > 11
console.log(f1()); // > 2
console.log(f2()); // > 12

Call オブジェクトは関数の実行のタイミングで生成されます。よって、f1 と f2 で fn 関数を実行(インスタンス化)することで、別々の Call オブジェクトが生成され、スコープチェーンも別物になります。それぞれのローカル変数 cnt も別々の記憶領域として確保され、独立しています。

fn 関数は、インスタンス化するときに、一度だけ実行されるコンストラクタの役割をしています。 クロージャ自身はメソッド、クロージャから参照されるローカル変数 cnt はプロパティと考えられます。

クロージャによって、データの記憶領域をもったメソッドを作ることができます。

クロージャの使いどころ

クロージャでできることはなんとなく分かったけれど、実際にはどう使えばいいのかと考えていたところ、猿でもわかるクロージャ超入門 6 クロージャの応用例「注文ボタン」 のページで、なるほどという使い方が紹介されていたので、引用させていただきます。

ショッピングカートの「注文する」ボタンで、2重クリックすると2回決済されてしまうというサイトをたまに見かけます。 jQuery + クロージャを使うことで、これを防止してみましょう。

<form name="frm" id="frm">
<input type="submit" value="注文する" />
</form>
$(function(){
    var isClicked = false;

    $('#frm').submit(function(){
        if (isClicked) {
            alert('すでにクリック済みです。');
            return false;
        }
        isClicked = true;
    });
});

たしかにクロージャを使って、isClicked ローカル変数の状態を保存しておけば、2 重注文を防げますね。

まとめ

  • JavaScript で変数の宣言と参照をするということは、変数オブジェクトを読み書きするということ
  • 変数の名前解決は、スコープチェーンによって内側のローカル変数から順に探していく
  • スコープチェーンは、生成したインスタンスごとで別物になる
  • クロージャによって、ローカル変数の状態を保持できるメソッドをつくれる

参考サイト & 本 Thx♡

JavaScript のスコープを理解する

スコープとは、変数の有効範囲のことで、プログラムのどの場所から参照できるかを決める概念です。

スコープの種類

JavaScript のスコープには、グローバル変数とローカル変数の 2 種類あります。

グローバル変数 ローカル変数
関数の外(トップレベル)で宣言した変数 関数の中で宣言した変数, 関数の仮引数
プログラム全体から参照できる その関数の中でのみ参照できる

ブロックスコープは存在しない

Java などの言語では、if や for などの {} で囲まれたブロックごとにもブロックスコープがありますが、JavaScript には存在しません。 JavaScript でどうしてもブロックスコープを使いたい場合は、with 命令を使う方法や、無名関数を定義と同時に呼び出すなどの方法で、擬似的にブロックスコープを作ることは可能です。

補足 : let を使うとブロックスコープがつくれると教えてもらったのですが、ちゃんと調べてないので、調べたらここに追記します!

スコープの役割

  • 同じ名前の変数が、意図せず競合することを避ける。
    スコープが違う場合、同じ名前の変数であっても別物として扱われる。
  • ローカル変数の記憶領域は、関数の実行が終わり次第、破棄される。
    グローバル変数はプログラムが終了するまで記憶領域を確保するため、関数内でしか必要ない変数までグローバル変数にした場合、無駄にメモリを消費することになる。

宣言時の var の有無

var を省略して変数宣言をした場合、関数内での宣言であっても、その変数はグローバル変数になってしまいます。 混乱を避けるため、変数宣言の var は必ずつけること。

ローカル変数の有効範囲

ローカル変数は「関数全体で有効」なので、同じ関数内であれば、変数宣言より前のコードからアクセスできます。 これをホイスティング(巻き上げ)と呼びます。

var scope = "Global";

function getValue() {
    console.log(scope);  // > undefined
    var scope = "Local";
    return scope;
}

console.log(getValue()); // > "Local" (ローカル変数を参照)
console.log(scope);      // > "Global" (グローバル変数を参照)

getValue 関数の console.log(scope); は、変数宣言 var scope = "Local"; より前にありますが、ホイスティングによってグローバル変数の scope ではなく、ローカル変数の scope を参照しています。 ただし、変数宣言はホイスティングされますが、代入はされないために undefined が返っています。

ホイスティングとは、JavaScript の内部で、上記コードの getValue 関数を、下記のように書き換えているようなイメージです。

function getValue() {
    var scope;           // ホイスティングで関数宣言だけを先頭に移動
    console.log(scope);  // > undefined
    scope = "Local";     // 代入は元の位置にそのまま残る
    return scope;
}

Java などのブロックスコープがある言語では、できるだけスコープを小さくするために、必要になった場所で変数宣言をしますが、JavaScript にはブロックスコープがないので、関数内の先頭に変数宣言を持っていった方が、直感的なスコープと実際のスコープが一緒になるので分かりやすいです。

関数の入れ子

関数を入れ子にして定義すると、それぞれの関数ごとに独自のローカルスコープを持ちます。 内側と外側のローカル変数が同じ名前の場合は、内側のローカル変数が優先されます。

仮引数のスコープ

仮引数もローカル変数です。

var n = 0;

function incrementValue(n) {
    n++;
    return n;
}

console.log(incrementValue(n)); // > 1 (ローカル変数を参照)
console.log(n);                 // > 0 (グローバル変数を参照)

incrementValue 関数の仮引数は、グローバル変数と同じ名前ですが、incrementValue 関数内のみで有効で、グローバル変数に影響は与えません。 また、incrementValue 関数の外から変数 n を参照した場合は、グローバル変数を参照します。 スコープの外から、スコープの中の変数を参照することはできません。

同じ名前でも別の変数として扱われるというのはつまり、仮引数の名前を変えた下記のコードと同じことです。

var n = 0;

function incrementValue(m) {    // n を m にするのと同じ意味
    m++;
    return m;
}

console.log(incrementValue(n)); // > 1 (ローカル変数を参照)
console.log(n);                 // > 0 (グローバル変数を参照)

引数で値を渡すということは、渡した先の関数のローカル変数に、渡した値をコピーしているようなイメージです。 よって、コピー先(ローカル変数)をいくら変更しても、コピー元(グローバル変数)に影響は与えません。

仮引数で参照型を渡したときのスコープ

参照型とは、値そのものではなく、その値があるメモリ上の番地(アドレス)が格納される変数のことです。 関数 function、配列 array などは参照型なので、これらの変数には実際のオブジェクトが格納されているわけではなく、別の場所にあるオブジェクトのアドレス情報が格納されているだけです。

引数で参照型を渡すということは、オブジェクトそのものをコピーしているわけではなく、アドレス情報をコピーしているだけなので、元々のオブジェクトはひとつのままです。

var ary = [1, 2];

function addValue(ary) {
    ary.push(3);
    return ary;
}

console.log(addValue(ary)); // > [1, 2, 3] (ローカル変数を参照)
console.log(ary);           // > [1, 2, 3] (グローバル変数を参照)

仮引数で渡されたアドレスにあるオブジェクトの値を関数内で変更すると、グローバル変数からも同じオブジェクトを参照するので、どちらも値が変わっています。仮引数はローカル変数ですが、参照渡しの場合はスコープの話からは外れます。

まとめ

  • グローバル変数はプログラム全体から参照でき、ローカル変数はその関数の中でのみ参照できる。
  • ローカル変数は var の有無でスコープが変わるので、変数宣言の var は必ずつける。
  • ホイスティングによる混乱を避けるため、関数内の先頭に変数宣言をまとめて書く。
  • 仮引数もローカル変数のスコープだが、参照渡しの場合は関数外から参照するオブジェクトにも影響を与えるので気をつける。

長くなったので、スコープチェーンとクロージャについては分割して書きます。たぶん今日中に。

→ この話の続き : JavaScript のスコープチェーンとクロージャを理解する

参考にした本 Thx♡

プロトタイプチェーンをもっと理解する

Markdown が使いたくて、はてなダイアリーからはてなブログに引っ越してきました。ちょっと書いては放置してるブログの残骸があちこちに散らかっております。

前回プロトタイプについて勉強してみてふんわり分かった気になったけど、その中で 1 箇所よく分からなかったところがありました。

前回のおさらい

Javaと比較しつつ、JavaScriptのプロトタイプについて調べてみる - tacamy memo の「分からなかったところ」を参照。

前回分からなかったところ

プロトタイプには次のような特徴がありました。

javascript のオブジェクト指向とかプロトタイプとか - (゚∀゚)o彡 sasata299's blog の「プロトタイプの再定義とオーバーライド」の項で、7 行目でコンストラクタのプロトタイプのプロパティ値を変更しているのに、それを参照している redBox.color が blue にならないのが不思議だったのです。でも、その点については、次のように理解して解決しました。

  • 7 行目は、プロパティの値を変更したのではなく、オブジェクトを再定義し直したのではないか
  • 再定義することで、名前は同じ Box.prototype.color だけど、それ以前とは別のオブジェクトとなってしまったのではないか
  • その根拠は、13 行目では、プロパティの値を yellow に変更しているだけなので、この場合はインスタンスである blueBox.color も yellow に変わっているから

新たな疑問

ここで、また新たな疑問が発生しました。13 行目でコンストラクタのプロトタイプのプロパティ値を変更しているのに、同じコンストラクタから生成した blueBox.color は連動して変わるのに、redBox.color は値が変わらないのはなぜだろう、と。

今回勉強したこと

変数には、基本型と参照型がある

数値や真偽値などは基本型で、オブジェクトは参照型で、それぞれ次のような違いがあります。

項目 基本型 参照型
型の分類 number, string, boolean, undefined, null, NaN object, function, array
変数に格納されるもの 値そのものを直接格納 オブジェクト本体が保存されているメモリ上のアドレスを格納
変数のコピー 値そのものがコピーされ、別々の要素ができる アドレスがコピーがされ、オブジェクトそのものはひとつのまま

ここで大事なのは、参照型のオブジェクトをコピーしたとき、オブジェクト自身が複製されるのではなく、オブジェクトへのアドレスがコピーされるので、実際のオブジェクトはひとつのまま増えないということです。

コンピュータのメモリ上で、それぞれ値がどのように保存されているかのイメージを図にしてみました。

基本型と参照型の違い

JavaScript のオブジェクトのコピーは参照のコピー

オブジェクトのコピーは参照(アドレス)のコピーということは、どれだけオブジェクトをコピーしても、オブジェクトの実体はひとつだけということです。

ということは、コピーしたオブジェクトの片方の値を変更すると、もう片方も連動して変わります。
参照しているオブジェクトの実体は同じものなので、当然の結果です。

次のコードを使って、具体的に理解を深めます。

// sample1
var foo = { color: 'red' }; // foo オブジェクトを生成
var bar = foo;              // foo の実体への参照を bar にコピー

bar.color = 'blue';         // foo オブジェクトの color プロパティの値を変更
console.log(foo.color);     // blue
console.log(bar.color);     // blue

// sample2
var foo = { color: 'red' }; // foo オブジェクトを生成
var bar = foo;              // foo の実体への参照を bar にコピー

bar = { color: 'blue' };    // 新しい別のオブジェクトを定義
console.log(foo.color);     // red
console.log(bar.color);     // blue

sample1 と sample2 のどちらも、var bar = foo; の時点では、foo と bar どちらも同じ foo オブジェクトへの参照が格納されています。

sample1 の bar.color = 'blue'; は、参照先の foo オブジェクトが持っている color プロパティの値を変更しました。 foo と bar はどちらも同じ foo オブジェクトへの参照なので、そのオブジェクト実体のプロパティの値が変更されれば、連動して値が変わります。 これは、今までの流れに沿って考えると、違和感なく受け入れられます。

次に、sample2 の bar = { color: 'blue' }; についてです。 bar に新しく別のオブジェクトを定義し直しているので、bar に格納されていた foo オブジェクト実体への参照は、別のオブジェクトへの参照に置き換わってしまいました。 この時点で foo と bar は同じオブジェクトの参照ではなく、別々のオブジェクトの参照となってしまったため、bar の変更が foo と連動しなくなりました。 よって、foo.color と bar.color の結果も連動しません。

prototype と __proto__ の違い

プロトタイプチェーンについてもう一度おさらいします。

function Super() {}
Super.prototype.a = 10;

var Sub = function() {}
Sub.prototype = new Super();

var instance = new Sub();
console.log(instance.a); // 10

上記のコードでは、次のことをしています。

  1. スーパークラス Super のプロトタイプに a プロパティを定義
  2. サブクラス Sub のプロトタイプで Super のインスタンスを格納
  3. 変数 instance に Sub のインスタンスを作成して格納

instance から暗黙の参照によって、Super のプロトタイプにある a プロパティの値を表示する流れは次のとおりです。

  1. instance 自身に a プロパティはあるか探す
  2. 見つからないので、Sub の prototype を探す
  3. 見つからないので、Super の prototype を探す
  4. 見つかったのでここで Super.prototype.a の値を返す

ところでこの暗黙の参照、どうやって実現しているかよく考えてみると疑問です。自分の継承元の prototype をどうやって探してるのかなと。

ここで __proto__ プロパティの出番です。__proto__ プロパティには、次のような特徴があります。

  • __proto__ はすべてのオブジェクトが持つプロパティ
  • オブジェクトに対象のプロパティが見つからなければ、__proto__ の指すオブジェクトを探す
  • __proto__ の値が null になったらそこで探索終了
  • Object.prototype (すべてのオブジェクトの元) の __proto__ には null が入っている
  • new したときに、コンストラクタの prototype への参照(アドレス)を格納する

親の prototype がある場所(アドレス)を、自動でそれぞれのオブジェクトの __proto__ に代入してくれているイメージです。次のコードで __proto__ へのアドレスの代入のイメージを書いてみましたが、実際にこれを自分で書く必要はありません。

function Super() {}
Super.prototype.a = 10;
Super.prototype.__proto__ = Object.prototype; // Object.prototype のアドレス情報

var Sub = function() {}
Sub.prototype = new Super();
Sub.prototype.__proto__ = Super.prototype;    // Super.prototype のアドレス情報

var instance = new Sub();
instance.__proto__ = Sub.prototype;            // Sub.prototype のアドレス情報

この __proto__ が持っているアドレス情報をどんどん遡っていくことによって、プロトタイプチェーンが実現できます。

「新たな疑問」を振り返る

ここまで勉強したので、javascript のオブジェクト指向とかプロトタイプとか - (゚∀゚)o彡 sasata299's blog の「プロトタイプの再定義とオーバーライド」の項をもう一度見てみます。

13 行目でコンストラクタのプロトタイプのプロパティ値を変更しているのに、同じコンストラクタから生成した blueBox.color は連動して変わるのに、redBox.color は値が変わらない理由を考えてみます。

  1. 4 行目で var redBox = new Box(); をした時点では、redBox.__proto__ には、Box.prototype のアドレス情報が入る(仮に1000番地とする)
  2. 7 行目の Box.prototype = { color: "blue" }; でオブジェクトが再定義されたため、Box.prototype の値(参照)が、別のオブジェクトへの参照に変わってしまった(仮に2000番地とする)
  3. この時点で、redBox.__proto__ は、Box.prototype の番地を指していないことになる(1000番地のまま)
  4. 9 行目の var blueBox = new Box(); によって、blueBox.__proto__ に Box.prototype のアドレス情報が入る(2000番地)

この時点でそれぞれ、次の番地となっています。

  • Box.prototype => 2000番地
  • blueBox.__proto__ => 2000番地
  • redBox.__proto__ => 1000番地

redBox.__proto__ は Box.prototype のプロトタイプチェーンから外れてしまいました。 そのため、redBox から Box のプロトタイプを見に行くことができないため、コンストラクタのプロトタイプの変更が反映されなくなってしまったのです。

やっと意味が分かった!!!長かった・・。

プロトタイプチェーンでの継承の意義

プロトタイプチェーンによって、スーパークラス(親)からサブクラス(子)へ、プロパティやメソッドを継承できます。

こういう例を出すと実務とかけ離れるので、あんまりよくないかもですが、
動物(親) > 犬(親 / 子) > チワワ(子) > ゆに(インスタンス
みたいなイメージでしょうか。

継承には、次のようなメリットがあります。

  • コードの再利用
    • 新しいオブジェクトの定義にかかる手間を削減できる
    • 継承しなければ、すべてのプロパティを一から定義し直さなければならない
  • 継承関係によって複数のオブジェクトをグループ化できる
    • プロパティの追加やメソッドのオーバーライドで機能を拡張
  • メモリの節約
    • 共通部分はひとつのプロパティを見に行くので、メモリ使用量を節約できる

大規模開発でこそ威力を発揮してくれそうですし、設計力が問われそうですね・・(ヽ'ω`)

まとめ

プロトタイプチェーンを使うと、JavaScript でも、Java のクラスベースのオブジェクト指向的なことができるっぽい。 でも Java とは考え方も書き方も違うところがいっぱいあるから気をつけましょう。

参考サイト & 本 Thx♡