やれlet
を使え、やれconst
を使え、と言われているES2015+時代。var
がなぜ問題だったのか考えます。
他のC風言語とは違う!
JavaScript(ECMAScript)はC風言語(C-family programming languages)の一つと言われています。List of C-family programming languagesの仲間であり、その中の一つとして数えられています。他にメジャーなものでは、C、C++、Java、C#、Perl、PHPあたりでしょう。PerlとPHPは変数名の規則に違いがある1ので除くとしても、他の言語とJavaScriptとでは変数の取り扱い方に大きな違いがありました。
ブロックスコープでは無い
まず、C、C++、Java、C#とES5以前のJavaScriptにはスコープに対する決定的な違いがありました。それはif文やwhile文等のブロックがJavaScriptではスコープを作らなかったと言うことです。
#include <stdio.h>
int main(void)
{
int i = 42;
char *str = "hoge";
for (int i = 0; i < 10; i++) {
char *str = "fuga";
printf("%s %d\n", str, i);
}
printf("%s %d\n", str, i); // => hoge 42
return 0;
}
var i = 42;
var str = "hoge"
for (var i = 0; i < 10; i++) {
var str = "fuga";
console.log(str, i);
}
console.log(str, i); // => fuga 10
上のCとJavaScriptでは同じようなことをしているように見えますが、最後に表示される数値が異なります。ES5以前ではfor文でブロックスコープを作らないため、var
で宣言されたi
やstr
のスコープは全体となり、for文内とi
やstr
も同じものであるという扱いになります。これは、C等の他のC風言語を扱っている人から見ると奇妙に思えるものでした。そこで登場したのがlet
です。
ES2015からはブロックスコープが導入され、let
で宣言された場合は変数がブロックスコープになるようになりました。
let i = 42;
let str = "hoge";
for (let i = 0; i < 10; i++) {
let str = "fuga";
console.log(str, i);
}
console.log(str, i); // => hoge 42
let
で書き換えると、Cと同じ動作になることがわかります。ただし、ES2015+であっても、var
で宣言した変数は関数スコープ(トップレベルの場合はグローバルスコープやモジュールスコープ)のままです。
勘違いしないで欲しいのは「if等でブロックスコープが作られないことが悪い」ということではありません。C風言語ではないPythonやRubyではif等でブロックスコープが作られません2。ですが、if等で{}
を使っているわけでは無いため、それほど混乱することはありません。C風言語では文のひとまとめに{}
を使うため、これで一つのブロックを形成し、一つのまともまりを持つと考えるのが多数です。そのため、その中で宣言されたローカル変数はそのブロック内でしか使えないというのが半ば常識のように扱われていました。その常識から逸脱するJavaSriptのvar
は奇妙であり、勘違いによるバグを産み出す原因の一つになっていたと考えられます。
なお、const
についてもlet
と同様です。
宣言の巻き上げ
var
には巻き上げという現象が起きます。Cと比べて見ましょう。
#include <stdio.h>
int i = 42;
int main(void){
printf("%d\n", i); // => 42
int i = 2;
printf("%d\n", i); // => 2
}
var i = 42;
function main() {
console.log(i); // => undefined
var i = 2;
console.log(i); // => 2
}
main();
二番目のi
はCとJavaScriptとでは対象となっているi
が違います。Cの場合はグローバル変数のi
ですが、JavaScriptではmain
関数のローカル変数であるi
です。JavaScriptのvar
はどこで宣言したとしても、そのスコープの最初に宣言したものと見なします。そして、初期化前であれば、暗黙的にundefined
が入っていると見なします。つまり、下記のコードと同じです。
var i = 42;
function main() {
var i;
console.log(i); // => undefined
i = 2;
console.log(i); // => 2
}
main();
サンプルのコードはすぐ近くにあるので気づけますが、関数の行数が多い場合(それ自体はあまり良いものとは言えませんが)、二番目のようなi
がトップレベルのi
だと勘違いしてしまうかも知れません。問題なのは、このコードがエラーにならないと言うことです。
ES2015のlet
を使えば、この勘違いに即座に気づけます。
let i = 42;
function main() {
console.log(i); // => ReferenceError
let i = 2;
console.log(i);
}
main();
上のコードではReferenceErrorが発生し、コードは停止しますので、テストの時に気づくことができます。注意して欲しいのはCのような動作では無いと言うことです。let
であっても宣言自体の巻き上げは発生します。しかし、暗黙的にundefined
を入れると言うことをするのではなく、初期化前に評価されるとエラーが発生するようにしています。
なお、const
についてもlet
と同様です。
多重宣言
C等では同じ変数名で二重に宣言することはできません。往々にして、多重宣言は変数名の重複を意味し、どこか間違っているからです。
#include <stdio.h>
int main(void)
{
int i = 42;
int i = 54; // => コンパイルエラー
printf("%d\n", i);
return 0;
}
しかし、JavaScriptのvar
では何のエラーも無くできてしまいます。
var i = 42;
var i = 54;
console.log(i); // => 54
これも同じく、let
を使えば防ぐことができます。
let i = 42;
let i = 54; // => SyntaxError
なお、const
についてもlet
と同様です。
再代入可能
var
には再代入を抑止する方法がありません。しかし、他の言語では再代入できないようにすることは重要なことだと考えられています。
#include <stdio.h>
int main(void)
{
char *const str = "hoge";
str = "fuga"; // => コンパイルエラー
printf("%s\n", str);
return 0;
}
ES2015以降では、このような再代入禁止を行う為にconst
が用意されました。
const str = "hoge";
str = "fuga"; // = > TypeError
console.log(str);
const
は宣言時の初期化子でのみ代入が可能であり、それ以降は代入ができません。
そして、解決したのか?
C風に合わせただけ
色々見てきましたが、let
やconst
がしていることは、他のC風言語と同じような動きに合わせただけと言っても過言ではありません。もっと言えば、別にvar
を使ってもうまくいっていたのです。ただ、C風な文法なのにC風の動きでは無いという所が多くのプログラマーを惑わせ、わかりにくくしていたと考えられます。
逆に言うとC風の文法で無ければ、var
のような動きでもかまわないと考えられます。実際、PythonやRubyのローカル変数の動きはvar
に近い物がありますし、PythonとRubyの文法を参考に作られたCoffeeScriptでは、バージョン2を作るときもシンプルさを重視し、let
とconst
を採用しませんでした。
JavaScriptの不幸は、すべてJavaの文字にかかっています。JavaScriptが純粋にSchemeをブラウザの世界に持ってきたものであれば、LISPのような構文になっていれば、この問題は起きなかったはずです。しかし、それを無理矢理Javaと似たもの、つまり、C風の文法にしてしまったのが、このC風言語開発者にとって奇妙に思うようなvar
が産まれた理由と推測されます。一度できてしまい、ブラウザ戦争で急激に発展し、Ajaxによってブレイクして、Web周りの中心技術の一つになったとき、やっと、当時の間違いに気付いたのです。
つまり、let
やconst
でJavaScriptが便利になったというのでは無く、やっと、C風言語の仲間入りができたというものと思われます。もっと別の文法であれば、きっとそれらは必要なかったのだと思います。
constと言う名前は適切か?
Javaではfinal
、C#では(フィールドだけですが)readonly
が使われています。C風とはいうのはいささか無理があるかも知れませんが、Scalaはval
を使っています。でも、なぜconst
なのでしょうか?
C/C++はconst
がありますが、型にかかります。const char *s
とchar *const s
では意味が違います。JavaScriptのconst
は後者の意味に近いですが、前者の意味だと勘違いするものも多々見られます。C#にもconst
はありますが、こちらはコンパイル時定数の意味であり、Cで言えばマクロ定数、C++で言えばconstexpr
に近いものであり、さらに様相が異なります。
考えてみればconst
の前に「定数」はとても曖昧です。「定数」という言葉は
- 変数の評価値が定まっている。しばしば、コンパイル時に評価され、それに置き換えられている。
- 変数が束縛されているオブジェクトの値が定まっている。
- 変数が束縛されているオブジェクトが定まっている。
の三つの意味で使われる場合があります。JavaScriptのconst
は3.ですが、C#のconst
は1.であり、C/C++のconst
は(かかっている型に対して)2.になります。const
という言葉はとても曖昧なような気がします。対してJavaのfinal
やC#のreadonly
は3.の意味で使われます。
final
やreadonly
、val
といった名前の方が良かったのでは無いかと私は少し思っています。
結局何が言いたいか?
JavaScriptをC風言語の一つとして扱いたければ、let
やconst
は勘違いを防ぐという意味で大変優良です。これからは積極的に使っていくべきです。ただ、一旦JavaScriptを離れ、C風言語という柵から解放されたとき、変数に三つのの種類があると言うことはどうなのだろうと思ってしまいます。過去の互換性のためにvar
をなくすことはできなかったと言っても、JavaScriptの難しさが一段上がっただけのような気もしないでは無いです。