The King's Museum

ソフトウェアエンジニアのブログ。

『Effective JavaScript』を読んで(項目13〜17)

『Effective JavaScript』を読んだシリーズ。

項目13:ローカルスコープを作るには即時関数式(IIFE)をfu使おう

JavaScript ではブロックスコープがないので、新たなスコープを作成するために即時関数式(Immediately Invoked Function Expression)と呼ばれる手法を利用する。

function wrapElements(a) {
  var result = [], i, n;
  for (i = 0, n = a.length; i < n; i++) {
      result[i] = function() { return a[i]; }
  }
  return result;
}
var wrapped = wrapElements([10, 20, 30]);
wrapped[0](); // => undefined
wrapped[1](); // => undefined
wrapped[2](); // => undefined

上記のプログラムにはバグがある。ポイントは、クロージャは外側の変数を、値ではなくリファレンスによって保存するというところ。function () { return a[i] } というクロージャは外側の変数 i を参照しているので、for による i の変更の影響を受けてしまって、wrapped を呼び出す時には、i は n まで進んでしまっている。

そこで、以下の様にして即時関数式を作成することでこの問題を解決する。 新たな関数を定義することで新たなスコープが作られ、その中で i の値を j として保持する。 その関数をすぐに実行することで、通常のコードの実行と同じ効果が得られる。 もう一つの方法として、引数として値を渡す方法もある。

function wrapElements(a) {
  var result = [], i, n;
  for (i = 0, n = a.length; i < n; i++) {
      function () {  // 即時関数式
        var j = i; // 一旦保存
        result[i] = function() { return a[j]; }
      }();
  }
  return result;
}
var wrapped = wrapElements([10, 20, 30]);
wrapped[0](); // => 10
wrapped[1](); // => 20
wrapped[2](); // => 30

function wrapElements2(a) {
  var result = [], i, n;
  for (i = 0, n = a.length; i < n; i++) {
      function (j) {  // 即時関数式
        result[i] = function() { return a[j]; }
      }(i); // i の値を渡しておく
  }
  return result;
}

項目14:名前付き関数式のスコープは可搬性がないので注意しよう

JavaScript の関数は同じように見えるがコンテキストに依存していくつか種類がある。

function f() { return true; } // 関数宣言
var x = function() { return true; } // 無名関数式
var x = function f() { return true; } // 名前付き関数式

名前付き関数式では、関数名がその関数のローカル変数に束縛されるので、その関数内で再帰的に関数を呼び出すことが出来る。

古い仕様(ES3)では、これを実現するためにスコープ内から Object.prototype を参照できるように要求しており、スコープ内が Object.prototype で汚染されてしまう。最新の仕様ではこれは削除されたが、古い仕様の実装のままの処理系も多くある。 また、無名関数式であるのに Object.prototype を参照できるようにしている処理系や、外側のスコープから名前付き関数式の関数名を参照できる 処理系もあるので注意する。

上記のような理由で無理して使わないことが推奨されている。

項目15:ブロックローカルな関数宣言のスコープも可搬性がないので注意しよう

JavaScript では関数宣言のブロックスコープ性は仕様書で明言されていないので可搬性がない。 関数の先頭で関数宣言を行うことは認められているが、ブロックスコープ内で関数宣言を行った場合の挙動は処理系に依存している。

function f(flag) {   
  if (flag) { 
     function local() { return true; }
  }  
  local; // => undefined? function object?
}

上記のコードは処理系に依存して、以下の二種類の挙動に別れるらしい。

  • 関数 local が「巻き上げられ」て、関数内では local が利用できる
  • ブロックスコープとして解釈し、ブロック外では undefined となる(仕様に明記されていない挙動)

一応、最新の仕様書では警告、またはエラーにすることが推奨されている。 また、strict モードではこのコードはエラーとなる。

項目16:eval でローカル変数を作らない

eval を利用して var で変数宣言を行うと、そのスコープのローカル変数として宣言される。 一般的にプログラムの動的な振る舞いに基づいてスコープが変更されるのはよくない。 特に、eval に与えるコードを外から与えると、内部のスコープを変更する権利を外部に与えることになり、プログラムの構造を著しく破壊する。

var y = "global";
function test(code) {
  eval(code);
  return y;
}
test("var y = 'local';"); // => "local"
test("var z = 'local';"); // => "global"
}

eval を利用する場合は即時実行関数式を利用してスコープの汚染を防ぐ。

function test(code) {
  function () { eval(code); }();
  return y;
}

項目17:直接 eval より、間接 eval が好ましい

eval はそれを呼び出した場所の完全なスコープにアクセスできる。 この機能はあまりにも柔軟性が高く強力なため JavaScript の最適化が難しくなる。(インライン化が難しくなるのかな?)

そこで、JavaScript は eval を「直接 eval」と「間接 eval」に分けた。 「直接 eval」は前述したように完全なスコープにアクセスできるが、「間接 eval」は呼び出した場所の完全なスコープにアクセスにアクセスできないようになっている。 「直接 eval」は eval をそのまま呼び出せばよく、「間接 eval」は eval 関数を一度他の変数に代入して呼び出せばよい。

eval("true"); // 直接 eval
var f = eval;
f("true") // 間接 eval

ちなみに間接 eval を呼び出すための共通イディオムがあり、それは以下の通り。

(0, eval)(src);

そもそも間接 eval という言葉を始めて知ったな。 こういうのは言語の暗部という感じがしてそれはそれでおもしろい。

あと、可搬性って何のことだ?と思ったが、Portability の訳語みたいだね。

なんとか第2章を今年中にまとめることができてよかった。

(c) The King's Museum