9. 変数とスコープ
目次
本書をサポートしてください:購入 (PDF、EPUB、MOBI) または 寄付
(広告、ブロックしないでください。)

9. 変数とスコープ



9.1 概要

ES6は、変数を宣言する2つの新しい方法を提供します:letconstです。これらは主にES5の変数宣言方法であるvarに取って代わるものです。

9.1.1 let

letvarと似ていますが、宣言された変数はブロックスコープを持ち、現在のブロック内でのみ存在します。var関数スコープです。

以下のコードでは、letで宣言された変数tmpは、A行から始まるブロック内でのみ存在することがわかります。

function order(x, y) {
    if (x > y) { // (A)
        let tmp = x;
        x = y;
        y = tmp;
    }
    console.log(tmp===x); // ReferenceError: tmp is not defined
    return [x, y];
}

9.1.2 const

constletと似ていますが、宣言する変数はすぐに初期化され、その後は値を変更できません。

const foo;
    // SyntaxError: missing = in const declaration

const bar = 123;
bar = 456;
    // TypeError: `bar` is read-only

for-ofはループの反復ごとに1つのバインディング(変数の記憶領域)を作成するため、ループ変数をconstで宣言しても問題ありません。

for (const x of ['a', 'b']) {
    console.log(x);
}
// Output:
// a
// b

9.1.3 変数の宣言方法

次の表は、ES6で変数を宣言できる6つの方法の概要を示しています(kangaxによる表を参考にしています)。

  ホイスティング スコープ グローバルプロパティを作成します
var 宣言 関数 はい
let 一時的デッドゾーン ブロック いいえ
const 一時的デッドゾーン ブロック いいえ
関数 完全 ブロック はい
クラス いいえ ブロック いいえ
import 完全 モジュールグローバル いいえ

9.2 letconstによるブロックスコープ

letconstの両方とも、ブロックスコープを持つ変数を作成します。これらは、それらを囲む最も内側のブロック内でのみ存在します。次のコードは、constで宣言された変数tmpif文のブロック内でのみ存在することを示しています。

function func() {
    if (true) {
        const tmp = 123;
    }
    console.log(tmp); // ReferenceError: tmp is not defined
}

対照的に、varで宣言された変数は関数スコープです。

function func() {
    if (true) {
        var tmp = 123;
    }
    console.log(tmp); // 123
}

ブロックスコープは、関数内で変数をシャドウイングできることを意味します。

function func() {
  const foo = 5;
  if (···) {
     const foo = 10; // shadows outer `foo`
     console.log(foo); // 10
  }
  console.log(foo); // 5
}

9.3 constによる不変変数の作成

letによって作成された変数は変更可能です。

let foo = 'abc';
foo = 'def';
console.log(foo); // def

定数、constによって作成された変数は不変です。異なる値を割り当てることはできません。

const foo = 'abc';
foo = 'def'; // TypeError

9.3.1 落とし穴:constは値を不変にしません

constは、変数が常に同じ値を持つことを意味するだけであり、値自体が不変である、または不変になるという意味ではありません。たとえば、objは定数ですが、それが指す値は変更可能です。プロパティを追加できます。

const obj = {};
obj.prop = 123;
console.log(obj.prop); // 123

しかし、objに異なる値を割り当てることはできません。

obj = {}; // TypeError

objの値を不変にするには、自分で対処する必要があります。たとえば、凍結することによって。

const obj = Object.freeze({});
obj.prop = 123; // TypeError
9.3.1.1 落とし穴:Object.freeze()は浅い凍結です

Object.freeze()浅い凍結であることに注意してください。引数のプロパティのみを凍結し、そのプロパティに格納されているオブジェクトは凍結しません。たとえば、オブジェクトobjは凍結されています。

> const obj = Object.freeze({ foo: {} });
> obj.bar = 123
TypeError: Can't add property bar, object is not extensible
> obj.foo = {}
TypeError: Cannot assign to read only property 'foo' of #<Object>

しかし、オブジェクトobj.fooは凍結されていません。

> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'

9.3.2 ループ本体内のconst

const変数が作成された後は変更できません。しかし、それはスコープを再入力して、新しい値で最初からやり直せないという意味ではありません。たとえば、ループを使用すると。

function logArgs(...args) {
    for (const [index, elem] of args.entries()) { // (A)
        const message = index + '. ' + elem; // (B)
        console.log(message);
    }
}
logArgs('Hello', 'everyone');

// Output:
// 0. Hello
// 1. everyone

このコードにはA行とB行に2つのconst宣言があります。そして、各ループの反復で、それらの定数は異なる値を持ちます。

9.4 一時的デッドゾーン

letまたはconstで宣言された変数には、いわゆる一時的デッドゾーン(TDZ)があります。スコープに入ると、実行が宣言に到達するまでアクセス(取得または設定)できません。varで宣言された変数(TDZがない)とletで宣言された変数(TDZがある)のライフサイクルを比較してみましょう。

9.4.1 varで宣言された変数のライフサイクル

