JavaScriptのvarとlet、constの違いを理解しよう!

JavaScriptを学び始めた方も、すでにある程度経験のある方も、「変数宣言」のキーワードであるvarletconstの違いについて、正確に理解することは非常に重要です。

これらのキーワードは、それぞれ異なる特性を持ち、コードの挙動、可読性、保守性に大きな影響を与えます。特に、ES2015(ES6)でletconstが導入されて以来、JavaScriptの変数宣言のベストプラクティスは大きく変化しました。

この記事では、それぞれのキーワードの基本的な機能から、その違いがコードに与える影響までを詳しく解説し、現代のJavaScript開発における適切な変数宣言の選び方をまとめていきます。信頼できる技術文書であるMDN Web Docsの情報を参考に、理解を深めていきましょう。

JavaScriptの変数宣言、varの基本

varの歴史と役割

JavaScriptの初期から存在していた変数宣言キーワードがvarです。ES2015(ES6)でletconstが導入されるまでは、JavaScriptにおける変数宣言の唯一の方法でした。

varはシンプルながらも、その特性ゆえに開発者が意図しない挙動を引き起こすことがあり、特に大規模なアプリケーション開発においては混乱の元となることもありました。しかし、その簡潔さから小規模なスクリプトやレガシーコードでは依然として広く使われています。

Web開発の歴史を語る上では欠かせない存在であり、JavaScriptの進化を理解するためにもvarの挙動を把握することは重要です。MDN Web Docsによれば、「varキーワードは、オプションで初期値を与えて関数スコープまたはグローバルスコープの変数を宣言します」と説明されています。

この関数スコープという特性が、後に登場するletconstとの大きな違いを生み出すことになります。現代のJavaScriptを学ぶ上で、varの特性を理解することは、過去のコードを読み解き、なぜ新しい変数宣言が導入されたのかを深く理解するための第一歩となります。

varのスコープ(関数スコープ)

varで宣言された変数は、関数スコープを持ちます。これは、変数が宣言された関数内全体からアクセス可能であることを意味します。たとえブロック({})の中に変数を宣言したとしても、その変数はブロックの外側、つまり関数全体で有効になります。

例えば、if文やforループの中でvarを使って変数を宣言した場合、その変数はif文やforループのブロック外でも参照できてしまいます。

function exampleVarScope() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10 が出力される
}
exampleVarScope();

function anotherExample() {
  for (var i = 0; i < 3; i++) {
    // ループ内
  }
  console.log(i); // 3 が出力される (ループ終了後の値)
}
anotherExample();

上記の例では、ifブロック内で宣言されたxifブロックの外からもアクセスできること、またforループ内で宣言されたiがループ終了後も参照できることを示しています。

この特性は、他の多くのプログラミング言語のブロックスコープとは異なるため、JavaScript初心者が陥りやすい混乱の原因の一つでした。関数スコープは、コードの見通しを悪くし、予期せぬ変数の上書きやバグにつながる可能性がありました。

varの巻き上げ(ホイスティング)

varで宣言された変数は、ホイスティング(巻き上げ)という特殊な挙動を示します。これは、JavaScriptエンジンがコードを実行する前に、varで宣言された変数をそのスコープの先頭に移動(巻き上げ)させるかのように振る舞う現象です。

実際にはコードが物理的に移動するわけではなく、変数宣言のみがメモリに登録され、初期化(undefinedが代入される)が行われます。そのため、変数が宣言される前に参照してもエラーにはならず、undefinedが返されます。

console.log(myVar); // undefined が出力される
var myVar = "Hello";
console.log(myVar); // "Hello" が出力される

// これは内部的に以下のように解釈されます
// var myVar;
// console.log(myVar); // undefined
// myVar = "Hello";
// console.log(myVar); // "Hello"

このホイスティングの挙動は、コードの可読性を低下させ、予期せぬバグを引き起こす可能性があります。変数がどこで宣言されているかを正確に把握していないと、undefinedを扱ってしまうことになりかねません。

特に、同じ変数名が異なるスコープでvarによって宣言されている場合、この挙動はさらに複雑な問題を引き起こすことがあります。MDN Web Docsでも、「ホイスティングは、JavaScriptのコードにおける宣言の解析方法が、多くのプログラマが考えるものとは異なるため、混乱の元となる可能性があります」と注意喚起されています。

