現代的JavaScriptおれおれアドベントカレンダー2017 – 02日目

かつて変数宣言といえば var でしたが、今なら他に letconst も使えます。

基本的な使い方は一緒。

let foo = 123;
const bar = 'THIS_IS_A_PEN';

要約

  • できるだけ const を使おう
  • const が適さない場面では let を使おう
  • var を積極的に使うべき場面はない
  • letconst はブロック {} がスコープになる
  • 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);
  }
}

maxtwice も、ついでに 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 の括弧はブロックの内側になるので、 itwice が同じスコープになります。(次項参照)

まあこの例では特に困らないですけど。

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);
}

ただ varlet に置き換えただけですが、これでもう大丈夫。 let で宣言した変数 ifor の中に閉じ込められるだけでなく、ループごとに固定されます。(たぶん仕様書の 13.7.4.7 Runtime Semantics: LabelledEvaluationloopEnv がその話だと思うんだけど、合ってる?)

ちなみにこれは 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;

まあ代入は元の位置なので bundefined なんですけど。

一方 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 に置き換えよう

ここまで述べてきたように varlet は全く同じというわけではないので、単純に置き換えると問題が起こる可能性があります。

しかしその問題は本来対処されるべきものが見逃されてきただけです。これを機に適切な記述へ修正しましょう。

ブロック

forif といった {} がブロックです。関数ももちろんブロックです。

逆に 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 派へ改宗しました。

定数とはちょっと違う

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コードでアプリ実装する我々エンドユーザ(?)的には「巻き上げはない」という認識で良いです。よね?

初期化と代入

本記事では同一視することにしましたのでよろしくお願いします。

参考

おしまい

const 使おう! な!