[JavaScript]var宣言は本当にNGかという事を考察してみた

JavaScript

JavaScriptという言語には、変数宣言の方法として3つの方法が定義されています。
const, let, varというキーワードを使用します。
このうち、varで宣言するという方法は、何故かあらゆるサイト/書籍で使用しないように記載されています。

今回は、このvarというキーワードで宣言する方法が忌み嫌われているという事について、いったいどうしてそのような風潮があるのか、使うとどうなるのかを確認し、そのうえでうまく使う道はないのか、私の見解とともに検証してみたいと思います。

変数にはスコープという概念が常に付きまといますので、このスコープという概念も少しだけ触れながら進めたいと思います。

いつもの通りindexがあるので、好きなところからどうぞ。

[おさらい]3つの変数宣言とそれぞれの特徴を確認

JavaScriptの勉強を始めるとすぐにぶち当たるのが3つの変数宣言方法です。
const, let, varの3つです。
それぞれ異なる特徴のある宣言方法なのですが、少しだけ歴史的背景によって使い分けされています。
varについて触れる前に、まずは3つの宣言方法について特徴的な違いをおさらいしますね。

1-1. const宣言

他のプログラミング言語をご存じの方がconstという名前を見ると、たいていは定数定義のための宣言かと思われると思います。
私もそう思いました。
実際、概ねそうなのですが、書籍によっては厳密には定数ではないと説明されているので少しその部分について説明します。

stringやnumberなどの単純値を扱う場合、const宣言した変数は実際に定数として機能します。
変数定義と同時に初期化が必要です。
変数定義の後に値を変更しようとするとTypeErrorになります。

const fruitName = 'バナナ'; // 変数定義と同時に初期化する
fruitName = 'メロン'; // NG : TypeError: Assignment to constant value.

ですが、クラスやオブジェクトや構造体といった、複雑な構造をもったインスタンスの場合、インスタンス自体を変更しようとするとエラーになりますが、インスタンスのメンバー変数は変更できます。

const fruit = { name: 'バナナ', price: 150 };
fruit = { name: 'メロン', price: 1000 }; // TypeError
fruit.name = 'いちご'; // OK.
fruit.age = 350; // OK.

これが、「厳密には定数ではない」と言われているゆえんです。

この特性を理解したうえで定数として利用するのは全く問題ないと思います。

JavaScriptの世界では、一般的にはconst宣言された変数の使用が推奨されています。
理由は、「変数の使いまわし」というバグの温床になったり可読性の劣化につながる使い方を防止することが目的のようです。

個人的には、同じ意味の値を保持する変数として使用する前提で、使いまわしするのは全く問題ないと思っており、無理にconst宣言に固執する必要はないと思います。

例えば、あるプログラムロジックにおいて、初期値を代入した後に、ロジックの結果によって再計算した結果を再代入する場合もあるという事も少なからずあります。
その時に、いちいち初期値用の変数と算出結果用の変数2つを定義するという事のほうが、よっぽど煩雑化する可能性が高いと考えます。

明確に定数として宣言できる値以外は、次に説明するletを使うというように、明確なポリシーをもって使い分ければ全く問題ないと思います。

1-2. let宣言

続いてletです。
再代入可能な変数を宣言できます。
変数の初期化は変数宣言の後でもできますが、その間、変数はundefinedになります。

let fruitName;
console.log(`Fruit name is ${fruitName}.`); // Fruit name is undefined.
fruitName = 'いちご'; // OK
console.log(`Fruit name is ${fruitName}.`); // Fruit name is いちご.

再代入が必要なシーンとしては、例えばforやwhileなどの繰り返し要素で同じ変数に何度も異なる値を代入する必要がある場合に使用します。

const  fruits = [ 'バナナ', 'いちご' ];
for (let  idx = 0; idx < fruits.length; idx++) {
    console.log(`Fruit name is ${fruits[idx]}`);
}

That’s it !

