第16章 変数:スコープ、環境、クロージャ
目次
書籍を購入する
(広告です。ブロックしないでください。)

第16章 変数:スコープ、環境、クロージャ

この章では、まず変数の使用方法を説明し、次に変数の仕組み(環境、クロージャなど)について詳しく説明します。

変数の宣言

JavaScriptでは、変数を使用する前にvarステートメントを使用して宣言します

var foo;
foo = 3; // OK, has been declared
bar = 5; // not OK, an undeclared variable

宣言と代入を組み合わせて、変数をすぐに初期化することもできます

var foo = 3;

初期化されていない変数の値はundefinedです:

> var x;
> x
undefined

背景:静的と動的

プログラムの動作を調べるには、2つの角度があります:

静的に(またはレキシカルに)

プログラムを実行せずに、ソースコードに存在するままの状態で調べます。次のコードが与えられた場合、関数gが関数fの中にネストされているという静的な主張をすることができます

function f() {
    function g() {
    }
}

形容詞レキシカル静的と同義語として使用されます。なぜなら、どちらもプログラムのレキシコン(単語、ソース)に関係するからです。

動的に

プログラムの実行中(「実行時」)に何が起こるかを調べます。次のコードが与えられた場合

function g() {
}
function f() {
    g();
}

f()を呼び出すと、g()が呼び出されます。実行時に、fによってgが呼び出されることは、動的な関係を表します。

背景:変数のスコープ

この章の残りの部分では、次の概念を理解する必要があります:

変数のスコープ

変数のスコープとは、アクセス可能な場所のことです。例えば

function foo() {
    var x;
}

ここで、x直接スコープは関数foo()です。

レキシカルスコープ
JavaScriptの変数はレキシカルスコープです。つまり、プログラムの静的な構造によって変数のスコープが決まります(たとえば、関数がどこから呼び出されるかによって影響を受けることはありません)。
ネストされたスコープ

スコープが変数の直接スコープ内にネストされている場合、変数はそれらのすべてのスコープでアクセス可能です

function foo(arg) {
    function bar() {
        console.log('arg: '+arg);
    }
    bar();
}
console.log(foo('hello')); // arg: hello

argの直接スコープはfoo()ですが、ネストされたスコープbar()でもアクセスできます。ネストに関して、foo()外側のスコープbar()内側のスコープです。

シャドーイング

スコープが、周囲のスコープにある変数と同じ名前の変数を宣言する場合、外側の変数へのアクセスは内側のスコープとその中にネストされたすべてのスコープでブロックされます。内側の変数の変更は外側の変数には影響しません。外側の変数は、内側のスコープが終了した後、再びアクセス可能になります:

var x = "global";
function f() {
    var x = "local";
    console.log(x); // local
}
f();
console.log(x); // global

関数f()内では、グローバルなxはローカルなxによってシャドーイングされます。

変数は関数スコープです

ほとんどの主流の言語はブロックスコープです。変数は最も内側の周囲のコードブロックの「内部に存在」します。Javaの例を次に示します。

public static void main(String[] args) {
    { // block starts
        int foo = 4;
    } // block ends
    System.out.println(foo); // Error: cannot find symbol
}

上記のコードでは、変数fooはそれを直接囲むブロック内でのみアクセス可能です。ブロックの終了後にアクセスしようとすると、コンパイルエラーが発生します。

対照的に、JavaScriptの変数は関数スコープです:新しいスコープを導入するのは関数のみです。ブロックはスコープに関しては無視されます。例えば:

function main() {
    { // block starts
        var foo = 4;
    } // block ends
    console.log(foo); // 4
}

言い換えれば、fooはブロック内だけでなく、main()全体でアクセス可能です。

変数宣言はホイストされます

JavaScriptはすべての変数宣言をホイストします。つまり、それらを直接スコープの先頭に移動します。これにより、変数が宣言される前にアクセスされた場合に何が起こるかが明確になります:

function f() {
    console.log(bar);  // undefined
    var bar = 'abc';
    console.log(bar);  // abc
}

変数barf()の最初の行に既に存在しますが、まだ値がありません。つまり、宣言はホイストされていますが、代入はホイストされていません。 JavaScriptは、コードが次のようであるかのようにf()を実行します

function f() {
    var bar;
    console.log(bar);  // undefined
    bar = 'abc';
    console.log(bar);  // abc
}

既に宣言されている変数を宣言しても、何も起こりません(変数の値は変更されません)

> var x = 123;
> var x;
> x
123

