[JavaScript講座] 変数・定数とスコープの基本

当ページのリンクには広告が含まれています。
目次

「変数」とは何か

一旦、varconst は忘れて、let だけで考えます。

let count = 1;
console.log(count); // 1

count = count + 1;
console.log(count); // 2

let でやっていることはシンプルで、

  • 「名前(count)に値(1)を紐づける」
  • 後から別の値を代入し直せる

というだけです。

ここから先の説明は、

varletconst はすべて『名前と値を紐づける』という意味では同じ。
ただし、ルールと挙動が違う。

という視点で見ていくと整理しやすいです。

var / let / const の大きな違い

結論から表でざっくりまとめます。

キーワード再代入再宣言スコープHoisting 時の挙動
varできるできる関数スコープ宣言が巻き上がり、初期値は undefined
letできるできないブロックスコープ巻き上がるが TDZ に入り、宣言前参照はエラー
constできないできないブロックスコープlet と同じく TDZ、宣言時に必ず初期化

一つずつ見ていきます。

再代入の有無

let x = 1;
x = 2; // OK

const y = 1;
y = 2; // エラー(TypeError)
  • let:後から値を変えられる
  • const:同じ名前に別の値を入れ直すことはできない

※ 後で説明しますが、const でオブジェクトを束縛した場合、
「オブジェクトの中身」は変えられます。
ここでの「再代入」は “別の値に束縛し直す” という意味です。

再宣言の可否

var a = 1;
var a = 2; // OK(上書きされる)

let b = 1;
// let b = 2; // エラー(Identifier 'b' has already been declared)

const c = 1;
// const c = 2; // 同上エラー

var は同じスコープ内で何度でも宣言できてしまうので、タイポなどがエラーになりにくいです。(≒バグに気づきにくい)

let / const は「同じ名前を同じスコープで二度宣言しようとするとエラーになります」
→ こちらの方が安全と言えます

スコープ:var は「関数」、let / const は「ブロック」

var は関数スコープ

if (true) {
  var x = 10;
}
console.log(x); // 10(ブロックの外でも見える)

var で宣言した変数は、関数全体がスコープになります。

function sample() {
  if (true) {
    var v = 123;
  }
  console.log(v); // 123
}

sample();
// console.log(v); // ここは関数の外なのでエラー
  • v は「sample 関数の中ならどこからでも見える」
  • 逆に言うと、if ブロックではスコープは区切られていない

let / const はブロックスコープ

if (true) {
  let x = 10;
  const y = 20;
}
// console.log(x); // ReferenceError
// console.log(y); // ReferenceError

let / const{} で囲まれたブロックの中だけ有効です。

function sample() {
  if (true) {
    let v = 123;
  }
  // console.log(v); // エラー
}
  • 関数内でも、「if{} の内側だけ」というより細かいスコープを作れる
  • これが let / const を推奨する大きな理由の一つです

グローバル / 関数 / ブロック の3レベル

スコープのレベル感を整理しておきます。

  1. グローバルスコープ
    • どこからでも参照できる領域(モジュールでは少し意味が変わる)
  2. 関数スコープ
    • 関数の中だけ有効
  3. ブロックスコープ
    • {} の中だけ有効(if, for, 単純な {} など)
const globalValue = "global"; // グローバル

function func() {            // ← 関数スコープ開始
  const inFunc = "func内";

  if (true) {                // ← ブロックスコープ開始
    const inBlock = "ブロック内";
    console.log(globalValue); // OK
    console.log(inFunc);      // OK
    console.log(inBlock);     // OK
  }

  console.log(globalValue); // OK
  console.log(inFunc);      // OK
  // console.log(inBlock);  // エラー
}

func();

// console.log(inFunc);  // エラー
// console.log(inBlock); // エラー
  • 外側から内側は見えない
  • 内側から外側は見える

という 「入れ子構造」 をイメージしておくと、この後のクロージャやthisの理解が楽になります。

Hoisting(巻き上げ)と TDZ(Temporal Dead Zone)

var の巻き上げ

var には典型的な「巻き上げ」の挙動があります。

console.log(x); // undefined(エラーではない)
var x = 10;
console.log(x); // 10

内部的には次のように扱われていると思ってください。

var x;              // 宣言だけ先にされる(初期値は undefined)
console.log(x);     // undefined
x = 10;             // ここで代入
console.log(x);     // 10
  • 宣言だけがスコープの先頭に「巻き上げられる」
  • 初期値は自動的に undefined で埋められる

これが var がバグを生みやすい大きな原因です。

let / const と TDZ

let / const も宣言自体は巻き上がりますが、「初期化されるまでのあいだ」が TDZ(Temporal Dead Zone) と呼ばれる状態になり、その間に変数を触るとエラーになります。

console.log(x); // ReferenceError
let x = 10;
console.log(x); // 10

内部イメージ:

// let x;  // ★宣言はスコープの先頭に存在するが、まだ初期化されていない(TDZ)

console.log(x); // ここで触ると「初期化前だからダメ」と怒られる

x = 10;         // ここで初めて初期化される
console.log(x); // 10

const も同じく TDZ がある上に、「宣言と同時に初期化が必須」です。

const y; // SyntaxError(初期値が必須)

const z = 10; // OK

strict mode との関係

  • var
    • strict でも non-strict でも「巻き上げ+初期値 undefined」の挙動は同じ
    • ただし、x = 10; と どこにも var x がない場合は strict ではエラー
  • let / const
    • strict / non-strict に関係なく、TDZ で「宣言前参照は ReferenceError」

モダンコードでは基本 strict mode + let / const 前提なので、
「宣言前に変数を触るとエラーになる」という挙動に慣れておくとよいです。

const とオブジェクト

const は「再代入ができない」だけであって、
オブジェクトの中身が不変(イミュータブル)になるわけではありません。

配列の場合

const nums = [1, 2, 3];

nums.push(4); // OK(配列の中身を変更)
console.log(nums); // [1, 2, 3, 4]

// nums = [1, 2]; // これはエラー(別の配列を入れ直そうとしている)
  • nums という「名前 → 配列オブジェクト」の紐付けは変えられない
  • しかし、紐付けた先のオブジェクトの内容(配列の中身)は操作できる

オブジェクトの場合

const user = {
  name: "Taro",
  age: 20,
};

user.age = 21; // OK
user.country = "Japan"; // OK

console.log(user);
// { name: "Taro", age: 21, country: "Japan" }

// user = {}; // これはエラー(新しいオブジェクトを代入しようとしている)
  • const は「参照先を変えない」
  • 中身を変えたくない場合は Object.freeze などを使う

と理解しておくと、挙動に納得がいきやすくなります。

実務でのおすすめルール

実務では、次のようなシンプルなルールにしているチームが多いです。

  1. デフォルトは const を使う
    • 「後から値を変える必要がある」と判断したときだけ let にする
  2. var は使わない
    • 古いコードを読むときに挙動を理解できれば十分
    • 新しいコードでは ESLint で var を禁止していることが多い
  3. スコープを意識する
    • 「どこからどこまで見えてほしい変数か」を考える
      → なるべく 狭いスコープ(ブロックの中) で宣言する

この講座でも、以降のサンプルは基本的に const / let で書きます。


<<前へ(構文ルールと “use strict”)

>>次へ(執筆中)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次