letとconstの登場とvarとの違い

letの導入とその特性

ES2015(ES6)で導入されたletは、varが抱えていた問題点を解決するために生まれました。letで宣言された変数は、ブロックスコープを持ちます。

これは、変数が宣言されたブロック({}で囲まれた範囲、例えばif文やforループの中)内でのみ有効であることを意味します。ブロックの外からはアクセスできないため、varのように意図しない変数の上書きや参照を防ぐことができます。これにより、コードの予測可能性が高まり、バグの発生を抑制する効果が期待できます。

function exampleLetScope() {
  if (true) {
    let y = 20;
    console.log(y); // 20 が出力される
  }
  // console.log(y); // ReferenceError: y is not defined が発生
}
exampleLetScope();

for (let j = 0; j < 3; j++) {
  // ループ内でのみ j は有効
}
// console.log(j); // ReferenceError: j is not defined が発生

このように、letはスコープをより細かく制御できるため、変数の寿命が短くなり、コードの局所性を高めることができます。

また、letホイスティングされますが、TDZ(Temporal Dead Zone)と呼ばれる期間が存在します。これは、スコープの先頭から変数が宣言される行までの間、その変数にアクセスしようとするとReferenceErrorが発生するというものです。これにより、宣言前に変数を使用することによる混乱を避けることができます。

constの導入と不変性

constもES2015(ES6)で導入された変数宣言キーワードで、letと同様にブロックスコープを持ちます。しかし、constの最大の特徴は、一度宣言した変数に再代入ができない点、つまり不変性を持つ点にあります。

constで宣言された変数には必ず初期値を代入する必要があり、後からその値を変更しようとするとTypeErrorが発生します。これは、その値がプログラムの実行中に変わることがない定数を表すのに最適です。

const PI = 3.14159;
// PI = 3.0; // TypeError: Assignment to constant variable. が発生

const USER_NAME = "Alice";
// USER_NAME = "Bob"; // TypeError

const COLORS = ["red", "green"];
COLORS.push("blue"); // これは可能!配列の内容は変更できる
console.log(COLORS); // ["red", "green", "blue"]

// COLORS = ["yellow"]; // TypeError: Assignment to constant variable. が発生

重要な注意点として、constが変数の「値そのもの」を不変にするわけではない、という点があります。オブジェクトや配列をconstで宣言した場合、そのオブジェクトや配列への参照は変更できませんが、参照が指すオブジェクトや配列の内部のプロパティや要素は変更可能です。

上記の例でCOLORS配列にpushできるのはこのためです。constは、あくまで変数への再代入を禁止することで、コードの意図を明確にし、予期せぬ値の変更を防ぐのに役立ちます。

varとlet/constの主な違い比較

varletconstの最も重要な違いは、そのスコープ再代入の可否、そしてホイスティングの挙動にあります。これらの違いを理解することが、現代のJavaScriptで適切な変数宣言を選択する鍵となります。以下の表に主要な違いをまとめます。

特徴 var let const
スコープ 関数スコープ/グローバルスコープ ブロックスコープ ブロックスコープ
再代入 可能 可能 不可能(一度宣言したら変更不可)
再宣言 可能 不可能(同じスコープ内で不可) 不可能(同じスコープ内で不可)
初期化 任意(初期値なしで宣言可能) 任意(初期値なしで宣言可能) 必須(宣言時に初期値が必要)
ホイスティング される(undefinedで初期化) されるがTDZがある されるがTDZがある

出典: MDN Web Docsの各変数宣言キーワードに関する情報をもとに作成。

この表が示すように、varは自由度が高い反面、予期せぬ挙動を生み出すリスクがあります。一方、letconstはより厳格なルールを持ち、これによってコードの信頼性と保守性が向上します。

特に、ブロックスコープの導入は、JavaScriptが他のC系言語とより一貫性のある変数スコープモデルを持つようになったことを意味します。現代のJavaScript開発では、これらの違いを理解し、適切に使い分けることが求められます。

ブロックスコープとvarの挙動

ブロックスコープとは何か

