現代的JavaScriptおれおれアドベントカレンダー2017 – 02日目
かつて変数宣言といえば var
でしたが、今なら他に let
とconst
も使えます。
基本的な使い方は一緒。
let foo = 123; const bar = 'THIS_IS_A_PEN';
要約
- できるだけ
const
を使おう const
が適さない場面ではlet
を使おうvar
を積極的に使うべき場面はないlet
とconst
はブロック{
~}
がスコープになるvar
は関数function(){
~}
がスコープconst
の参照先は変えられないが中身(オブジェクトのプロパティ等)は変えられる
それでは以下、こんなに長くなるはずじゃなかった記事です。
var
vs let
変数のスコープ(有効範囲)が異なります。
var
は関数単位のスコープになりますが、 let
ならブロック単位のスコープになります。
例えばこんなやつ。
function hey() { var max = 3; for (var i = 0; i < max; i++) { var twice = i * 2; console.log(i, twice, max); } }
max
も twice
も、ついでに i
も全て同じ関数 hey()
全体がスコープになります。これを let
に置き換えると、ブロック {
~ }
だけが範囲になります。
function hey() { let max = 3; for (let i = 0; i < max; i++) { let twice = i * 2; console.log(i, twice, max); } }
あ、 for
の括弧はブロックの内側になるので、 i
と twice
が同じスコープになります。(次項参照)
まあこの例では特に困らないですけど。
for (var i = 0; i < max; i++)
の問題
さて以下のコード、どんな動きを想定しますか。
// カウントダウン var max = 5; for (var i = 0; i < max; i++) { setTimeout(() => { console.log(max - i); }, i * 1000); }
一秒おきにカウントダウンしていってほしいんだけど、残念、実際はそうはなりません。全部 0
になっちゃいます。
なぜかというと、 setTimeout()
はすぐに実行されて、コールバック実行の待機を開始し、ループは終了します。その後 setTimeout()
のコールバックが実行される頃には i
の中身がもう 5
になっちゃってるからです。 addEventLister()
ないし $el.on('click', ...)
なんかでも同じことやりがち。
まあFAQですね。
一方 let
なら、この問題を解決します。
// カウントダウン let max = 5; for (let i = 0; i < max; i++) { setTimeout(() => { console.log(max - i); }, i * 1000); }
ただ var
を let
に置き換えただけですが、これでもう大丈夫。 let
で宣言した変数 i
は for
の中に閉じ込められるだけでなく、ループごとに固定されます。(たぶん仕様書の 13.7.4.7 Runtime Semantics: LabelledEvaluation の loopEnv がその話だと思うんだけど、合ってる?)
ちなみにこれは let
のおかげなので、 var
じゃなくても例えば obj.counter
を使ったら駄目です。
デモ
その他
「スコープが違う」以外の var
と違う点はこちら。
- グローバルスコープに置かれない
- 巻き上げが起こらない
- 二重に宣言できない
グローバルスコープに置かれない
グローバルスコープで var foo
とすると、グローバル変数 foo
が作られ、 window.foo
でも操作できるようになります。
let
だとそうはなりません。
巻き上げが起こらない
var
を使ったこれ↓は、
a = 123; console.log(a, b); var a; var b = 234;
こう↓解釈されます。
var a, b; a = 123; console.log(a, b); b = 234;
まあ代入は元の位置なので b
は undefined
なんですけど。
一方 let
では巻き上げられないので、宣言以前に変数を使おうとすると undefined
どころかエラーになります。
Exception: ReferenceError: can’t access lexical declaration `a’ before initialization
(巻き上げについては別項も参照。)
二重に宣言できない
同じ名前で二回宣言するとエラーになります。
let a; // … // すごく長いコード // … let a;
Exception: SyntaxError: redeclaration of let a
……というのをわざとやる意味はない
どちらかというと「なんで今までそれで良かったんだよ」というようなヘンテコ仕様ですね、 var
は。
JavaScriptの生い立ちが生い立ちなものでこういう仕様になってしまったんじゃないかと思うんですが、意図的にそう記述する意味はないので、そう書かれていたら何かしらの誤りであるはずです。 let
にするとそれらが浮き彫りになります。エラーが見えるので、ついうっかりが減るわけです。
let
に置き換えよう
ここまで述べてきたように var
と let
は全く同じというわけではないので、単純に置き換えると問題が起こる可能性があります。
しかしその問題は本来対処されるべきものが見逃されてきただけです。これを機に適切な記述へ修正しましょう。
ブロック
for
や if
といった {
~ }
がブロックです。関数ももちろんブロックです。
逆に switch
は、全体でひとつのブロックになり、 case
ごとにはブロックは生まれません。
switch (code) { case 200: let message = 'OK'; showMessage(message); break; case 404: let message = 'Not found'; showErrorMessage(message); break; //... }
Exception: SyntaxError: redeclaration of let message
無理やりブロックを作る
何もないところに {
~ }
を書いてブロックにすることもできます。
switch (code) { case 200: { let message = 'OK'; showMessage(message); break; } case 404: { let message = 'Not found'; showErrorMessage(message); break; } //... }
あんまりやらない方が良いと思う。
Scratchpadで試しにコード書くときに全体を括るときくらいかなあ。
let
vs const
さてもういっちょ、 let
の他に const
というのもあります。違いは「再代入できるか」です。
let message = 'hello world'; message = message.toUppwerCase(); // OK
const message = 'hello world'; message = message.toUppwerCase(); // NG
代入できないので、名前だけ先に宣言するってのは駄目です。 const foo;
みたいなのは SyntaxError
になります。
あと代入できないだけなんで、値の変更ができないというわけでもありません。オブジェクトのプロパティを変えるとかね。
const obj = { name: 'Alice' }; obj.name = 'Bob'; // OK
const
つかおう
基本的にはできるだけ const
使おうぜ、というのが流行だと思います。 const
で記述されることで、一度設定された値がその後変更されないことが担保されるので。
逆に let
を見かけたら「あ、どこかで変更されるんだな、なんでかな」と考えましょう。
letじゃなくてconst使え論、分からないでもないんだけどc-o-n-s-tと入力するよりl-e-tと入力する方がなんかすげえ楽ちんなのでlet派です。
— 高梨ギンペイ (@ginpei_jp) October 17, 2016
昔は let
でいいじゃんと思ってたんだけど、今は const
派へ改宗しました。
定数とはちょっと違う
const
を聞くとどうもC言語の #define
を思い浮かべてしまって、実行前に確定する値を入れるものかなーとか名前は大文字にした方が良いかなーとか思ったりもしてました。実際はそうでもないですね。というかC言語でも const
を仮引数に付けたりしてたね。
というわけでできるだけ const
にしましょう。
その他
if let
ほしい
Swiftに if let
って構文があってこれ便利なんすよ。ESにも入らないかなー。
これ↓を、
let currentUser = getCurrentUser(); if (currentUser) { updateLatestInfo(currentUser); }
こう↓書ける。
if (let currentUser = getCurrentUser()) { updateLatestInfo(currentUser); }
初見で「ぐわーきもいー」と思ったんだけど、でも変数のスコープが小さくなるのが良い感じです。
現状では構文エラーです。
巻き上げの話
さっきは「 let
は巻き上げないよ」と言ったんですが、実は内部的に巻き上げ的なことは起こることになってます。ただそれでもやっぱり宣言個所より前で参照するとエラーになります。
The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.
変数はそれを内包するLexical Environmentが初期化された際に生成されるが、変数のLexicalBindingが評価されるまで、アクセスされてはいけない。
いずれにしろ、JSエンジンの実装じゃなくてJSコードでアプリ実装する我々エンドユーザ(?)的には「巻き上げはない」という認識で良いです。よね?
初期化と代入
本記事では同一視することにしましたのでよろしくお願いします。
参考
- ECMAScript 2015 Language Specification – ECMA-262 6th Edition
- 13.3.1 Let and Const Declarations
- let – JavaScript | MDN
- const – JavaScript | MDN
- var – JavaScript | MDN
- ES6 In Depth: let and const – Mozilla Hacks – the Web developer blog
- JavaScriptのスコープ総まとめ – スコープの種類とその基本 | CodeGrid
おしまい
const
使おう! な!