letへの異常な愛情、(ry

2日目にチョンボをして申し訳ありませんでした。
7日目は1日目にconstさんがconstについて書いてくれたので僕はletで!

ただし、ES.nextのletではなく、現状のMozilla JavaScript (mozjs と略すよ) の let について(部分部分でES.nextの話も含めるけど)。使い方、注意点等をば。

Mozilla JavaScript の let

ES.next にも LetDeclarationドラフトに入ってきてるけど、mozjs はもっと凄いぜ! って話。微妙な部分もあるけどね。

さて、let と言っても mozjs には幾つか種類がある。

  1. let 文 (let statement)
  2. let 式 (let expression)
  3. let 定義 (let definition)

いずれもブロックスコープとなる変数を定義できるのは変わらない。

let文

これは文(statement)として let を使用する方法。

var a = 5, b = 0;
let (a = a + 10, b = 12) {
  print(a + b); // 27
}
print(a + b); // 5

という感じに書く。

let ( VariableDeclarator ) BlockStatementと、VariableDeclaratorに宣言した変数は、BlockStatement中でブロックスコープとして作用する。

let式

こちらは式(expression)としてletを使用する方法。

var a = 5, b = 0;
print(let (a = a + 10, b = 12) a + b); // 27
print(a + b); // 5

let ( VariableDeclarator ) Expressionと、VariableDeclaratorに宣言した変数は、Expression中でブロックスコープとして作用する。ブロックとなる{ }は無いけど暗黙的に追加される。というか、Expression{ }を書くとObjectLiteralとして扱われるのでBlockStatementは入れられない。

瞬間的に値を変えたいときに使用するのと思われるが、正直、外のスコープと同じ変数を宣言するのは混乱の元ではないかと思うので、そういう使い方はしたことがない。

let 式の個人的な使い方は後述。

let定義

たぶん、一番良く使う。varの変数宣言と使い方は一緒。
違いは、関数スコープではなく、ブロックスコープであること。現在のブロックに対してスコープを形成する。

また、次期ECMAScriptの草案とほぼ同等である。

{
  let a = 10, b = 0;
  print(a + b); // 10
}
print(a); // undefined てか ReferenceError

もちろん、if 文や while 文、for 文などのいろいろなブロック中で使用可能。

ブロックスコープ万歳!

ところで、

function foo () {
  let a = 10;
  let (a = a + 5) {
    print(a);
  }
  return a;
}
print(foo.toSource());

の結果はどうなるでしょうか?

答えは

function foo () {
  var a = 10;
  let (a = a + 5) {
    print(a);
  }
  return a;
}

でしたー。関数ブロックと同一ブロックだと勝手にvarに変換される模様。

for文での注意点

また、for 文にも let を使える。

for (let i = 0; i < 10; ++i) {
  print(i);
}
print(i); // undefined てか ReferenceError

for 文にも使えるということは、

var nodeList = document.getElementsByTagName("a");
for (var i = 0, len = nodeList.length; i < len; ++i) {
  (function (n) {
    nodeList[n].addEventListener("click", function(){
      alert(nodeList[n].textContent);
    }, false);
  })(i);
}

今までは上記の様に書いて、関数の引数にi入れて関数スコープに変数を束縛させていたものを以下の様に書ける!

var nodeList = document.getElementsByTagName("a");
for (let i = 0, len = nodeList.length; i < len; ++i) {
  nodeList[i].addEventListener("click", function(){
    alert(nodeList[i].textContent);
  }, false);
}

と言いたいところだが、現状のmozjsではできない。残念orz
詳細は、Bug 449811 ? Create a fresh ForInBinding for each iteration of ForStatement loopを参照。

var nodeList = document.getElementsByTagName("a");
for (let i = 0, len = nodeList.length; i < len; ++i) {
  let n = i;
  nodeList[n].addEventListener("click", function(){
    alert(nodeList[n].textContent);
  }, false);
}

上記のように、let定義をしてブロックスコープに束縛する必要がある。

念のため注意書きするが、この書き方はmozjsでの書き方であって、正式なECMAScript6thでは解消されると思う。まだドラフトに挙がってないので予想というか願望だけど。

即時関数的に使う

(function(win) {
  // ...
})(this);

一時的にスコープを限定するために即時関数と呼ばれる上記のような手法が広く使われている。今までのJavaScriptでは、ブロックスコープが存在せず、関数スコープであったため、言わば苦し紛れに編み出された手法だ。バッドノウハウと言っても良い。

しかし、letが使えると、この必要がなくなる。

let (win = this) {
  // let 文
  // ...
}

または

{
  // ブロック中にlet宣言
  let a = 10;
  // ...
}

どう? JavaScriptに見慣れると違和感があるけど、この方がスッキリしているでしょ? そしてこれが本来の姿でしょ?

ただし、現状のmozjsでは即時関数の方が良い。
何故なら、関数宣言の問題がある。mozjsがBlockStatement中の関数宣言的なもの*1を許しているのが、ある意味そもそもの問題とも言えるが、まあそれは脇に置いておこう。

関数宣言的なものでの関数は関数スコープまたはグローバルスコープに置かれる。

{
  function foo (msg) {
    print("foo: " + msg);
  }
  foo("inner block"); // "foo: inner block"
}

foo("outer block"); // "foo: outer block"

上記、コードでは、ブロック外のfoo("outer block")は実行できてしまう。これは意図した挙動ではないだろう。

やるのであれば、let 宣言で書かないといけない。

{
  let foo = function (msg) {
    print("foo: " + msg);
  }
  foo("inner block"); // "foo: inner block"
}

foo("outer block"); // ReferenceError
一方、ES.nextでは

ところで、

mozjsがBlockStatement中の関数宣言的なものを許しているのが、ある意味そもそもの問題とも言えるが、まあそれは脇に置いておこう。

と書いた。これは、ECMAScript 5thまでの話である。
ES.nextドラフト版では文法BlockStatement中にDeclarationという区切りが入り、その中にはFunctionDeclarationがある。つまり、BlockStatement中にFunctionDeclarationを置くことができる。

ES.nextでは新たにDeclarationという区切りが入り, LetDeclaration, ConstDeclaration, FunctionDeclarationが導入されました.

Declarationは全てblock scopedな変数の宣言です. VariableStatementと違い, 有効範囲がblockの範囲(もしくはFunctionBody)に収まります.

http://d.hatena.ne.jp/Constellation/20111201/1322678350

とあるように、FunctionDeclarationブロックスコープに収まることになるのだ。速く来い! ES.next!!

let 文/式/宣言の返り値

ご存知、演算子や文にも返り値というものがある。typeofは文字列を返すし、voidUndefinedを返す。Statementに属するものは通常返り値を得ることはできないが、evalすることで得ることができる。

let 宣言の返り値

これはvarと同じく、Undefinedである。

let 式の返り値

Expressionを評価した値が返る。

let 文の返り値

let 宣言、let 式までは想像しやすい。問題はStatementであるlet 文である。

let 文もある値を返す。返す値は、なんとBlockStatement中の最後のStatementを評価した値である。

var str = "let (a = 10) {" +
          "  a += 10;" +
          "  a + 30;" +
          "}";
print(eval(str)); // 50

ちょっとVimperator*2の話をしよう。Vimperatorにはechoコマンドがある。echoコマンドは引数をJavaScript文字列とみなしてevalした結果を出力するコマンドだ。コマンドラインは1行なので、コードは出来るだけ短くしたい、かつ、下手に変数宣言をすると実際に変数が作成されてしまい誤動作に繋がりかねない。

:echo (function(){ var a = []; ...; return a; })()

通常なら上記の様に書くのだが、let文が最後に評価した値を返す特性を生かして、以下の様に書ける。

:echo let (a = []) { ...; a; }

functionreturnを書かずに済んで短く書ける。ゴルフに便利。

はい、普通は絶対に使わないであろう、変なtipsでしたー。

ちょい黒魔術ちっくなlet式

あまり使いそうにないlet式だが、関数式(FunctionExpression)と組み合わせるとちょっと面白いことができる。

例えば、ユーザの行動にあわせて、ステータス文字を変更し、何秒か立ったら消すUIを考えてみる

var status = {
  statusElement: document.getElementById("status-label"),
  timeoutID: null,
  set: function setStatus (msg) {
    // 前回分のsetTimeoutが有効な場合は、クリアする
    if (this.timeoutID) {
      clearTimeout(this.timeoutID);
    }

    this.statusElement.innerHTML = msg;

    // 10秒経ったらステータス文字とtimeoutIDをクリア
    this.timeoutID = setTimeout(function(self){
      self.statusElement.innerHTML = "";
      self.timeoutID = null;
    }, 10 * 1000, this);
  }
}

ステータス文字がまだ表示中、さらにステータスを変更する場合、前回分のsetTimeoutは有効なままなのでステータス文字を更新しても10秒立たずに消されてしまうことが考えられるのでsetTimeoutのid値を保管しておく必要がある。

しかし、timeoutIDは他から使うことは無いし、できればpublicにしておきたくない。と考えるのが人情だろう(え?そんなこと考えない?そいつぁごめんなさい。僕は考えるの!)

もちろん、即時関数を使って

var status = (function(){
  var statusElement = document.getElementById("status-label");
  var timeoutID = null;
  return {
    set: function setStatus (msg) {
      // 前回分のsetTimeoutが有効な場合は、クリアする
      if (timeoutID) {
        clearTimeout(timeoutID);
      }

      statusElement.innerHTML = msg;

      // 10秒経ったらステータス文字とtimeoutIDをクリア
      timeoutID = setTimeout(function(){
        statusElement.innerHTML = "";
        timeoutID = null;
      }, 10 * 1000);
    }
  };
})();

と書き換えることは可能だが、全体の構造を変更する必要があり、ちょいと面倒である。

そこで、let式を使用する。

var status = {
  statusElement: document.getElementById("status-label"),
  set: let (timeoutID = null) function setStatus (msg) {
    // 前回分のsetTimeoutが有効な場合は、クリアする
    if (timeoutID) {
      clearTimeout(timeoutID);
    }

    this.statusElement.innerHTML = msg;

    // 10秒経ったらステータス文字とtimeoutIDをクリア
    timeoutID = setTimeout(function(self){
      self.statusElement.innerHTML = "";
      timeoutID = null;
    }, 10 * 1000, this);
  }
}

こうすることで、setStatus関数内のみで使用できるtimeoutIDが使える。すばらしい!

以上、Mozilla JavaScript 全快なエントリでした。

*1:実際にはFunctionDeclarationとは違う挙動である

*2:Firefox拡張機能vimの様なコマンドラインを持つ