JavaScript中級者への道【3. 関数スコープ】
JavaScriptのつまづきやすそうなところ
- 関数はオブジェクトの一種
- 4種類のthis
- 関数スコープ ← いまここ
- 非同期関数
- コールバック関数
- クロージャ
- プロトタイプ継承
スコープについて
プログラミングでのスコープとは、ある変数や関数が特定の名前で参照される範囲のこと。ある範囲の外に置いた変数等は、通常、その名前だけでは参照できない。このときこれらの変数はスコープ外である、「見えない」といわれる。
変数のアクセス権や生存期間といった話になります。
グローバルスコープ
「関数スコープ」と題しましたが、その前にグローバルスコープについて見てみましょう。
// グローバルな変数
var globalVar = 'globalVar';
// グローバルな関数
function globalfunc () {
console.log('This is ' + globalVar);
}
// globalVarはどこからでも誰からでも変更出来る
globalFunc(); // => 'This is globalVar'
globalVar = 'new globalVar'
globalFunc(); // => 'This is new globalVar'
要するにトップレベルの箇所に変数や関数を置いてしまうと
「グローバルオブジェクト」への代入となってしまい、
プログラムのどこからでもアクセス(呼び出しや変更)が出来てしまいます。
また、勘違いしがちなのが、別のjsファイルに書いたからといって、
別々のスコープが生まれる訳ではないということです。
簡略化の為に、ソース例としてこのような書き方をすることがありますが、
本来的にはこのような書き方は推奨されるべきではありません。
じゃあどうするんだ。
関数スコープ
そこで関数スコープの出番です。
functionの中に置かれた変数や関数は、その内部でしか変更、参照出来ません。
// グローバルな関数
function globalFunc () {
// ローカルな変数
var localVal = 'localVar';
// ローカルな関数
function localFunc() {
console.log('This is' + localVal);
}
// グローバルな関数を呼び出すとローカルな関数が呼ばれる
localFunc();
}
// localVarもlocalFuncも外側のスコープからは参照出来ない
globalFunc(); // => 'This is localVar'
console.log(globalFunc.localVar); // => undefined
console.log(globalFunc.localFunc); // => undefined
// globalFuncのプロパティにlocalVarをlocaFuncを追加することは出来るが、
// globalFuncの呼び出し結果には影響を及ぼさない
globalFunc.localVar = 'propVar';
globalFunc.localFunc = function () { console.log(this.localVar) };
globalFunc(); // => 'This is localVar'
// グローバルにlocalVarを置いても、globalFuncの結果には影響しない
var localVar = 'lier globalVar';
globalFunc(); // => 'This is localVar'
単純にfunctionの中に変数や関数を置いただけですが、関数スコープを使うことで以下のメリットがあります。
- 変更に強いモジュールを作り出すことが出来る
- グローバル変数を多用する開発者の作業に影響を与えない
- 新しいモジュールを追加する時に、変数の名前衝突を考える時間が減る
注意点すべきルールとしては、変数の場合は必ず「var」を付けることです。
varを付け忘れると、その変数はグローバルスコープ行きとなります。
function globalFunc () {
// 関数内で変数にvarを付け忘れた場合、グローバルスコープ行き
forgotAddVar = 'forgotAddVar';
// ありがちなのが、変数宣言時にvarを付けているがタイポしているケース。グローバルスコープ行き
var type = 'type';
function typoFunction (){
/*
実際にはここに長い処理があるとする
*/
typo = 'typo'
}
typoFunction();
}
// 関数を呼び出すと、グローバルスコープにforgotAddVar、typoが追加される
globalFunc();
console.log(forgotAddVar); // => 'forgotAddVar'
console.log(typo); // => 'typo'
前回紹介した、コンストラクタをnewを付けずに呼び出した場合と似ており、
意図しないグローバル汚染が起きてしまうパターンです。
タイポの例はありがちで、そこでエラーが出れば直して問題無いのですが、
「タイポしたまま動く = 知らぬ間にグローバル変数に依存した処理になっている」場合、
気付かぬうちにバグの温床となってしまう可能性が考えられます。
ここでは深く取り上げませんが、IDEの補完機能や"use strict"、lint系のツールで防止するようにしましょう。
ネストした関数のスコープ
function outerFunc () {
// 外側の関数スコープに定義された変数
var outerVal = 'outerVal';
function innerFunc () {
// 内側の関数スコープに定義された変数
var innerVal = 'innerVal';
// 内側からはアクセス出来る
console.log('This is ' + outerVal);
}
innerFunc();
// 外側からはアクセス出来ないのでReferenceErrorとなる
console.log(innerVal);
}
outerFunc(); // => 'This is innerVal'
// => 'This is outerVal'
// => ReferenceError
ここで覚えておくべきは、下記の2点です。
- 関数スコープは各function毎に独立して発生すること
- 内側の関数スコープから外側の関数スコープにアクセス出来ること
即時関数
先ほどの例では、関数スコープによってglobalFunc()の内部で宣言した変数や関数に対して
外部からのアクセスが出来ないという例でした。
ですが、globalFunc()自身はグローバルスコープに追加していました。
(function () {
var instantVar = 'instantVar';
function instantFunc () {
console.log('This is ' + instantVar)
}
instantFunc();
})(); // => 'This is instantVar'
// 外側からはアクセス出来ない
console.log(instantVar); // => ReferenceError
即時関数を使えば、誰にも影響を与えずに変数や関数の宣言を自由に行い、即座に実行することが出来ます。
何度見ても構文としては奇妙なものを感じますが、絶対不可侵領域を作り出します。
使いどころとしては、1度しか行わない処理を実行させたり、関数スコープを無理やり作ったり、
クロージャを作る際のファクトリーのような役割を行わせる、などがあります。
ブロックスコープ
if文やfor文の内部におけるスコープの話になります。JavaScriptにはありません。
JavaScriptは一般的な言語(JavaやPHPとか)と文法が似ており、とっつきやすさがあるのですが、
他の言語とは違い、ブロックレベルでのスコープが無いので注意が必要です。
if (true) {
var hoge = 'hoge';
}
for (var i = 0; i < 3; i++) {
var fuga = 'fuga';
}
// ブロックスコープが無いので、アクセス出来てしまう
console.log(hoge); // => 'hoge'
console.log(i); // => 3
console.log(fuga) // => 'fuga'
ちなみに上記はグローバル変数に追加されてしまっているパターンです。
functionの内部に移した場合、関数スコープに対してhogeやfugaが追加されることになります。
いずれにしても、if文やfor文内のみにおけるブロックレベルのスコープは発生しません。
例外として、例外処理のcatch節のみブロックスコープを持ちます。
参考:CMAScript のレキシカル環境 ~catch 節のスコープのお話~
オブジェクトとの違いについて考える
用途やレイヤーが違うので比較するべきでは無いかもしれませんが、
オブジェクトと関数スコープの違いについても見てみます。
var myObj = {
prop : 'prop',
method : function () {
console.log(this.prop);
}
};
function myFunc () {
var prop = 'prop';
console.log(prop);
}
// 同じような働きをする
myObj.method(); // => 'prop'
myFunc(); // => 'prop'
// propにアクセスしてみると、オブジェクトの場合はアクセス出来る
console.log(myObj.prop); // => 'prop'
console.log(myFunc.prop); // => undefined
当たり前の結果ではあります。ただ、1つ重要なことを言えば、
オブジェクトのプロパティをprivateにすることが出来ないということです。
propに対して外部からのアクセスを防ぎたい場合、
単純な解決策としてはpropをmethod()の内部に入れることが考えられます。
var myObj = {
method : function () {
// propはmethodの関数スコープのみ有効
var prop = 'prop';
console.log(prop);
}
}
console.log(myObj.method.prop); // => undefined
これでpropは外部からの変更にさらされることは無くなりました。
しかし、状態を持つオブジェクトについて次のようなパターンを考えてみてください。
var counter = {
count : 0,
increment : function () { this.count++; },
decrement : function () { this.count--; },
getCount : function () { console.log(this.count); }
};
// 意図されている使われ方
counter.increment();
counter.getCount(); // => 1
counter.decrement();
counter.getCount(); // => 0
// 意図されていない使われ方
counter.count = 100;
console.log(counter.count);
このように、オブジェクトに状態を持たせたい(複数のメソッドで共有したい場合)などは、
単純に関数スコープに入れることが出来ない為、カプセル化が出来ません。
上記はオブジェクトリテラルで実装したパターンですが、コンストラクタを使っても同じことです。
JavaやPHPなどのクラスベースの言語ではpublicやprivateといったキーワードで
オブジェクトのカプセル化が可能でしたが、JavaScriptにはそのようなキーワードはありません。
オブジェクトに状態を持たせたいが、インターフェースは制限したい。
じゃあどうするのか、ということでクロージャを使う必要が出てくるのです。
クロージャについては、別の機会でやります。
まとめ
・トップレベルのソースに変数や関数を宣言すると、グローバルスコープに追加される
・外部からのアクセスを防ぐには、functionの中に変数や関数を宣言すること(関数スコープ)
・関数がネストしている場合、内側の関数スコープから外側の関数スコープはアクセス出来る
・if文やfor文などのブロックは独立したスコープを持たない
・単純な実装では、オブジェクトは外部へのインターフェースを制限出来ない
あとがき
ES5ではObject.defineProperty()というオブジェクトに書き込み不可の設定などを行う方法が追加されたり、
ES6でletというキーワードでローカル変数が作れたりなど、スコープは色々と見直されている箇所なので、
当然ながら今回取り上げたことは全てでは無いことを今更ながら断っておきます。
最後に、クラスベースでは基本的に「1ファイル=1クラス」という方式が成り立っているので、
その感覚のままJavaScriptを始めると無事に死ねます。
トップレベルの箇所に変数の初期化や関数宣言を行うのはやめましょう(泣)
以上。