var変数には一時的デッドゾーンがありません。そのライフサイクルは次の手順で構成されます。

9.4.2 letで宣言された変数のライフサイクル

letで宣言された変数には一時的デッドゾーンがあり、そのライフサイクルは次のようになります。

const変数はlet変数と同様に動作しますが、イニシャライザ(つまり、すぐに値に設定する)が必要であり、変更できません。

9.4.3

TDZ内では、変数を取得または設定すると例外がスローされます。

let tmp = true;
if (true) { // enter new scope, TDZ starts
    // Uninitialized binding for `tmp` is created
    console.log(tmp); // ReferenceError

    let tmp; // TDZ ends, `tmp` is initialized with `undefined`
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
}
console.log(tmp); // true

イニシャライザがある場合、TDZはイニシャライザが評価され、結果が変数に割り当てられたに終了します。

let foo = console.log(foo); // ReferenceError

次のコードは、デッドゾーンが実際に時間的(時間に基づく)であり、空間的(位置に基づく)ではないことを示しています。

if (true) { // enter new scope, TDZ starts
    const func = function () {
        console.log(myVar); // OK!
    };

    // Here we are within the TDZ and
    // accessing `myVar` would cause a `ReferenceError`

    let myVar = 3; // TDZ ends
    func(); // called outside TDZ
}

9.4.4 typeofはTDZ内の変数に対してReferenceErrorをスローします

typeofを使用して一時的デッドゾーン内の変数にアクセスすると、例外が発生します。

if (true) {
    console.log(typeof foo); // ReferenceError (TDZ)
    console.log(typeof aVariableThatDoesntExist); // 'undefined'
    let foo;
}

なぜ?その理由は次のとおりです。fooは宣言されていませんが、初期化されていません。その存在を認識する必要がありますが、認識していません。したがって、警告されることは望ましいように思われます。

さらに、この種のチェックは、グローバル変数を条件付きで作成する場合にのみ役立ちます。これは、通常のプログラムでは行う必要のないことです。

9.4.4.1 条件付きで変数を作成する

条件付きで変数を作成する場合、2つのオプションがあります。

オプション1 - typeofvar

if (typeof someGlobal === 'undefined') {
    var someGlobal = { ··· };
}

このオプションはグローバルスコープでのみ機能します(したがって、ES6モジュール内では機能しません)。

オプション2 - window

if (!('someGlobal' in window)) {
    window.someGlobal = { ··· };
}

9.4.5 なぜ一時的デッドゾーンがあるのか?

constletに一時的デッドゾーンがある理由はいくつかあります。

9.4.6 参考資料

このセクションの情報源

9.5 ループの先頭におけるletconst

次のループでは、ループの先頭で変数を宣言できます。

変数を宣言するには、varlet、またはconstのいずれかを使用できます。それぞれに異なる効果があり、次に説明します。

9.5.1 forループ

forループの先頭でvarを使用して変数を宣言すると、その変数に対して単一のバインディング(記憶領域)が作成されます。

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]

3つのアロー関数の本体内のすべてのiは同じバインディングを参照するため、すべて同じ値を返します。

letを使用して変数を宣言すると、ループの各反復ごとに新しいバインディングが作成されます。

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]

今回は、各iは特定の反復のバインディングを参照し、その時点で現在の値を保持します。そのため、各アロー関数は異なる値を返します。

constvarのように動作しますが、constで宣言された変数の初期値を変更することはできません。

// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
    console.log(i);
}

各反復ごとに新しいバインディングを取得するのは最初は奇妙に見えるかもしれませんが、後のセクションで説明するように、ループ変数を参照する関数をループを使用して作成する際に非常に役立ちます。

9.5.2 for-ofループとfor-inループ

for-ofループでは、varは単一のバインディングを作成します。