各関数宣言もホイストされますが、方法は少し異なります。変数の作成だけでなく、完全な関数がホイストされます(ホイストを参照)。

ベストプラクティス:ホイストを意識するが、恐れない

JavaScriptのスタイルガイドの中には、ホイストによって騙されないように、変数宣言を関数の先頭にのみ配置することを推奨するものがあります。関数が比較的短い場合(そうでなければなりません)、そのルールを少し緩和して、使用される場所の近く(たとえば、forループ内)で変数を宣言することができます。これにより、コードの一部がより適切にカプセル化されます。もちろん、関数全体のホイストは依然として発生するため、そのカプセル化は概念的なものにすぎないことを認識しておく必要があります。

IIFEを介した新しいスコープの導入

通常、変数の寿命を制限するために新しいスコープを導入します。 そうしたくなる例としては、ifステートメントの「then」部分が挙げられます。条件が満たされた場合にのみ実行されます。そして、ヘルパー変数を排他的に使用する場合、それらが周囲のスコープに「リーク」することを避けたいのです。

function f() {
    if (condition) {
        var tmp = ...;
        ...
    }
    // tmp still exists here
    // => not what we want
}

thenブロックに新しいスコープを導入する場合、関数を定義してすぐに呼び出すことができます。これはブロックスコープの回避策、シミュレーションです。

function f() {
    if (condition) {
        (function () {  // open block
            var tmp = ...;
            ...
        }());  // close block
    }
}

これはJavaScriptでは一般的なパターンです。 Ben Almanは、これを即時実行関数式(IIFE、「イフィー」と発音)と呼ぶことを提案しました。一般に、IIFEは次のようになります

(function () { // open IIFE
    // inside IIFE
}()); // close IIFE

IIFEについて注意すべき点がいくつかあります

すぐに呼び出されます
関数の閉じかっこに続くかっこは、関数をすぐに呼び出します。つまり、その本体はすぐに実行されます。
式でなければなりません
ステートメントがキーワードfunctionで始まる場合、パーサーはそれが関数宣言であると想定します(式とステートメントを参照)。しかし、関数宣言をすぐに呼び出すことはできません。したがって、ステートメントを開きかっこで始めることで、キーワードfunctionが関数式の始まりであることをパーサーに伝えます。かっこ内には、式のみを含めることができます。
末尾のセミコロンが必要です

2つのIIFEの間にセミコロンを忘れると、コードは機能しなくなります。

(function () {
    ...
}()) // no semicolon
(function () {
    ...
}());

上記のコードは関数呼び出しとして解釈されます。最初のIIFE(かっこを含む)が呼び出される関数であり、2番目のIIFEがパラメーターです。

注意

IIFEはコスト(認知的にもパフォーマンス的にも)がかかるため、ifステートメント内で使用することはほとんど意味がありません。上記の例は、教育的な理由で選択されました。

IIFEのバリエーション:接頭辞演算子

接頭辞演算子を使用して式コンテキストを強制することもできます。たとえば、論理NOT演算子を使用してこれを行うことができます。

!function () { // open IIFE
    // inside IIFE
}(); // close IIFE

または、void演算子を使用して(void演算子を参照)

void function () { // open IIFE
    // inside IIFE
}(); // close IIFE

接頭辞演算子を使用する利点は、末尾のセミコロンを忘れても問題が発生しないことです。

IIFEのバリエーション:既に式コンテキスト内にある

既に式コンテキスト内にいる場合、IIFEの式コンテキストを強制する必要はありません。その場合、かっこや接頭辞演算子は必要ありません。例えば:

var File = function () { // open IIFE
    var UNTITLED = 'Untitled';
    function File(name) {
        this.name = name || UNTITLED;
    }
    return File;
}(); // close IIFE

上記の例では、Fileという名前の変数が2つあります。一方では、IIFE内でのみ直接アクセスできる関数があります。他方では、最初の行で宣言されている変数があります。IIFEで返される値が代入されます。

IIFEのバリエーション:パラメーター付きのIIFE

パラメーターを使用して、IIFE内部の変数を定義できます。

var x = 23;
(function (twice) {
    console.log(twice);
}(x * 2));

これは次のようになります

var x = 23;
(function () {
    var twice = x * 2;
    console.log(twice);
}());

IIFEのアプリケーション

IIFEを使用すると、プライベートデータを関数に添付できます。そうすれば、グローバル変数を宣言する必要がなく、関数とその状態を緊密にパッケージ化できます。グローバル名前空間の汚染を回避できます。

