JavaScript for impatient programmers (ES2022 edition)
本書をサポートしてください:購入 または 寄付
(広告、ブロックしないでください。)

11 変数と代入



これらはJavaScriptの主な変数宣言方法です。

ES6以前には、varもありましたが、いくつかの癖があるため、現代のJavaScriptでは避けるのが最善です。詳しくはSpeaking JavaScriptをご覧ください。

11.1 let

let で宣言された変数は変更可能です。

let i;
i = 0;
i = i + 1;
assert.equal(i, 1);

同時に宣言と代入を行うこともできます。

let i = 0;

11.2 const

const で宣言された変数は不変です。常にすぐに初期化する必要があります。

const i = 0; // must initialize

assert.throws(
  () => { i = i + 1 },
  {
    name: 'TypeError',
    message: 'Assignment to constant variable.',
  }
);

11.2.1 const と不変性

JavaScriptでは、const は、*束縛*(変数名と変数値の関連付け)が不変であることを意味するだけです。値自体は、次の例の obj のように、変更可能な場合があります。

const obj = { prop: 0 };

// Allowed: changing properties of `obj`
obj.prop = obj.prop + 1;
assert.equal(obj.prop, 1);

// Not allowed: assigning to `obj`
assert.throws(
  () => { obj = {} },
  {
    name: 'TypeError',
    message: 'Assignment to constant variable.',
  }
);

11.2.2 const とループ

const は、各反復で新しい束縛が作成される for-of ループで使用できます。

const arr = ['hello', 'world'];
for (const elem of arr) {
  console.log(elem);
}
// Output:
// 'hello'
// 'world'

ただし、プレーンな for ループでは、let を使用する必要があります。

const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
  const elem = arr[i];
  console.log(elem);
}

11.3 constlet の使い分け

constlet の使い分けについては、次のルールをお勧めします。

  演習:const

exercises/variables-assignment/const_exrc.mjs

11.4 変数のスコープ

変数の *スコープ* は、プログラム内でアクセスできる領域です。次のコードを考えてみましょう。

{ // // Scope A. Accessible: x
  const x = 0;
  assert.equal(x, 0);
  { // Scope B. Accessible: x, y
    const y = 1;
    assert.equal(x, 0);
    assert.equal(y, 1);
    { // Scope C. Accessible: x, y, z
      const z = 2;
      assert.equal(x, 0);
      assert.equal(y, 1);
      assert.equal(z, 2);
    }
  }
}
// Outside. Not accessible: x, y, z
assert.throws(
  () => console.log(x),
  {
    name: 'ReferenceError',
    message: 'x is not defined',
  }
);

各変数は、直接のスコープとそのスコープ内にネストされたすべてのスコープでアクセスできます。

constlet で宣言された変数は、スコープが常に最も内側のブロックであるため、*ブロックスコープ* と呼ばれます。

11.4.1 変数のシャドーイング

同じレベルで同じ変数を2回宣言することはできません。

assert.throws(
  () => {
    eval('let x = 1; let x = 2;');
  },
  {
    name: 'SyntaxError',
    message: "Identifier 'x' has already been declared",
  });

  なぜ eval() を使うのか?

eval() は、assert.throws() のコールバックが実行されるまで、解析(したがって SyntaxError)を遅延させます。これを使用しない場合、このコードが解析された時点で既にエラーが発生し、assert.throws() は実行されません。

ただし、ブロックをネストし、ブロックの外で使用したのと同じ変数名 x を使用できます。

const x = 1;
assert.equal(x, 1);
{
  const x = 2;
  assert.equal(x, 2);
}
assert.equal(x, 1);

ブロック内では、内側の x がその名前を持つ唯一のアクセス可能な変数です。内側の x は、外側の x を *シャドーイング* すると言われます。ブロックを離れると、古い値に再度アクセスできます。

  クイズ:基本

クイズアプリ を参照してください。

11.5 (高度な内容)

残りのセクションはすべて高度な内容です。

11.6 用語:静的 vs. 動的

これら2つの形容詞は、プログラミング言語の現象を説明します。

