kidoOooOoooOOom

IT系で開発やってます

Effective JavaSceipt 読書メモ 項目12~17

第2章 変数のスコープ

項目12 変数の巻き上げ(ホイスティング)を理解する

JSではブロックスコープをサポートしない。変数定義は、それを最も近く囲んでいるステートメントやブロックではなく、それを含む関数をスコープとする。
JSの変数宣言の振る舞いは、宣言と代入の2つの部分で構成されていると考えれば理解しやすい。宣言の部分を暗黙のうちに、それを囲む関数の冒頭に「巻き上げる」が、代入の部分は、その場所に残す。
下記のようなコードを書いた場合、

function f() {
  // ...
  {
    var x = ...
  }
}

巻き上げにより、振る舞いとしては下記のコードのようになる。

function f() {
  var x;
  // ...
  {
    x = ...
  }
}

この紛らわしさを防ぐために、var宣言を関数の先頭で宣言することによる手作業巻き上げを行うのも有り。自分はその方が分かり易くて良いと思う。
JSで、例外的にブロックをスコープとするとのはexceptionである。try...catchで補足される例外は、そのcatchブロックだけをスコープとする変数になる。

function test() {
  var x = "aaa";
  try {
    // ...
  } catch (x) {
    x = "error"; // ここのx は 先頭で宣言されたxとは別スコープ
  }
}

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

次に示すコードは、プログラマーの意図に反してundefinedになるであろう。

function hogehoge(a) {
  var result =[];
  for (var i = 0, n = a.length; i < n; i++) {
    result[i] = function() { return a[i]; };
  }
  return result;
}
var foo = hogehoge([10,20,30]);
var f = foo[0];
console.log(f()); // undefined

変数iがクロージャによって最後の数値しか参照しないため、iがa.lengthを超えた値になった時のものを参照してしまっている。
これを解決する方法としては、即時関数式(immediately invoked function expression: IIFE) を用いる方法がある。

function hogehoge(a) {
  var result =[];
  for (var i = 0, n = a.length; i < n; i++) {
    (function(j) {
      result[i] = function() { return a[j]; };
    })(i);
  }
  return result;
}
var foo = hogehoge([10,20,30]);
var f = foo[0];
console.log(f()); // 10

これにより、変数jが即時関数式内でforループ毎の変数iを保持するため、期待した結果になる。

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

この項目は、正しく実装されたES5環境を前提にするかしないかで注意すべき観点が異なる。最近はES5環境前提になってきているので、リーダーブルコード優先する方針でメモる。

名前付き関数式は下記のように書ける。

var x = function double(x) { return x * 2; }

一方、名前無しの関数式は下記のように書ける。

var x = function(x) { return x * 2; }

この2つの関数式の違いとしては、デバッグ環境で現れる。
Errorオブジェクトのスタックトレースにこの関数の名前が現れてくるため、情報が欲しい場合はできるだけ名前をつけた方がデバッグもしやすい。また、ソースコードでも名前が付いていた方が理解を助けると思うため、ES5環境前提においては名前付き関数を使った方がよさそうである。

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

ES5になるまで、下記のようなブロックローカルな関数宣言を書いた場合の挙動について正式な見解が無かった。

function test(x) {
  if (x) {
    function f() { return "local" };
  }
  return null;
}

ES5からは、上記なようなコードをstrictモードでエラーとして報告するようになった。
今のところ、ネストした関数宣言を書きたい場合はvar宣言と関数式を使って行うことならば可能である。

function test2(x) {
  if (x) {
    var f = function f() { return "local" };
  }
  return f;
}

console.log(test2(true)()); // local

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

evalは非常に強力で柔軟性の高いツールであるがゆえ、その利用には注意しなければならない。
evalで最も単純な間違いのひとつが、スコープとの干渉である。evalで変数を作ることにより、呼び出し側のスコープが汚染されてしまうケースが起こりうる。

// use strict;
var evaltest = "i am evil";
function evalTest(x) {
  if (x) {
    eval("var evaltest = 'i am justice';");
  }
  return evaltest;
}
console.log(evalTest(true)); // i am justice 
console.log(evalTest(false)); // i am evil

このコードでは、evaltest変数がevalによって動的に汚染されてしまう。
ただし、ES5のstrictモードを付くと、evalはネストしたスコープ内で実行されるため変数が汚染されない。

use strict;

var evaltest = "i am evil";
function evalTest(x) {
  if (x) {
    eval("var evaltest = 'i am justice';");
  }
  return evaltest;
}
console.log(evalTest(true)); // i am evil
console.log(evalTest(false)); // i am evil

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

直接evalと間接evalの2通りの使い方が用意されている。
直接evalは、evalを呼び出した場所の完全なスコープをアクセスすることができる。この威力があまりにも強大であるため、直接evalを含むコードはパフォーマンス上に悪影響がある。
これに対し、間接evalは下記のような書き方で記述する方法である。

(0, eval)(src);

間接evalの場合、引数はグローバルスコープとして評価される。これにより、コンパイラの最適化が行いやすくなるためパフォーマンス上の影響が直接evalに比べて小さい。また、スコープも限定されるため安全性も高まる。
というわけで可能な限り直接evalではなく間接evalを使うべきである。