var setValue = function () {
    var prevValue;
    return function (value) { // define setValue
        if (value !== prevValue) {
            console.log('Changed: ' + value);
            prevValue = value;
        }
    };
}();

IIFEの他のアプリケーションについては、本書の他の場所で言及されています

グローバル変数

プログラム全体を含むスコープは、グローバルスコープまたはプログラムスコープと呼ばれます。 これは、スクリプト(Webページの<script>タグであっても、.jsファイルであっても)に入るときのスコープです。グローバルスコープ内では、関数を定義することでネストされたスコープを作成できます。そのような関数内では、再びスコープをネストできます。各スコープは、独自の変数と、それを囲むスコープ内の変数にアクセスできます。グローバルスコープは他のすべてのスコープを囲んでいるため、その変数にはどこからでもアクセスできます。

// here we are in global scope
var globalVariable = 'xyz';
function f() {
    var localVariable = true;
    function g() {
        var anotherLocalVariable = 123;

        // All variables of surround scopes are accessible
        localVariable = false;
        globalVariable = 'abc';
    }
}
// here we are again in global scope

ベストプラクティス:グローバル変数の作成を避ける

グローバル変数には2つの欠点があります。第1に、グローバル変数に依存するソフトウェアは副作用の影響を受けやすいです。堅牢性が低く、予測可能性が低く、再利用性が低くなります。

第2に、Webページ上のすべてのJavaScriptは同じグローバル変数を共有します。コード、組み込み関数、分析コード、ソーシャルメディアボタンなどです。つまり、名前の衝突が問題になる可能性があります。そのため、できるだけ多くの変数をグローバルスコープから隠すことが最善です。たとえば、次のようなことはしないでください

<!-- Don’t do this -->
<script>
    // Global scope
    var tmp = generateData();
    processData(tmp);
    persistData(tmp);
</script>

変数 tmp は、その宣言がグローバルスコープで実行されるため、グローバルになります。しかし、それはローカルでのみ使用されます。したがって、IIFE(IIFEによる新しいスコープの導入を参照)を使用して、ネストされたスコープ内に隠すことができます。

<script>
    (function () {  // open IIFE
        // Local scope
        var tmp = generateData();
        processData(tmp);
        persistData(tmp);
    }());  // close IIFE
</script>

モジュールシステムはグローバル変数の削減につながる

ありがたいことに、モジュールシステム(モジュールシステムを参照)は、グローバル変数の問題をほぼ解消します。モジュールはグローバルスコープを介してやり取りせず、各モジュールはモジュールグローバル変数用の独自のスコープを持っているためです。

グローバルオブジェクト

ECMAScript仕様では、内部データ構造である環境を使用して変数を格納します(環境:変数の管理を参照)。この言語には、グローバル変数の環境を、いわゆるグローバルオブジェクトと呼ばれるオブジェクトを介してアクセス可能にするという、やや珍しい機能があります。グローバルオブジェクトを使用して、グローバル変数を作成、読み取り、および変更できます。グローバルスコープでは、thisはそれを指します。

> var foo = 'hello';
> this.foo  // read global variable
'hello'

> this.bar = 'world';  // create global variable
> bar
'world'

グローバルオブジェクトにはプロトタイプがあることに注意してください。すべての(自身と継承された)プロパティを一覧表示する場合は、すべてのプロパティキーのリストgetAllPropertyNames()などの関数が必要です。

> getAllPropertyNames(window).sort().slice(0, 5)
[ 'AnalyserNode', 'Array', 'ArrayBuffer', 'Attr', 'Audio' ]

JavaScriptの作成者であるBrendan Eichは、グローバルオブジェクトを自身の「最大の反省点」の1つと考えています。パフォーマンスに悪影響を及ぼし、変数スコープの実装をより複雑にし、コードのモジュール性を低下させます。

クロスプラットフォームの考慮事項

ブラウザとNode.jsには、グローバルオブジェクトを参照するためのグローバル変数があります。残念ながら、それらは異なります。

どちらのプラットフォームでも、thisはグローバルオブジェクトを参照しますが、グローバルスコープ内にある場合のみです。これはNode.jsではほとんどありません。クロスプラットフォームの方法でグローバルオブジェクトにアクセスする場合は、次のようなパターンを使用できます。

(function (glob) {
    // glob points to global object
}(typeof window !== 'undefined' ? window : global));