1-3. var宣言

最後にvarです。
この宣言による変数も、再代入可能です。

var fruitName;
console.log(`Fruit name is ${fruitName}.`); // Fruit name is undefined.
fruitName = 'いちご'; // OK
console.log(`Fruit name is ${fruitName}.`); // Fruit name is いちご.

もちろん再代入が必要なシーンでも利用できます。
※変数の定義位置がletと異なっていますが、この理由はこの後説明します。

var idx;
const  fruits = [ 'バナナ', 'いちご' ];
for (idx = 0; idx < fruits.length; idx++) {
    console.log(`Fruit name is ${fruits[idx]}`);
}

上記3つの変数宣言について簡単に説明してきました。
letとvarは全く同じ用途で使えるように思えますが、何が違うのでしょうね。

結論から言うと、以下2つの重要な違いがあります。

  1. var宣言の変数は、スコープに関係なく同じ名前の変数が再定義ができる
  2. var宣言の変数は、宣言位置が自動的に関数スコープもしくはグローバルスコープの先頭になる(「巻き上げ」といわれる現象)

それをこれから確認していきます。

var宣言の変数は、スコープ内で同じ名前の変数が再定義できる

varというキーワードによる変数宣言は、ES2015というバージョンになる前までバリバリ使われていたのですが、今となっては「諸悪の権化」とまで言わんばかりに使ってはならぬとあらゆるサイトや書籍で紹介されています。
その理由の一つに「同名変数の重複宣言による混乱」があります。

次のコードでは、同じ変数名が別々の目的で定義されており、その後、最初に設定された値を想定した用途で使用されています。

var  itemPrice = 100; // 商品価格の値
var  taxRate = 1.03; // 税率の値(今は懐かしいレート)

var  itemPrice = '商品価格'; // 商品価格のラベル名
var  taxRate = '税率'; // 税率のラベル名

console.log(`${itemPrice}の税込み価格は${itemPrice * taxRate}です。`);

itemPriceという変数とtaxRateという変数が2度定義されていますが、エラーにはなりません。また、最後には、最初に設定された数値を想定して四則演算の結果を想定したログメッセージを生成していますが、設定されているのは文字列です。それにもかからず、四則演算の部分においてさえ、エラーにはならず、想定外のログメッセージが 正常に出力されます。

ログメッセージが出力されるという事は、ログメッセージを生成する処理でもエラーとして検出されないという事です。
先ほど説明した通り、itemPrice * taxRateという四則演算部分がありますが、この部分でさえも文字列同士の四則演算結果として”NaN”という文字列が生成されるだけであり、エラーになりません。

これ自体、ツッコミたくなる言語仕様なのですが、、この記事で言及したいことからズレますので一旦無視します。

このプログラムは非常に短いので、こんなミスしないと思いますが、本格的で大きなプログラムの場合、これが原因でバグが混入する可能性が非常に高くなります。
そして、これがエラーとして検出されないため、想定した値が入っていない状態がどこで発生したのかを追跡するのがどんどん難しくなっていきます。

これに加えて、次に説明するvar宣言の変数に特有の現象によって、正しいコードを書く難易度はさらに上がります。

var宣言の変数は、宣言位置が自動的に関数スコープもしくはグローバルスコープの先頭になる

var宣言の変数というのは、どこで宣言されていようが、自動的に対象スコープの先頭で宣言されたように動作するという現象があります。

ここで、「変数のスコープ」という概念の理解が必要になるので少し説明します。

3-1. 変数のスコープ

変数の参照可能な範囲のことを「スコープ」といいます。
変数には 「ブロックスコープ」「関数スコープ」「グローバルスコープ」という3つのスコープがあります。

※ES2015以降、classという構文も導入されましたが、これは別の記事で詳しく取り上げたいと思います。

(1) ブロックスコープ