これらの2つの用語の例を見てみましょう。

11.6.1 静的な現象:変数のスコープ

変数のスコープは静的な現象です。次のコードを考えてみましょう。

function f() {
  const x = 3;
  // ···
}

x は *静的* (または *字句的*)に *スコープ化* されています。つまり、そのスコープは固定されており、実行時に変更されることはありません。

変数のスコープは、静的なツリーを形成します(静的なネストによる)。

11.6.2 動的な現象:関数呼び出し

関数呼び出しは動的な現象です。次のコードを考えてみましょう。

function g(x) {}
function h(y) {
  if (Math.random()) g(y); // (A)
}

A行の関数呼び出しが発生するかどうかは、実行時にのみ決定できます。

関数呼び出しは、動的なツリーを形成します(動的な呼び出しによる)。

11.7 グローバル変数とグローバルオブジェクト

JavaScriptの変数のスコープはネストされています。それらはツリーを形成します。

ルートは、*グローバルスコープ* とも呼ばれます。Webブラウザーでは、そのスコープに直接存在する唯一の場所は、スクリプトの最上位レベルです。グローバルスコープの変数は *グローバル変数* と呼ばれ、どこからでもアクセスできます。グローバル変数には2種類あります。

次のHTMLフラグメントは、globalThis と2種類のグローバル変数を示しています。

<script>
  const declarativeVariable = 'd';
  var objectVariable = 'o';
</script>
<script>
  // All scripts share the same top-level scope:
  console.log(declarativeVariable); // 'd'
  console.log(objectVariable); // 'o'
  
  // Not all declarations create properties of the global object:
  console.log(globalThis.declarativeVariable); // undefined
  console.log(globalThis.objectVariable); // 'o'
</script>

各ECMAScriptモジュールには独自のスコープがあります。したがって、モジュールの最上位レベルに存在する変数はグローバルではありません。図 5 は、さまざまなスコープがどのように関連しているかを示しています。

Figure 5: The global scope is JavaScript’s outermost scope. It has two kinds of variables: object variables (managed via the global object) and normal declarative variables. Each ECMAScript module has its own scope which is contained in the global scope.

11.7.1 globalThis [ES2020]

グローバル変数 globalThis は、グローバルオブジェクトにアクセスするための新しい標準的な方法です。グローバルスコープでは this と同じ値を持つという事実から、この名前が付けられました。

  globalThis は必ずしもグローバルオブジェクトを直接指すとは限りません

たとえば、ブラウザーでは、間接参照 があります。その間接参照は通常は気づきませんが、存在しており、観測できます。

11.7.1.1 globalThis の代替

グローバルオブジェクトにアクセスする古い方法は、プラットフォームに依存します。

11.7.1.2 globalThis のユースケース

グローバルオブジェクトは、後方互換性のためにJavaScriptが排除できない誤りであると考えられています。パフォーマンスに悪影響を与え、一般的に混乱を招きます。

ECMAScript 6では、グローバルオブジェクトを回避しやすくするいくつかの機能が導入されました。たとえば、

通常、グローバルオブジェクト変数には、globalThis のプロパティを介してではなく、変数を介してアクセスする方が適切です。前者は、すべてのJavaScriptプラットフォームで常に同じように機能してきました。

Web上のチュートリアルでは、window.globVar を介してグローバル変数 globVar にアクセスすることがあります。しかし、接頭辞「window.」は不要であり、省略することをお勧めします。

window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes

したがって、globalThis のユースケースは比較的わずかです。たとえば、

11.8 宣言:スコープとアクティベーション

これらは宣言の2つの重要な側面です。

1に、さまざまな宣言がこれらの側面をどのように処理するかをまとめます。

