【JavaScript】変数はどの場所から参照できるか(スコープ)
さて引き続き関数の勉強です!(`・ω・´)
グローバルスコープとローカルスコープがある
スコープとは
「変数が作りぷとの中のどの場所から参照できるか」
を決める概念である
JavaScriptのスコープは以下の2つに分類できる
- グローバルスコープ
- スクリプト全体から参照できる
- ローカルスコープ
- 定義された関数の中でのみ参照できる
ここまではトップレベルで定義する
(関数の外で定義する)変数だけ見てきたので
スコープを意識する必要はほぼなかったが
いよいよ関数が登場したところで
このスコープについても理解しておく必要がある
はーい!がんばりまーす!
グローバル変数とローカル変数の違い
グローバルスコープを持つ変数のことをグローバル変数
ローカルスコープを持つ変数のことをローカル変数という
とりあえず一旦は
- グローバル変数
- 関数の外で宣言した変数
- ローカル変数
- 関数の中で宣言した変数
と覚えておく
(これ本当はやや嘘も混ざっているらしい‥後述を待つ!)
var scope = 'Global Variable'; function getValue() { var scope = 'Local Variable'; return scope; } document.writeln(getValue()); //Local Variable document.writeln(scope); //Global Variable
関数の外で宣言された1行目の変数scopeはグローバル変数
関数getValue内で宣言された変数scopeは
ローカル変数とみなされる
結果としてgetValue関数を介して変数scopeを参照した場合は
ローカル変数scopeの値が、
最後の行のように直接変数scopeを参照した場合には
グローバル変数scopeの値が、それぞれ返されることが確認できる
スコープが異なる場合、それぞれの変数は
(同名であっても)別ものとして認識されている
変数宣言にvar命令は必須
scope = 'Global Variable'; function getValue() { scope = 'Local Variable'; return scope; } document.writeln(getValue()); //Local Variable document.writeln(scope); //Local Variable
さっきのコードの変数宣言から
var命令を取り除いたコードである
JavaScriptにおいて変数宣言を表すvar命令は
省略可能なためこのコードは正しく動作する
しかし、結果はどちらも「Local Variable」を返す
JavaScriptではvar命令を使わずに宣言した変数は
すべてグローバル変数とみなすため
getValue関数が実行された段階で
変数scopeが「scope = 'Local Variable';」によって
上書きされてしまうことになる
ほほーなるほど、これが
とりあえず一旦は
- グローバル変数
- 関数の外で宣言した変数
- ローカル変数
- 関数の中で宣言した変数
と覚えておく
と云っていたのをやや嘘と述べた理由なのか!
ローカル変数を定義するには必ず
var命令を使用する必要がある
ふむふむー!
以上の理由から
「関数内でグローバル変数を書き換える」ような用途を除いて
原則としてvar命令を省略すべきではない
グローバル変数を宣言する場合も
「グローバル変数にはvar命令をつけず、
ローカル変数にはvar命令を付ける」というのは
かえって混乱のもとになるため
原則、「変数宣言はvar命令で行う」癖をつけておくことで
無用なバグの混入を防ぐことができる
はーい!きをつけます!(・∀・)
ローカル変数の有効範囲はどこまで?
ローカル変数は「宣言された関数の中でのみ有効な変数」と述べたが
より厳密には「宣言された関数全体で有効な変数」である
var scope = 'Global Variable'; function getValue() { document.writeln(scope); //undefined var scope = 'Local Variable'; return scope; } document.writeln(getValue()); //Local Variable document.writeln(scope); //Global Variable
これは
- JavaScriptではローカル変数は「関数全体で有効」である
- getValue関数内の「document.writeln(scope);」を実行するときすでに変数scopeは有効になっている
- しかしローカル変数scopeは確保されているだけでvar命令は実行されていない
- つまりローカル変数scopeの中身は未定義(undefined)である
というように処理をしているためである
JavaScriptのこのような挙動が思わぬ不具合の原因になる
これを避けるという意味でも
ローカル変数は関数の先頭で宣言するように
心がけるべきである
仮引数のスコープ(基本型と参照型の違いに注意する)
仮引数とは
「呼び出し元から関数に対して渡されたパラメータを受け取る変数」
function triangle(base, height) {..}
上記のようなtriangle関数であれば
仮引数はbase、heightとなる
仮引数は基本的にローカル変数として処理される
var value = 10; function decrementValue(value) { value--; return value; } document.writeln(decrementValue(100)); //99 document.writeln(value); //10
上記のコードでは
- グローバル変数valueに10がセットされる
- 「document.writeln(decrementValue(100));」でdecrementValue関数が呼び出される
- decrementValue関数内部で使用されている仮引数valueはローカル変数とみなされるため、これをいくら操作してもグローバル変数valueに影響はない
- 「document.writeln(decrementValue(100));」でdecrementValue関数に100を仮引数valueに渡しても、仮引数valueをデクリメントしても、グローバル変数valueが書き換えられることはない
結果として「document.writeln(value);」では
もともとの値である10が返される
仮引数に渡される値が参照型の場合はどうなるのか
具体的なコードをみてみる
var value = [1, 2, 4, 8, 16]; function decrementElement(value) { value.pop(); //末尾の要素を削除 return value; } document.writeln(decrementElement(value)); //1,2,4,8 document.writeln(value); //1,2,4,8
参照型とは「値そのものでなく値を格納した
メモリ上の場所(アドレス)だけ格納している型」である
そして参照型の値を受け渡しする場合には渡される値も
(値そのものではなく)メモリ上のアドレスだけになる
(このような渡し方を参照渡しという)
つまりここでは
- 一行目で定義されたグローバル変数valueと二行目で定義された仮引数(ローカル変数)とは変数としては別物である
- しかし「document.writeln(decrementElement(value));」でグローバル変数valueの値が仮引数valueに渡された時点で「結果的に」実際に参照しているメモリ上の場所が等しくなる
したがってdecrementElement関数の中で配列を操作した場合
結果はグローバル変数valueにも反映される
‥(・∀・;)
わかってるような、わかってないような?←
ちょっとこれは
ちゃんと先生に聞いておいた方がよさそうだー
ブロックレベルのスコープは存在しない
JavaやC#のようなプログラム言語では
文ブロック({..})の範囲内でのみ
有効とするスコープ(ブロックスコープ)が存在する
以下はJavaによるごく簡単なサンプルである
if (ture) { int i = 5; } System.out.println(i); //エラー
Javaの世界ではブロック単位でスコープが決定するため
この場合「変数iの有効範囲はifブロックの内部だけ」になる
つまり「System.out.println(i);」の時点で
「変数iが未定義である」とみなされエラーとなる
一方JavaScriptの世界では、同様のコードが正しく動作する
if (ture) { var i = 5; } documet.writeln(i); //5
JavaScriptではブロックレベルのスコープが存在せず
ブロック(ここではifブロック)を抜けた後も
変数iが有効であり続けるためこのような結果になる
疑似的にブロックスコープを定義する
「変数の意図せぬ競合を防ぐ」という意味でも
変数のスコープをできるだけ必要最低限に留めることは重要である
JavaScriptでも下記のような記述で疑似的に
ブロックスコープを実現することができる
with ({i:0}) { if (ture) { i = 5; } } documet.writeln(i); //変数iはスコープ外なのでエラー
ここではwith命令を利用することで
withブロックの中でのみ参照可能な変数i
(実際には匿名オブジェクトのiプロパティ)を定義している
かなりトリッキーなコードなため
日常的に使用することはおすすめしないが
どうしてもブロックスコープを実装したいという場合には
このような記述を試してみるのも良い
ふむふむー
関数リテラル/Functionコンストラクタにおけるスコープの違い
関数リテラルとFunctionコンストラクタは
いずれも匿名関数を定義するための機能を提供するものだが
実は関数の中でこれらを利用した場合
スコープの解釈が異なる
var scope = 'Global Variable' function checkScope() { var scope = 'Local Variable' var f_lit = function() { return scope; }; document.writeln(f_lit()); //Local Variable var f_con = new Function('return scope;'); document.writeln(f_con()); //Global Variable } checkScope();
関数リテラルf_litも、Functionコンストラクタf_conも
関数内部で定義している
そのためいずれもローカル変数scopeを参照するように見えるが
結果を見てもわかるように
Functionコンストラクタはグローバル変数を参照している
これは直観的にもわかりにくい挙動だが
仕様書を確認すると正しい挙動であると書かれている
なんかながーい名前の仕様書だから一旦置いておこう‥←
Functionコンストラクタは
原則として利用しないことを前提とすれば
このような混乱が生じるケースも少ないかもしれないが
ここで改めて
「関数の3つの記法は必ずしも意味的に等価ではない」
ということを確認しておくべきである
はーい!
スコープ長かった‥
それだけ大事ということですよねわかります