ブロックスコープとは、変数が宣言された{}(ブロック)の内側でのみ有効となるスコープのことです。これは、if文、forループ、whileループ、function文、または単独のブロックなど、波括弧で囲まれた任意のコードのまとまりを指します。

ブロックスコープを持つ変数は、そのブロックを抜けると参照できなくなり、メモリからも解放されます。この特性により、変数の生存期間を限定し、コードの局所性を高めることができます。多くのプログラミング言語(C++, Java, Pythonなど)では、このブロックスコープが一般的であり、変数の管理をシンプルかつ安全に行うための基本的な考え方となっています。

例えば、ループ処理の中で一時的に使用する変数をブロックスコープで宣言すれば、その変数がループの外に漏れ出して他の変数と衝突したり、意図しない場所で参照されたりする心配がなくなります。これにより、大規模なアプリケーション開発において、変数の命名衝突やサイドエフェクトのリスクを大幅に減らすことが可能です。MDN Web Docsでは、ブロックスコープがJavaScriptの変数の振る舞いをより予測可能にする重要な機能であると強調されています。

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

letconstで宣言された変数は、このブロックスコープに従います。これは、varが持つ関数スコープとは根本的に異なる点であり、現代のJavaScriptプログラミングにおいて非常に重要です。

if文、forループ、try...catchブロックなど、{}で囲まれた任意のブロック内でletconstを使って変数を宣言すると、その変数はそのブロック内でのみ有効となります。ブロックの外側から同じ名前の変数を参照しようとすると、ReferenceErrorが発生します。

function blockScopeExample() {
  let a = 1;
  if (true) {
    let b = 2; // b はこの if ブロック内のみで有効
    const C = 3; // C もこの if ブロック内のみで有効
    console.log(a); // 1 (関数スコープの変数にはアクセス可能)
    console.log(b); // 2
    console.log(C); // 3
  }
  console.log(a); // 1
  // console.log(b); // ReferenceError: b is not defined
  // console.log(C); // ReferenceError: C is not defined
}
blockScopeExample();

この挙動により、開発者はより安心して一時的な変数をブロック内で宣言できるようになります。例えば、forループ内でletを使ってループカウンタを宣言した場合、各イテレーションごとに新しい変数が生成されるかのように振る舞うため、非同期処理と組み合わせた際にも期待通りの結果が得られやすくなります(クロージャ問題の回避)。これは、varを使用した場合によく見られた問題の一つを解決します。

varがブロックスコープを持たないことの影響

varがブロックスコープを持たず、関数スコープを持つことは、特にループ処理や条件分岐において、予期せぬ挙動やバグの原因となることが多くありました。例えば、forループの中でvarを使って変数を宣言した場合、その変数はループが終了しても残り続け、最終的な値がループの外からアクセスできてしまいます。

// var がブロックスコープを持たない例
var callbacks = [];
for (var i = 0; i < 3; i++) {
  callbacks.push(function() {
    console.log(i); // ここで i の値を参照
  });
}
callbacks.forEach(function(callback) {
  callback(); // すべて 3 を出力する
});

上記の例では、本来であれば0, 1, 2と出力されることを期待しますが、実際にはすべてのコールバックがループ終了時のiの値(つまり3)を参照してしまうため、すべて3が出力されます。これは、var iが関数スコープを持つため、ループ全体で同じi変数を共有し、クロージャがその最終的な値をキャプチャしてしまうために発生します。この「クロージャ問題」は、varの関数スコープの典型的な副作用であり、JavaScript開発者にとって長年の課題でした。

letを使用すると、各ループイテレーションで新しい変数が生成されるため、この問題は解決されます。

// let ならブロックスコープにより問題が解決する例
var callbacksLet = [];
for (let i = 0; i < 3; i++) {
  callbacksLet.push(function() {
    console.log(i); // 各イテレーションの i の値を参照
  });
}
callbacksLet.forEach(function(callback) {
  callback(); // 0, 1, 2 を順に出力する
});

このように、varのスコープの挙動は、特に非同期処理やクロージャと組み合わせた場合に複雑なバグを引き起こす可能性があり、これがletconstの導入を促した大きな理由の一つです。

JavaScriptの変数宣言まとめ:letとconstを使いこなす

現代のJavaScriptにおける推奨される使い方

