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, |
object, |
変数に格納されるもの | 値そのものを直接格納 | オブジェクト本体が保存されているメモリ上のアドレスを格納 |
変数のコピー | 値そのものがコピーされ、別々の要素ができる | アドレスがコピーがされ、オブジェクトそのものはひとつのまま |
ここで大事なのは、参照型のオブジェクトをコピーしたとき、オブジェクト自身が複製されるのではなく、オブジェクトへのアドレスがコピーされるので、実際のオブジェクトはひとつのまま増えないということです。
コンピュータのメモリ上で、それぞれ値がどのように保存されているかのイメージを図にしてみました。
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
上記のコードでは、次のことをしています。
- スーパークラス Super のプロトタイプに a プロパティを定義
- サブクラス Sub のプロトタイプで Super のインスタンスを格納
- 変数 instance に Sub のインスタンスを作成して格納
instance から暗黙の参照によって、Super のプロトタイプにある a プロパティの値を表示する流れは次のとおりです。
- instance 自身に a プロパティはあるか探す
- 見つからないので、Sub の prototype を探す
- 見つからないので、Super の prototype を探す
- 見つかったのでここで 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 は値が変わらない理由を考えてみます。
- 4 行目で
var redBox = new Box();
をした時点では、redBox.__proto__ には、Box.prototype のアドレス情報が入る(仮に1000番地とする) - 7 行目の
Box.prototype = { color: "blue" };
でオブジェクトが再定義されたため、Box.prototype の値(参照)が、別のオブジェクトへの参照に変わってしまった(仮に2000番地とする) - この時点で、redBox.__proto__ は、Box.prototype の番地を指していないことになる(1000番地のまま)
- 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 とは考え方も書き方も違うところがいっぱいあるから気をつけましょう。