今後、グローバルオブジェクトを参照するためにwindowを使用しますが、クロスプラットフォームコードでは、前述のパターンとglobを使用する必要があります。

windowのユースケース

このセクションでは、windowを介してグローバル変数にアクセスするためのユースケースについて説明します。ただし、一般的なルールは、できるだけそれを避けることです。

ユースケース:グローバル変数のマーキング

プレフィックスwindowは、コードがローカル変数ではなくグローバル変数を参照していることを示す視覚的な手がかりです。

var foo = 123;
(function () {
    console.log(window.foo);  // 123
}());

ただし、これはコードを壊れやすくします。fooをグローバルスコープから別の周囲のスコープに移動するとすぐに、動作しなくなります。

(function () {
    var foo = 123;
    console.log(window.foo);  // undefined
}());

したがって、foowindowのプロパティとしてではなく、変数として参照することをお勧めします。fooがグローバル変数またはグローバルのような変数であることを明確にする場合は、g_などの名前プレフィックスを追加できます。

var g_foo = 123;
(function () {
    console.log(g_foo);
}());

ユースケース:組み込み関数

windowを介して組み込みグローバル変数を参照することは好みません。それらはよく知られた名前なので、グローバルであることを示すインジケーターから得られるものはほとんどありません。また、プレフィックス付きのwindowが煩雑になります。

window.isNaN(...)  // no
isNaN(...)  // yes

ユースケース:スタイルチェッカー

JSLintやJSHintなどのスタイルチェックツールを使用する場合、windowを使用すると、現在のファイルで宣言されていないグローバル変数を参照したときにエラーが発生しません。ただし、どちらのツールも、そのような変数について通知し、そのようなエラーを防ぐ方法を提供しています(ドキュメントで「グローバル変数」を検索してください)。

ユースケース:グローバル変数が存在するかどうかを確認する

これはよくあるユースケースではありませんが、特にシムとポリフィル(シムとポリフィルの比較を参照)は、グローバル変数someVariableが存在するかどうかを確認する必要があります。その場合、windowが役立ちます。

if (window.someVariable) { ... }

これは、このチェックを実行するための安全な方法です。次のステートメントは、someVariableが宣言されていない場合に例外をスローします。

// Don’t do this
if (someVariable) { ... }

windowを介してチェックできる2つの追加の方法があります。それらはほぼ同等ですが、もう少し明示的です。

if (window.someVariable !== undefined) { ... }
if ('someVariable' in window) { ... }

変数が存在する(そして値を持っている)かどうかを確認する一般的な方法は、typeofを使用することです(typeof:プリミティブの分類を参照)。

if (typeof someVariable !== 'undefined') { ... }

ユースケース:グローバルスコープでのものの作成

windowを使用すると、グローバルスコープに要素を追加できます(ネストされたスコープにいる場合でも)。また、条件付きで追加することもできます。

if (!window.someApiFunction) {
    window.someApiFunction = ...;
}

通常、グローバルスコープにいる間に、varを介してグローバルスコープに要素を追加するのが最善です。ただし、windowは、条件付きで追加を行うためのクリーンな方法を提供します。

環境:変数の管理

変数は、プログラムの実行がそのスコープに入るときに存在するようになります。その後、ストレージスペースが必要になります。そのストレージスペースを提供するデータ構造は、JavaScriptでは環境と呼ばれます。変数名を値にマッピングします。その構造は、JavaScriptオブジェクトの構造と非常によく似ています。環境は、スコープを離れた後も存続することがあります。したがって、スタックではなくヒープに格納されます。

変数は2つの方法で渡されます。いわば、2つの次元があります。

これらの2つの次元は次のように処理されます。

例を見てみましょう。

function myFunction(myParam) {
    var myVar = 123;
    return myFloat;
}
var myFloat = 1.3;
// Step 1
myFunction('abc');  // Step 2

