みこむらめもむらむらむら

なんかHTML5とかJS勉強とかやりながらめもを綴るブログ

【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つの記法は必ずしも意味的に等価ではない」
ということを確認しておくべきである

はーい!





スコープ長かった‥
それだけ大事ということですよねわかります