表1:宣言の側面。「重複」は、宣言が(スコープごとに)同じ名前で2回使用できるかどうかを表します。「グローバルプロパティ」は、スクリプトのグローバルスコープで実行されたときに、宣言がグローバルオブジェクトにプロパティを追加するかどうかを表します。TDZ一時的なデッドゾーン(後で説明します)を意味します。(*)関数宣言は通常ブロックスコープですが、sloppyモードでは関数スコープです。
スコープ アクティベーション 重複 グローバルプロパティ
const ブロック 宣言(TDZ)
let ブロック 宣言(TDZ)
function ブロック(*) 開始
class ブロック 宣言(TDZ)
import モジュール exportと同じ
var 関数 開始、部分的に

import については§27.5 “ECMAScriptモジュール”で説明しています。以下のセクションでは、他の構造について詳しく説明します。

11.8.1 constlet:一時的なデッドゾーン

JavaScriptの場合、TC39は、宣言の前に直接スコープ内で定数にアクセスした場合に何が起こるかを決定する必要がありました。

{
  console.log(x); // What happens here?
  const x;
}

考えられるいくつかの方法は次のとおりです。

  1. 名前は現在のスコープを囲むスコープで解決されます。
  2. undefinedが返されます。
  3. エラーが発生します。

アプローチ1は、このアプローチの先例が言語にないため、却下されました。したがって、JavaScriptプログラマーにとっては直感的ではありません。

アプローチ2は、そうするとxは定数ではなくなり、宣言の前と後で異なる値を持つことになるため、却下されました。

letconstと同じアプローチ3を使用するため、両方が同様に機能し、それらを簡単に切り替えることができます。

変数のスコープに入ってから宣言を実行するまでの時間は、その変数の一時的なデッドゾーン(TDZ)と呼ばれます。

次のコードは、一時的なデッドゾーンを示しています。

if (true) { // entering scope of `tmp`, TDZ starts
  // `tmp` is uninitialized:
  assert.throws(() => (tmp = 'abc'), ReferenceError);
  assert.throws(() => console.log(tmp), ReferenceError);

  let tmp; // TDZ ends
  assert.equal(tmp, undefined);
}

次の例は、一時的なデッドゾーンが本当に一時的(時間に関連する)であることを示しています。

if (true) { // entering scope of `myVar`, TDZ starts
  const func = () => {
    console.log(myVar); // executed later
  };

  // We are within the TDZ:
  // Accessing `myVar` causes `ReferenceError`

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

func()myVarの宣言の前にあり、その変数を使用している場合でも、func()を呼び出すことができます。ただし、myVarの一時的なデッドゾーンが終わるまで待つ必要があります。

11.8.2 関数宣言と早期アクティベーション

  関数に関する詳細情報

このセクションでは、関数を適切に学習する前に関数を使用しています。うまくいけば、すべて理解できるでしょう。理解できない場合は、§25「呼び出し可能な値」を参照してください。

関数宣言は、スコープ内のどこにあるかに関係なく、そのスコープに入るときに常に実行されます。これにより、関数が宣言される前に関数foo()を呼び出すことができます。

assert.equal(foo(), 123); // OK
function foo() { return 123; }

foo()の早期アクティベーションは、前のコードが次と同等であることを意味します。

function foo() { return 123; }
assert.equal(foo(), 123);

constまたはletを使用して関数を宣言した場合、それは早期にアクティベートされません。次の例では、宣言後にのみbar()を使用できます。

assert.throws(
  () => bar(), // before declaration
  ReferenceError);

const bar = () => { return 123; };

assert.equal(bar(), 123); // after declaration 
11.8.2.1 早期アクティベーションなしでの先読み呼び出し

関数g()が早期にアクティベートされない場合でも、次のルールに従えば、前の関数f()(同じスコープ内)から呼び出すことができます。f()は、g()の宣言後に呼び出す必要があります。

const f = () => g();
const g = () => 123;

// We call f() after g() was declared:
assert.equal(f(), 123);

モジュールの関数は通常、その本体全体が実行された後に呼び出されます。したがって、モジュールでは、関数の順序を心配する必要はほとんどありません。

最後に、早期アクティベーションが前述のルールを自動的に維持する方法に注目してください。スコープに入るとき、すべての関数宣言は、呼び出しが行われる前に最初に実行されます。

11.8.2.2 早期アクティベーションの落とし穴

早期アクティベーションに依存して、関数を宣言前に呼び出す場合は、早期にアクティベートされていないデータにアクセスしないように注意する必要があります。

funcDecl();

const MY_STR = 'abc';
function funcDecl() {
  assert.throws(
    () => MY_STR,
    ReferenceError);
}

MY_STRの宣言後にfuncDecl()を呼び出すと、問題は解消されます。

11.8.2.3 早期アクティベーションの長所と短所

早期アクティベーションには落とし穴があり、それを使用しなくてもその利点のほとんどを得ることができることがわかりました。したがって、早期アクティベーションは避けるのが賢明です。しかし、私はこれについて強くは感じておらず、前述のように、構文が好きなので、関数宣言をよく使用します。

11.8.3 クラス宣言は早期にアクティベートされない

クラス宣言は、いくつかの点で関数宣言に似ていますが、クラス宣言は早期にアクティベートされません。

assert.throws(
  () => new MyClass(),
  ReferenceError);

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);

