スコープとは
実行中のコードから値と式が参照できる有効範囲のこと。変数、定数、引数が「どこで値を参照したり変えたりできるのか」有効な範囲を決めるもの。
同じ名前の変数が意図せず競合を避けるためグローバルスコープとローカルスコープを使い分け、実行範囲をあらかじめ決め無駄なメモリ消費を避ける。
JavaScriptの場合
- グローバルスコープ
- ローカルスコープ
- 関数スコープ
- ブロックスコープ
- モジュールスコープ
という5つのスコープが存在している。
グローバルスコープ
グローバルスコープ(大域スコープ)
JavaScriptのプログラムが実行が開始し、関数を呼び出す前にはグローバルスコープで実行される。グローバルスコープで宣言したものはすべてプログラムのすべてのスコープで利用可能になる。
グローバルスコープで定義された変数は「グローバル変数」あるいは「大域変数」と呼ばれる。
なお、グローバルスコープを使わざるを得ないが、グローバルスコープに属する変数はプログラムから参照され続けるため、不要なメモリ領域を確保し続けてしまうので濫用は極力避けたい。
Windowオブジェクトとは、JavaScriptコードが動作しているWebブラウザのウィンドウまたはフレームを表す。
関数スコープとブロックスコープ
関数スコープ
関数に囲まれた波括弧で囲まれた範囲を関数スコープと呼ぶ
書き方としては下記のようになり、bの値が取得できる(この場合は0)。
function a(){
let b = 0;
console.log(b);
}
a();
main.js:4 Uncaught ReferenceError: b is not defined at
というエラーが出る。bの変数が関数スコープの中でのみ取得可能なためエラーとなる。
ブロックスコープ
ブロックの言葉の定義は波括弧のことで、その波括弧で囲まれた範囲のことをブロックスコープと呼ぶ。関数スコープとの違いはfunctionで関数宣言しているか否か。
また、if文、for文などで生成される。
ES6からサポートされている。JavaScriptの中で、ブロックスコープを使う場合、letもしくはconstを仕様することが条件。(var非推奨)
書き方としては下記のようになり、cの値が取得できる(この場合は1)。
{
let c = 1;
console.log(c);
}
また、関数スコープと同様、console.log(c)を外に出すとエラーが発生する。
{
let c = 1;
}
console.log(c);
Uncaught ReferenceError: c is not defined
at
constの場合も同じ挙動になる。
ただし、varを使用するとブロックスコープを無視され、エラーが出ずに値取得ができてしまう(ホイスティング)。意図しない挙動をとってしまうため非推奨となっている。
{
var c = 2; //非推奨
}
console.log(c); //ブロックスコープが無視され、表示できてしまう。
また気をつけておきたい点として、ブロックスコープの中で関数宣言をしてしまうとブロックスコープを無視されてしまうことも注意が必要。
{
function d(){//意図しない挙動になる
let e = 2;
console.log(e);
}
}
d(); //ブロックスコープが無視され、表示できてしまう。
なお、ブロックスコープ内で関数を使う場合は下記のように変数に代入する形となる。
関数の呼び出しをブロックスコープ外で行うとエラーが生じる。
{
const f = function(){//またはlet
let g = 3;
console.log(g);
}
f();
}
レキシカルスコープ(静的スコープ)
ソースコードの「どこ」に「何」を書いているのか。つまりはコードを書く「場所」によって参照できる変数が変わるスコープのこと。またコードを記述した時点で決定するため「静的スコープ」とも呼ばれる。
また、2つの意味合いで使われることがある。
- 実行中のコードから見た外部スコープ(内側のスコープから見た参照可能な外側のスコープのこと)のこと
- 前述の通り、どのようにしてスコープを決定するかのプログラミング言語の仕様のこと。もしくは静的スコープ。
下記のような書き方をするとエラーが出る。
let a = 0;
function fn1(){
let b = 1;
}
fn1();
function fn2(){
let c = 2;
console.log(b); //スコープ外なので参照できない。変数bと同じ階層にある場合は参照可能。
}
fn2();
Uncaught ReferenceError: b is not defined
関数スコープの中に関数bが入っており、fn2内からは参照できない状態。
そのため、参照する場合は下記のような書き方をする。
let a = 0;
function fn1(){
let b = 1;
function fn2(){
let c = 2;
console.log(b);
}
fn2();
}
fn1();
この場合だと、変数bは外部スコープの中にあるので参照が可能となる。
スコープチェーン
スコープが複数階層連なっている状態。レキシカルスコープのように外部スコープがある場合(スコープの中にスコープがある場合)のこと。
スコープチェーンの場合は、内側の自スコープから変数を探しに行き、外側に同じ変数を探しにいく。見つかった段階で変数を取得する。
グローバルスコープとスクリプトスコープがある場合
グローバルスコープとスクリプトスコープがある場合は、変数がある方を取得してくる。グローバルスコープがない場合はスクリプトスコープを取得してくる。
クロージャー
レキシカルスコープを関数の変数が使用している状態のこと。
クロージャーを使うメリットとしてプライベートプロパティ及びメソッドの実現と保持とユーザーがカスタマイズ可能な「高階関数」として使用可能ということ。クロージャーを使用する必要がある場合はほぼ前者確率が高い。
function fn1(){
let b = 1;
function fn2(){
console.log(b);
}
fn2();
}
fn1();
内側に定義されている関数(fn2)からレキシカルスコープの変数参照(変数b)を保持している場合、クロージャーという。
注意すべき点
クロージャーはJavaScript野中でも便利でよく見る機能だが、メモリリークの温床になる原因となる。クロージャーを使う際のメモリリークの原因として
- 変数への参照保持
- 循環参照の発生
を念頭に置きながら使用したほうが良さそう。
プライベート変数
グローバル変数にするとどこからも書き換えができてしまうため、関数内に変数を置き関数内でしか使用できなくする方法。
function incrementFactory(){//Factoryは何かを生成する関数につける場合が多い
let num = 0;
function increment(){
num += 1;
console.log(num);
}
return increment; //incrementFactoryに返却
}
const increment = incrementFactory(); //incrementFactoryを実行
increment(); //定数は関数が返ってきているため、丸括弧が必要
increment();
increment();
上記のように、親の関数を実行(この場合はincrementFactoryを実行)するとnumの宣言が行われ、ネストされた関数(この場合はincrement)の宣言が行われ、increment関数を返すという流れになっている。
また最後に定数に代入しているため、定数には丸括弧が必要。
こうすることで関数内からしかアクセスできなくなる。
動的な関数の生成
function addNumberFactory(num){
function addNumber(value){
return num + value;
}
return addNumber;
}
const add5 = addNumberFactory(5);
const add10 = addNumberFactory(10);
const result = add10(10);
console.log(result);
親の関数にもネストされた関数にも引数を持つことで、親の関数に渡す値によって生成される関数が変わるため、動的な関数の生成と呼ぶことができる。
親関数が宣言される度にネストされた関数が宣言されることになる。宣言された際にレキシカルスコープの変数(num)が内部で使用されているため、値が保持された状態でネストされている関数が宣言される状態になる。そのため、親の関数が持つ値が違えば、その分ネストされた関数が呼び出されため、独自の関数を複数作成することができるようになる。
即時関数(即時実行関数/IIFE)
関数定義と同時に一度だけ「即時に」実行される関数。
また、ブロック内からホイスティングやスコープ汚染を防ぐことが可能。
名前付き関数は再利用を前提とした一覧の処理を定義し、即時関数は一時的な処理を新たなスコープで包み込むことを得意とするため、処理の再利用をしない場合は即時関数を使い、スコープの外側へ影響を与えることを防ぐようにする。
let result = (function(仮引数){
return 戻り値;
})(実引数);
という書き方が一般的。なお、JavaScriptコード品質チェックツールJSLintが推奨している記述方法は、下記となる。どちらも書き方としては問題ない。
let result = (function(仮引数){
return 戻り値;
}(実引数));
関数宣言を丸括弧で囲み、その後ろに実行する際の引数を渡す。
実引数で渡されたものは仮引数に渡され、関数内で使用される。
また、戻り値は呼び出し元の変数に戻される。
一般的な関数の書き方は下記のようになるが、
function a(){
let a = 0;
console.log(a);
}
a();
即時関数で書くと下記となる。
(function(){
let b = 1;
console.log(b);
})();
即時関数は関数をグループ化してエラーを発生させないようにしており、即時関数を使う理由はスコープ汚染を防ぐためにある。
また、関数内にreturn文がある場合、returnに続く値が変数に代入される。
let c = (function(){
console.log('hoge');//hoge
return 0;
})();
console.log(c);//0
なお、引数を渡すと無名関数で引数を定義すると、渡した引数が表示される。
let d = (function(e){
console.log('hoge:' + e);
})(10);//hoge10
即時関数を使うシチュエーションは関数の中でしか使えない変数や関数と関数の外でしか使えない変数や関数を区別する際に使われる。
下記のような記述をするとスコープ内とスコープ外の出しわけができるようになる。
let f = (function(){
console.log('hoge');
let privateVal = 0; //関数の中でしか使えない
let publicVal = 10; //外でも呼び出せるようにreturnにオブジェクトリテラルとして記述
function privateFn(){
console.log('privateFn');
}
function publicFn(){
console.log('publicFn');
}
return {
publicVal,//変数名とプロパティの名前が一致する場合はコロンは不要
publicFn
};
})();
f.publicFn();//外から呼び出せる
console.log(f.publicVal);
なお、クロージャーに基づく実装に関して、publicFnの中でprivateValの値を使う場合・・・
publicFnが定義されているレキシカルスコープにprivateValが存在し、privateVal変数は即時関数が実行された一度だけ初期化されるためそれ以降はpublicFnが呼び出された時点で実行されるようになる。
let g = (function(){
console.log('called');
let privateVal = 0; //即時関数が実行された一度だけ初期化
let publicVal = 10; //外でも呼び出せるようにreturnにオブジェクトリテラルとして記述
function privateFn(){
console.log('privateFn is called');
}
function publicFn(){
console.log('publicFn is called:' + privateVal++);
}
return {
publicVal,//変数名とプロパティの名前が一致する場合はコロンは不要
publicFn
};
})();
g.publicFn();//呼び出すたび数値が+1される
g.publicFn();
g.publicFn();
g.publicFn();
このような形でprivateFnとprivateValは関数内でしか使えないようになり、即時関数内でのみ扱うものとなる。
スコープには2種類ある
最後に、スコープには大きく分けて2種類あります。文末に書いた理由として言語での変数参照の仕様であるため混乱しないように最後に記述。
- レキシカルスコープ
- ダイナミックスコープ
レキシカルスコープを採用している言語
- Ruby
- Java
- JavaScript
- Python
など多くのメジャーな言語で採用されている。
ダイナミックスコープを採用している言語
ダイナミックスコープを採用している言語はPerlなど。
JavaScriptをダイナミックスコープをもし採用した場合は下記のようになる。(結果は意図した状態ではにはならない)
let h = 0;
function i(){
console.log(h);
}
function j(){
let h = 1;
i();
}
i(); //10
j(); //10
レキシカルスコープ(静的スコープ)では関数が定義された時点でスコープが決定し保持されるため、上記のように関数jに新たに変数hを定義されていても関数iで定義された時点で決定しているので参照していたh(この場合は0)が出力される。
前述のようにレキシカルスコープ(静的スコープ)は関数が定義された時点でスコープが決定し保持されるが動的スコープは実行時の親子関係の子側(呼び出された側)から親側(呼び出し側)のスコープを参照できる。
もし、JavaScriptがダイナミックスコープを採用していた場合、関数で出力される変数hはグローバル変数hではなく関数jで定義されているローカル変数xを参照されるようになる。
コメント