[javascript]スコープとクロージャについて勉強したのでまとめます
- 2015年06月19日
- コーディング系
- Javascript
今回はスコープとクロージャにスポットライトを当てて勉強をしてみました。正直難しかったです。完璧に理解できているかというと・・・そうでもないかもですが、一応まとめてみたのでご紹介しますね。
Contents
Javascriptのスコープは基本的には2種類
スコープとは、変数や関数の有効範囲のことです。言い換えるとある変数や関数が見える範囲と言えます。そして、Javascriptのスコープにはグローバルスコープとローカルスコープの2種類あります。
グローバルスコープ
関数の外(トップレベル)で宣言した変数のこと
プログラム全体から参照できる
たとえ関数内であってもvarを使わずに変数を宣言するとグローバルスコープになってしまいます。
なので関数内で変数を定義する際は常にvarを使い、意図せずグローバルスコープにならないようにします。
ローカルスコープ
関数内でvarを使って宣言された変数に適用される
関数の中で宣言した変数, 関数の仮引数
ローカルスコープはその関数の中でのみ参照できます。
スコープの役割って?
では、スコープの役割とはなんでしょう?
・同じ名前の変数が、意図せず競合することを避ける。
・スコープが違う場合、同じ名前の変数であっても別物として扱われる。
・ローカル変数の記憶領域は、関数の実行が終わり次第、破棄される。
・グローバル変数はプログラムが終了するまで記憶領域を確保するため、関数内でしか必要ない変数までグローバル変数にした場合、無駄にメモリを消費することになる。
javascriptは関数を使ってこのスコープを管理しています。なので、関数の内部で宣言された変数は、その関数内でのみ使用ができます。これがローカルスコープですね。反対にグローバル変数は関数の外に存在していて、どんなとこからでも参照できてしまいます。
グローバル変数はどれも同じ場所に存在している状態なので、変数の名前が衝突してしまう危険性がありますよね。
自分一人で開発していれば危険性は低いですけど、チームで開発している場合は衝突する危険性は高そうです。それにライブラリを使う場合もそうですよね。もしそのライブラリのグローバル変数にたまたま自分たちが開発しているjsに同じ名前の変数がある場合も考えられます。
なので、かならず関数内で使う変数はvarで宣言をし極力グローバル変数は増やさないようにするのがいいですね。
スコープチェーン
javascriptが変数に関連付けられている値を検索する際たどる順番のようなものです。スクリプト内に定義された変数がどの値を参照しているかって大事ですよね。
ここでのポイントは
・内側から外側に向かって順番に検索する
・スコープチェーンの検索は最初に発見した値を返す
内側から外側に向かって順番に検索する
コードで見てみると分かりやすいと思います。
var text3 = 'テキスト3'; var func1 = function(){ var text2 = 'テキスト2'; var func2 = function(){ var text1 = 'テキスト1'; console.log(text1); console.log(text2); console.log(text3); }(); }; func1();
一番内側のfunc2関数から最終的に一番外側のグローバル変数のtext3まで検索しにいっているのがわかると思います。なので、値が関数内に存在しなければグローバル変数までさかのぼります。
スコープチェーンの検索は最初に発見した値を返す
var func1 = function(){ var text = 'テキスト2'; var func2 = function(){ var text = 'テキスト1'; console.log(text); }(); }; func1();
関数func1、func2どちらも同じ変数名でtextがありますね。この場合func2内でtextを使う場合は「テキスト1」と表示されます。これはスコープチェーンの検索は最初に発見した値を返すっていうのがわかりますね。さらに一旦目的の値が見つかるとスコープチェーンはそれ以上の検索をしません。
クロージャとは
クロージャについても勉強をしました。正直、ちゃんとわかっているのか不安かも。
クロージャとは、ローカル変数の状態を保持できる関数のことです。 通常ローカル変数は関数の呼び出しが終わると破棄されますが、クロージャはローカル変数を参照し続けられます。
??全然なんだかよくわからない・・・。
んで、クロージャのサンプルとしてよくあるのがこれ
function countUp(){ var x = 1; return function (){ console.log(x); x = x + 1; }; } var f = countUp(); f(); // 1 f(); // 2 f(); // 3
なんとなくはわかる。
console.logで1を表示→「1」を覚えておいて「2」を表示→繰り返し
ってかんじですよね。「ローカル変数の状態を保持できる関数のこと」っていうのはできているのはわかりました。
でも、なんでそんなことができるの?って腑に落ちませんでした。
ただ、JavaScriptにおけるメモリの浪費を避けるコーディングという記事を見つけて理解が深まった気がします。
というのも、スコープチェーンとアクティベーションオブジェクトについて理解できればクロージャの仕組みもわかったって感じです。
アクティベーションオブジェクトってなに?
アクティベーションオブジェクトとは関数のコールが発生した際に、自動的に生成されるオブジェクトです。アクティベーションオブジェクトには、引数、ローカル変数だけでなく、argumentsオブジェクト、thisが格納されます。
// hoge()実行時に生成されるアクティベーションオブジェクト // { // arguments:, // this:, // foo:, // bar:, // baz:, // qux: // } function hoge(foo, bar) { var baz, qux; }
参照:JavaScriptにおけるメモリの浪費を避けるコーディング
実は、スコープチェーンでたどって検索するときもこのアクティベーションオブジェクトをたどっています。このアクティベーションオブジェクトですが、通常は関数の実行終了時にメモリ上から解放されます。ただ、対象の関数がメモリ上に存在している間は保持され続けるという特徴があります。
上記のサンプルでいうと
var f = countUp();
でcountUp()関数はグローバル変数fに代入されているためcountUp()内部のアクティベーションオブジェクトは保持され続けます。
この性質を利用して、スコープチェーンによりローカル変数の値を参照し続けるデータ構造を、クロージャと呼びます。
なるほど!!なんで参照し続けられるのかっていうのはアクティベーションオブジェクトがどんな性質なのかがわかればよかったんですね。
ここで大事になってくるのがメモリーリークっていうやつです。
クロージャはあまり使わない方がいいのかもしれない
クロージャはすごく便利な仕組みなんですけど、メモリを大量に使ってしまっていたり、別のページに遷移してもメモリが開放されない(メモリーリーク)を引き起こしてしまう危険性があるようです。
え?じゃあ、どうすればいいのさ
解決策についてもJavaScriptにおけるメモリの浪費を避けるコーディングでは解説してくれていました。
クロージャを使わない方がいいケース
var hoge = function(foo) { this.bar = foo; this.fuga = function() { return this.bar + 'baz'; } };
上記のコードは、関数がコールされる度にスコープチェーンが生成され、メモリを浪費します。クロージャにする利点もありません。このような場合は、以下のようにprototypeを利用してメンバを定義します。
var hoge = function(foo) { this.bar = foo; }; hoge.prototype = { fuga: function() { return this.bar + 'baz'; } };
なるほどprototypeを使えばいいのか〜
でも、なんでそのほうがいいのかいまいちわかってないな・・・。
おわりに
今回はスコープチェーン、クロージャについて勉強したことをまとめてみました。コードを書くときにスコープチェーンの理解があるのと無いのとではだいぶ変わってきそうです。
クロージャについてもどうゆう仕組みなのか理解することができたのはよかったです。ただ、prototypeの理解がまだ足りないので、最後の方はちょっとうやむやになってしまいました。
ってことで、つぎはprototypeの勉強をしよう。
イチマルニデザインブログをフォローしよう
イチマルニデザインブログではTwitterアカウントでWebに関する情報をつぶやいています。フォローすることで最新情報をすぐに受け取ることができます
今すぐフォローしよう!
@102_designさんをフォロー