ブロックスコープは、処理の流れで一つの処理ブロックを定義するときに中かっこで囲んだ部分を言います。
主には、ifによる分岐、forやwhileによるループなどで使用しますが、単純に中かっこで囲みスコープを明確に分けたい場合に利用します。

処理;
if (条件式) {
    // if分岐によるブロックスコープ
    const a = "a";
}
for (繰り返し条件) {
    // forループによるブロックスコープ
    let b = "b";
}
{
    // 単純にスコープを分けたい場合にはこのように書くこともできる
    var c = "c":;
}
処理;
・・・

(2) 関数スコープ

関数スコープは、関数を定義するときに関数全体を中かっこで囲んだ部分を言います。

// 関数宣言
function func1() {
    // 関数スコープ
    const a = "a";
    ・・・
    return a;
}

// 関数式
const func2 = function() {
    // 関数スコープ
    let b = "b";
}

(3) グローバルスコープ

グローバルスコープとは、一番最上位のスコープで、ここに宣言した変数は、関数内でもブロック内でも、どこからでもアクセス可能となります。いわゆる グローバル変数 です。

// グローバルスコープ
const a = "a";
let b = "b";
var c = "c";

3-2. var宣言された変数の宣言位置の「巻き上げ」

スコープについて説明しましたので、「var宣言の変数というのは、どこで宣言されていようが、自動的に対象スコープの先頭で宣言されたように動作する」= var宣言された変数の宣言位置の巻き上げ という現象について説明していきます。

書籍や他のサイトではこの現象を「var変数の巻き上げ」という言い方で説明していますが、英語直訳のこの言い方は正直なじめないので、少し長いですが「var宣言された変数の宣言位置の巻き上げ」と記載しました。
影響力があり発信力のある誰かが、もっと分かりやすい言い方で発信してほしい。。

この現象が発生するのは以下の2か所で発生します。

  • 関数スコープ内に定義された ブロックスコープ
  • グローバルスコープ内に定義された ブロックスコープ

「宣言位置の巻き上げ」という表現をした理由は、var宣言された変数は、プログラム上どこで定義されても、宣言位置と使用位置が分離されたような挙動になるからです。

例えば、以下のようなプログラムでvar宣言の変数を定義します。

console.log(`var変数bが定義されていないとき、変数bには[${b}]が設定されています`);
var b = "1";
console.log(`bに値を設定しました。今変数bには[${b}]が設定されています`);

この時、コンソールにはどのような結果が出力されるかというと、以下のようになります。

var変数bが宣言されていないとき、変数bには[undefined]が設定されています
bに値を設定しました。今変数bには[1]が設定されています

const宣言やlet宣言された変数の場合、変数宣言前に変数を参照するとundefinedエラーが発生しますが、 var宣言された変数はエラーにならず、undefinedという値が代入されている という結果になります。

これは、以下のようにプログラムされた事と同義です。

var b;
console.log(`var変数bが定義されていないとき、変数bには[${b}]が設定されています`);
b = "1";
console.log(`bに値を設定しました。今変数bには[${b}]が設定されています`);

要するに、 変数の宣言位置が使用位置ではなく、対象スコープの先頭で行われたような動作になる という事です。

この動作は var宣言された変数のみ に発生します。
これがJavaScriptの言語仕様なのか、JavaScriptエンジン自体の仕様なのか分かりませんが、過去に開発されたプログラムとの互換性を保つために残されているらしいです。

これが「var宣言された変数の宣言位置の巻き上げ」という現象(言語仕様?エンジン仕様?)です。

3-3. 関数スコープ内に定義されたブロックスコープで発生する「巻き上げ」を検証

まずは関数スコープ内に定義されたブロックスコープで発生する「巻き上げ」について確認していきます。
以下のプログラムでは、関数内にif分岐によるブロックスコープがあり、その中でvar宣言の変数を定義しています。
このプログラムで2つのことが分かります。

  • 関数f()にあるif分岐ブロックスコープ内のvarBlockInFunctionという変数は、関数スコープ内で参照可能になっている (関数スコープまで巻き上げが発生している)
  • 関数f()にあるvarFunctionという変数と、先ほどのvarBlockInFunctionという変数は、どちらもグローバルスコープからは参照できない (巻き上げは関数スコープまでであり、関数の外までは巻き上げは発生していない)