現代のJavaScript開発では、varの使用は推奨されません。その代わりに、letconstを積極的に使用することがベストプラクティスとされています。この方針は、コードの予測可能性を高め、潜在的なバグを減らし、チーム開発におけるコードの一貫性を保つ上で非常に有効です。

具体的には、まずconstを使用することを第一に検討し、その変数が後で再代入される可能性がある場合にのみletを使用するというのが一般的な推奨事項です。

MDN Web Docsや多くの技術コミュニティでは、この「constをデフォルトに、必要ならlet」というアプローチを提唱しています。これにより、開発者は変数の値が変更される可能性のある箇所を意識しやすくなり、コードの意図がより明確になります。例えば、設定値や定数、一度セットされた後で変わらない参照などを宣言する場合はconstが適しています。一方、ループカウンタや、ユーザーの入力に応じて値が更新される変数など、再代入が必要な場合にはletを使用します。

適切な変数宣言の選択基準

varletconstの中から適切なキーワードを選択するための基準をまとめます。

  • 変数の値が変更されない場合(定数):
    • constを使用します。これは、再代入を禁止することで、意図しない値の変更を防ぎ、コードの堅牢性を高めます。
    • 例: const API_KEY = "your_api_key";, const MAX_ITEMS = 10;
    • オブジェクトや配列をconstで宣言した場合も、その参照自体は不変ですが、内部のプロパティや要素は変更可能である点に注意が必要です。
  • 変数の値が後で変更される可能性がある場合:
    • letを使用します。変数を宣言した後に、その値を再代入する必要がある場合に適しています。
    • 例: ループカウンタ (for (let i = 0; ...)), ユーザー入力によって更新される変数 (let userName = "Guest";)
    • letはブロックスコープを持つため、varのようなスコープの混乱を防ぎます。
  • varの使用:
    • 原則として新しいコードでは使用しないようにしてください。
    • レガシーコードの保守や、特定の古い環境での互換性を維持する必要がある場合にのみ検討します。しかし、ほとんどの場合、トランスパイラ(Babelなど)を使用すれば、letconstで記述したコードを古い環境でも実行できるように変換できます。

この選択基準に従うことで、コードの意図が明確になり、他の開発者がコードを読み解く際の手助けとなります。

コード品質と保守性への影響

letconstを適切に使いこなすことは、単に現代のJavaScriptの機能を使うというだけでなく、コード全体の品質と保守性に大きな影響を与えます。

  1. バグの削減:

    constを使用することで、変数が意図せず再代入されることによるバグを防ぐことができます。また、letconstのブロックスコープは、変数の影響範囲を限定し、予期せぬ副作用や命名衝突のリスクを低減します。MDN Web Docsでも、「ブロックスコープは、多くのプログラミング言語に見られる通常のスコープルールと一致するため、コードをより読みやすく、バグを少なくするのに役立ちます」と指摘されています。

  2. コードの可読性向上:

    コードを読む際、constで宣言された変数を見れば、その値が変更されることがない「定数」として扱われていることがすぐに理解できます。一方、letで宣言された変数であれば、その値が後で変更される可能性があることが読み手にも伝わります。このように、宣言キーワードが変数の意図を伝える役割を果たすことで、コードの可読性が大幅に向上します。

  3. リファクタリングの容易さ:

    ブロックスコープと再代入の制約により、コードの特定の部分を変更する際に、その変更が他の広範囲にわたる影響を与えにくい構造になります。これにより、リファクタリングがより安全かつ容易に行えるようになり、大規模なプロジェクトでもコードベースを健全に保つことができます。

結論として、letconstの適切な使用は、より堅牢で、理解しやすく、メンテナンスしやすいJavaScriptコードを書くための不可欠な要素です。現代のJavaScript開発者にとって、これらのキーワードをマスターすることは必須のスキルと言えるでしょう。

その他JavaScriptの便利な機能(void, XORなど)

`void`演算子の活用

JavaScriptには、値を評価し、常にundefinedを返すvoid演算子というものがあります。この演算子は、主に特定の文脈で予期せぬ副作用を避けるため、または、式を評価しつつその結果を無視したい場合に使用されます。

最も一般的なユースケースは、HTMLのアンカータグ(<a>)のhref属性でJavaScriptコードを実行しつつ、ページ遷移を防ぎたい場合です。