それはなぜですか?次のクラス宣言を考えてみましょう。

class MyClass extends Object {}

extendsのオペランドは式です。したがって、次のようなことができます。

const identity = x => x;
class MyClass extends identity(Object) {}

このような式の評価は、それが言及された場所で行う必要があります。そうでないと混乱します。それが、クラス宣言が早期にアクティベートされない理由です。

11.8.4 var:ホイスティング(部分的な早期アクティベーション)

varは、constlet(現在推奨)よりも古い変数を宣言する方法です。次のvar宣言を検討してください。

var x = 123;

この宣言には2つの部分があります。

次のコードは、varの効果を示しています。

function f() {
  // Partial early activation:
  assert.equal(x, undefined);
  if (true) {
    var x = 123;
    // The assignment is executed in place:
    assert.equal(x, 123);
  }
  // Scope is function, not block:
  assert.equal(x, 123);
}

11.9 クロージャ

クロージャを探索する前に、束縛変数と自由変数について学習する必要があります。

11.9.1 束縛変数と自由変数

スコープごとに、言及される変数のセットがあります。これらの変数の中で、次のものを区別します。

次のコードを検討してください。

function func(x) {
  const y = 123;
  console.log(z);
}

func()の本体では、xyは束縛変数です。zは自由変数です。

11.9.2 クロージャとは?

では、クロージャとは何ですか?

クロージャとは、関数と、その「出生地」に存在する変数への接続を組み合わせたものです。

この接続を維持することのポイントは何ですか?それは、関数の自由変数の値を提供します。たとえば、次のようになります。

function funcFactory(value) {
  return () => {
    return value;
  };
}

const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)

funcFactoryは、funcに割り当てられたクロージャを返します。funcは出生地の変数への接続を持っているため、(スコープから「エスケープ」した場合でも)A行で呼び出されたときに自由変数valueにアクセスできます。

  JavaScriptのすべての関数はクロージャである

JavaScriptでは、静的スコープがクロージャを介してサポートされています。したがって、すべての関数がクロージャです。

11.9.3 例:インクリメンタのファクトリ

次の関数はインクリメンタ(私が今作った名前)を返します。インクリメンタとは、内部に数値を格納する関数です。呼び出されると、その数値に引数を加算して更新し、新しい値を返します。

function createInc(startValue) {
  return (step) => { // (A)
    startValue += step;
    return startValue;
  };
}
const inc = createInc(5);
assert.equal(inc(2), 7);

A行で作成された関数が、自由変数startValueに内部数値を保持していることがわかります。今回は、出生スコープから読み取るだけでなく、関数呼び出しをまたいで変更および持続するデータを保存するために使用します。

ローカル変数を介して、出生スコープにさらに多くのストレージスロットを作成できます。

function createInc(startValue) {
  let index = -1;
  return (step) => {
    startValue += step;
    index++;
    return [index, startValue];
  };
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);

11.9.4 クロージャのユースケース

クロージャは何に役立ちますか?

  クイズ:上級者向け

クイズアプリ を参照してください。