const arr = [];
for (var i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // [2,2,2]

constは反復ごとに1つの不変のバインディングを作成します。

const arr = [];
for (const i of [0, 1, 2]) {
    arr.push(() => i);
}
arr.map(x => x()); // [0,1,2]

letも反復ごとに1つのバインディングを作成しますが、作成されるバインディングは変更可能です。

for-inループはfor-ofループと同様に動作します。

9.5.3 なぜ反復ごとのバインディングが有用なのか?

以下は、3つのリンクを表示するHTMLページです。

  1. 「yes」をクリックすると、「ja」に変換されます。
  2. 「no」をクリックすると、「nein」に変換されます。
  3. 「perhaps」をクリックすると、「vielleicht」に変換されます。
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div id="content"></div>
    <script>
        const entries = [
            ['yes', 'ja'],
            ['no', 'nein'],
            ['perhaps', 'vielleicht'],
        ];
        const content = document.getElementById('content');
        for (const [source, target] of entries) { // (A)
            content.insertAdjacentHTML('beforeend',
                `<div><a id="${source}" href="">${source}</a></div>`);
            document.getElementById(source).addEventListener(
                'click', (event) => {
                    event.preventDefault();
                    alert(target); // (B)
                });
        }
    </script>
</body>
</html>

表示される内容は変数target(B行)によって異なります。A行でconstではなくvarを使用していた場合、ループ全体に対して単一のバインディングが存在し、targetは後で'vielleicht'という値になります。そのため、どのリンクをクリックしても、常に'vielleicht'という翻訳が表示されます。

ありがたいことに、constを使用すると、ループの反復ごとに1つのバインディングが取得され、翻訳が正しく表示されます。

9.6 パラメータを変数として

9.6.1 パラメータとローカル変数

パラメータと同じ名前の変数をletで宣言すると、静的(ロード時)エラーが発生します。

function func(arg) {
    let arg; // static error: duplicate declaration of `arg`
}

ブロック内で同じことを行うと、パラメータがシャドウされます。

function func(arg) {
    {
        let arg; // shadows parameter `arg`
    }
}

これとは対照的に、パラメータと同じ名前の変数をvarで宣言しても何も起こらず、同じスコープ内でvar変数を再宣言しても何も起こりません。

function func(arg) {
    var arg; // does nothing
}
function func(arg) {
    {
        // We are still in same `var` scope as `arg`
        var arg; // does nothing
    }
}

9.6.2 パラメータのデフォルト値と一時的デッドゾーン

パラメータにデフォルト値がある場合、それらはlet文のシーケンスとして扱われ、一時的デッドゾーンの影響を受けます。

// OK: `y` accesses `x` after it has been declared
function foo(x=1, y=x) {
    return [x, y];
}
foo(); // [1,1]

// Exception: `x` tries to access `y` within TDZ
function bar(x=y, y=2) {
    return [x, y];
}
bar(); // ReferenceError

9.6.3 パラメータのデフォルト値は本体のスコープを見ません

パラメータのデフォルト値のスコープは本体のスコープとは別に存在します(前者は後者を囲みます)。つまり、パラメータのデフォルト値「内側」で定義されたメソッドまたは関数は、本体のローカル変数を見ません。

const foo = 'outer';
function bar(func = x => foo) {
    const foo = 'inner';
    console.log(func()); // outer
}
bar();

9.7 グローバルオブジェクト

JavaScriptのグローバルオブジェクト(ウェブブラウザではwindow、Node.jsではglobal)は、特にパフォーマンスに関して、機能というよりはバグに近いものです。そのため、ES6が区別を導入するのは理にかなっています。

モジュールの本体はグローバルスコープで実行されず、スクリプトのみが実行されることに注意してください。したがって、さまざまな変数の環境は次のチェーンを形成します。

9.8 関数宣言とクラス宣言

関数宣言は…

次のコードは、関数宣言のホイスティングを示しています。

{ // Enter a new scope

    console.log(foo()); // OK, due to hoisting
    function foo() {
        return 'hello';
    }
}

クラス宣言は…

クラスがホイスティングされないことは驚くべきことかもしれません。なぜなら、内部的には関数を作成するからです。この動作の理由は、それらのextends句の値が式によって定義され、それらの式は適切なタイミングで実行される必要があるためです。

{ // Enter a new scope

    const identity = x => x;

    // Here we are in the temporal dead zone of `MyClass`
    const inst = new MyClass(); // ReferenceError

    // Note the expression in the `extends` clause
    class MyClass extends identity(Object) {
    }
}

9.9 コーディングスタイル: constletvar

letまたはconstを常に使用するようにお勧めします。

  1. constを優先してください。変数が値をまったく変更しない場合はいつでも使用できます。言い換えれば、変数は代入の左辺または++または--のオペランドであってはなりません。const変数が参照するオブジェクトを変更することは許可されます。
     const foo = {};
     foo.prop = 123; // OK
    

    for-ofループでもconstを使用できます。ループの反復ごとに1つの(不変の)バインディングが作成されるためです。

     for (const x of ['a', 'b']) {
         console.log(x);
     }
     // Output:
     // a
     // b
    

    for-ofループの本体内では、xを変更できません。

  2. それ以外の場合は、変数の初期値が後で変更される場合はletを使用します。
     let counter = 0; // initial value
     counter++; // change
    
     let obj = {}; // initial value
     obj = { foo: 123 }; // change
    
  3. varは避けてください。

これらのルールに従うと、varはレガシーコードにのみ表示され、注意深いリファクタリングが必要であることを示すシグナルとなります。

varletconstにはない1つのことを行います。これを使用して宣言された変数はグローバルオブジェクトのプロパティになります。ただし、これは一般的に良いことではありません。window(ブラウザ)またはglobal(Node.js)に代入することで、同じ効果を得ることができます。

9.9.1 代替アプローチ

前述のスタイルルールに対する代替手段として、完全に不変なもの(プリミティブ値と凍結オブジェクト)に対してのみconstを使用するという方法があります。すると、2つのアプローチがあります。

  1. constを優先する: constは不変のバインディングを示します。
  2. letを優先する: constは不変の値を示します。

1を推奨しますが、2も可。

次:10. デストラクチャリング