図16-1は、上記のコードが実行されたときに何が起こるかを示しています。

  1. myFunctionmyFloatはグローバル環境(#0)に格納されています。myFunctionによって参照されるfunctionオブジェクトは、内部プロパティ[[Scope]]を介してそのスコープ(グローバルスコープ)を指していることに注意してください。
  2. myFunction('abc')の実行のために、パラメータとローカル変数を保持する新しい環境(#1)が作成されます。outermyFunction.[[Scope]]から初期化されます)を介して外部環境を参照します。外部環境のおかげで、myFunctionmyFloatにアクセスできます。

クロージャ:関数はその誕生スコープに接続されたままです

関数が作成されたスコープを離れると、そのスコープ(および周囲のスコープ)の変数に接続されたままになります。例えば:

function createInc(startValue) {
    return function (step) {
        startValue += step;
        return startValue;
    };
}

createInc()によって返される関数は、startValueへの接続を失いません。この変数は、関数呼び出し間で永続化する状態を関数に提供します。

> var inc = createInc(5);
> inc(1)
6
> inc(2)
8

クロージャとは、関数とその関数が作成されたスコープへの接続のことです。この名前は、クロージャが関数の自由変数を「閉じ込める」という事実から来ています。変数は、関数内で宣言されていない場合、つまり「外部から」来た場合、自由です。

環境によるクロージャの処理

ヒント

これは、クロージャの仕組みをより深く掘り下げた高度なセクションです。環境に精通している必要があります(環境:変数の管理を確認してください)。

クロージャとは、実行がスコープを離れた後も環境が存続する例です。クロージャの仕組みを説明するために、前の`createInc()`とのインタラクションを4つのステップに分割して見てみましょう(各ステップでは、アクティブな実行コンテキストとその環境が強調表示されます。関数がアクティブな場合は、それも強調表示されます)。

  1. このステップは、インタラクションの前、`createInc`の関数宣言の評価後に行われます。`createInc`のエントリがグローバル環境(#0)に追加され、関数オブジェクトを指します。

    image with no caption
  2. このステップは、関数呼び出し`createInc(5)`の実行中に行われます。`createInc`のための新しい環境(#1)が作成され、スタックにプッシュされます。その外部環境はグローバル環境(`createInc.[[Scope]]`と同じ)です。環境はパラメータ`startValue`を保持します。

    image with no caption
  3. このステップは、`inc`への代入後に行われます。`createInc`から戻った後、その環境を指す実行コンテキストはスタックから削除されましたが、`inc.[[Scope]]`がそれを参照しているため、環境はヒープ上にまだ存在します。`inc`はクロージャ(関数と生成環境)です。

    image with no caption
  4. このステップは、`inc(1)`の実行中に行われます。新しい環境(#1)が作成され、それを指す実行コンテキストがスタックにプッシュされました。その外部環境は`inc`の`[[Scope]]`です。外部環境は`inc`に`startValue`へのアクセスを提供します。

    image with no caption
  5. このステップは、`inc(1)`の実行後に行われます。`inc`の環境を指す参照(実行コンテキスト、`outer`フィールド、または`[[Scope]]`)はもうありません。したがって、それは必要なく、ヒープから削除できます。

    image with no caption

落とし穴:意図しない環境の共有

作成した関数の動作は、現在のスコープ内の変数の影響を受けることがあります。JavaScriptでは、各関数は、関数が作成された時点での変数の値で動作するべきであるため、問題となる可能性があります。しかし、関数はクロージャであるため、関数は常に変数の*現在の*値で動作します。`for`ループでは、それが原因で正常に動作しない場合があります。例を挙げると分かりやすくなります。

function f() {
    var result = [];
    for (var i=0; i<3; i++) {
        var func = function () {
            return i;
        };
        result.push(func);
    }
    return result;
}
console.log(f()[1]());  // 3

`f`は3つの関数を持つ配列を返します。これらの関数はすべて、`f`の環境、つまり`i`にアクセスできます。実際、それらは同じ環境を共有しています。しかし、ループが終了した後、その環境では`i`の値は3になります。したがって、すべての関数は`3`を返します。

これは私たちが望むものではありません。これを修正するには、それを使用する関数を作成する前に、インデックス`i`のスナップショットを作成する必要があります。言い換えれば、各関数を、関数の作成時点での`i`の値とパッケージ化したいのです。そのため、次の手順を実行します。

  1. 返される配列の各関数に対して新しい環境を作成します。
  2. その環境に*i*の現在の値(のコピー)を格納します。

環境を作成するのは関数だけなので、ステップ1を実行するためにIIFE(IIFEによる新しいスコープの導入を参照)を使用します。

function f() {
    var result = [];
    for (var i=0; i<3; i++) {
        (function () { // step 1: IIFE
            var pos = i; // step 2: copy
            var func = function () {
                return pos;
            };
            result.push(func);
        }());
    }
    return result;
}
console.log(f()[1]());  // 1

ループを介してDOM要素にイベントハンドラを追加する場合に同様のシナリオが発生するため、この例は現実世界との関連性があることに注意してください。

次:17. オブジェクトと継承