<!-- 古いJavaScriptコードや特定のフレームワークで見られる用法 -->
<a href="javascript:void(0)">何もせずリンクに見える要素</a>
<a href="javascript:void(alert('Hello!'))">警告を表示するがページは遷移しない</a>

void演算子は、式の結果をundefinedにするため、上記のようにhref属性に指定されたJavaScriptコードがURLとして解釈されることを防ぎます。ただし、現代のJavaScript開発では、このような目的でvoidを使用するよりも、イベントリスナー(addEventListener)を使用してイベントのデフォルト動作をpreventDefault()でキャンセルする方が推奨されています。

MDN Web Docsでも、voidの使用は「ほとんどの場合、その代わりにundefinedを直接使用する方が適切です」と述べられており、その使用頻度は減少傾向にあります。

XOR演算子(`^`)の応用

JavaScriptのビット演算子の一つであるXOR(排他的論理和)演算子 ^は、2つのオペランドのビットを比較し、対応するビットが異なる場合にのみ1を返します。この演算子は、主に数値のビット操作に使われますが、特定のアルゴリズムやデータの暗号化、シンプルな値の交換など、意外な場所で役立つことがあります。

例えば、一時変数を使わずに2つの数値を交換するテクニックとしてXORが利用されることがあります。

let a = 5;  // バイナリ: 0101
let b = 10; // バイナリ: 1010

console.log(`交換前: a = ${a}, b = ${b}`);

a = a ^ b;  // a = 0101 ^ 1010 = 1111 (15)
b = a ^ b;  // b = 1111 ^ 1010 = 0101 (5)  => b は元の a の値になる
a = a ^ b;  // a = 1111 ^ 0101 = 1010 (10) => a は元の b の値になる

console.log(`交換後: a = ${a}, b = ${b}`); // 交換後: a = 10, b = 5

このテクニックはコードの短縮にはなりますが、可読性を損なう可能性があり、現代のJavaScriptでは単に一時変数を使うか、配列の分割代入([a, b] = [b, a])を使う方が一般的で推奨されます。しかし、XOR演算子の基本的な理解は、低レベルのデータ操作や特定のアルゴリズムを理解する上で重要です。また、データのハッシュ化やチェックサムの計算など、より高度な用途で利用されることもあります。

その他の覚えておくと便利な小技

JavaScriptには、開発効率を高めるための小さな機能や慣習が数多く存在します。これらを活用することで、コードをより簡潔に、そして読みやすく記述することができます。

  1. テンプレートリテラル (Template Literals):

    バッククォート(` `)で囲むことで、複数行の文字列や、変数を埋め込んだ文字列を簡単に作成できます。

    const name = "World";
    const greeting = `Hello, ${name}!
    This is a multi-line string.`;
    console.log(greeting);

    これは、以前の文字列結合(+)や改行エスケープ(\n)に比べて非常に便利です。

  2. 分割代入 (Destructuring Assignment):

    配列やオブジェクトからプロパティを抽出し、個別の変数に代入するのに役立ちます。

    const person = { firstName: "John", lastName: "Doe" };
    const { firstName, lastName } = person;
    console.log(firstName, lastName); // John Doe
    
    const numbers = [1, 2, 3];
    const [first, second] = numbers;
    console.log(first, second); // 1 2

    これにより、コードが簡潔になり、必要なプロパティだけを取り出すことができます。

  3. スプレッド構文 (Spread Syntax):

    配列やオブジェクトを展開して、新しい配列やオブジェクトを作成する際に使われます。

    const arr1 = [1, 2];
    const arr2 = [...arr1, 3, 4]; // [1, 2, 3, 4]
    const obj1 = { a: 1, b: 2 };
    const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }

    配列の結合やオブジェクトのコピー、関数への引数渡しなど、多様な場面で活用できます。

これらの機能は、ES2015以降に導入されたものであり、現代のJavaScript開発では頻繁に利用されます。MDN Web Docsにはこれらの機能に関する詳細なドキュメントが豊富に用意されており、さらに深く学びたい場合は参照すると良いでしょう。効率的で読みやすいコードを書くために、積極的にこれらの「小技」を取り入れてみてください。