var  varGlobal = "var in the global scope.";

console.log("-- グローバルスコープで関数スコープ内のvar変数を参照する");
console.log(varFunction); // ReferenceError: 関数スコープの変数は関数の外から参照できない

console.log("-- グローバルスコープで関数内かつブロックスコープ内のvar変数を参照する");
console.log(varBlockInFunction); // ReferenceError: 関数内ブロックスコープの変数は関数の外から参照できない

var  f = function () {
    var  varFunction = "var in the function scope.";

    console.log("-- 関数スコープ内でグローバルスコープのvar変数を参照する");
    console.log(varGlobal); // グローバルスコープの変数は関数内から参照可能

    console.log("-- 関数スコープ内で、関数内かつブロックスコープのvar変数を参照する");
    console.log(varBlockInFunction); // 関数内ブロックスコープの変数は関数内から参照可能

    if (true) {
        var  varBlockInFunction = "var in the block in the function.";

        console.log("-- 関数内かつブロックスコープからグローバルスコープのvar変数を参照する");
        console.log(varGlobal); // グローバルスコープの変数は関数内ブロックスコープから参照可能
        console.log("-- 関数内かつブロックスコープから関数スコープのvar変数を参照する");
        console.log(varFunction); // 関数スコープの変数は関数内ブロックスコープで参照可能
    }
}
f();

var宣言の変数で混乱しないようにプログラムする方法

ここまでで、var宣言された変数に関する2つの特性を見てきましたが、これらが原因でプログラムデバッグがいかに困難になるのかイメージできますかね。。

var宣言の変数は、

  1. スコープに関係なく同じ名前の変数が再定義できる
  2. 宣言位置が自動的に関数スコープもしくはグローバルスコープの先頭になる(「巻き上げ」といわれる現象)

どちらも結構厄介な挙動というか仕様というか、理解して使わないと大変なことになります。

ですが、これが混乱のもととなるのは特定の状況の場合のみだと私は思います。
それは、プログラムが適切にデザインされていない(設計原理から外れた思想でコードを書いている)という状況です。

特にvar宣言の変数による弊害を最小化するという観点において、以下2つの状態になることを避ける必要があります。

  • グローバルスコープで数百行以上の規模のコードを書いている(これはvar宣言の変数うんぬんの話ではなく、プログラミングのリテラシーが低すぎて基本的にNGです。。)
  • 1つの関数に複数の機能を実装する

グローバルスコープでコードを書き続けるという事は、設計を意識しないエンジニアや初級エンジニアはこれをやりがちです。
また、1つの関数に、複数の機能が含まれている場合も、関数内でそれぞれの機能に対して同じ変数名が使用されるという事は容易に想像ができますので、var宣言による変数の特性が悪い方向に働く可能性があります。

要するに、ある程度まともに設計すれば、var宣言された変数を使っても、きちんとデバックできる保守性に優れたプログラムを書く事は可能だという事です。

とは言いつつも、ちゃんと言語仕様で担保されている方法でコードを書いたほうが無難ですし、チームで開発している場合は1つのマナーでしょうね。

まとめ

長々と書いてきましたが、言いたかったことは以下の2つです。

  1. 少し設計を勉強したエンジニアなら、varですら行儀よく使えるという事実
  2. グループ開発するなら、他エンジニアへのマナーとして、「varを使わず、constもしくはletを使用せよ」という理解

自分ひとりでコード書くなら、自分が困らない程度に好きなように書けばいいでしょう。言語仕様として禁止されていないので。
でもグループ開発の場合は自分よがりなコードはダメよという事です。

コメント

タイトルとURLをコピーしました