読者です 読者をやめる 読者になる 読者になる

『Effective JavaScript』を読んで(項目8〜12)

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

第2章はスコープとかクロージャとかの話。クロージャを持たない言語で育った自分にとって、この概念を理解するのは大変だった記憶がある。

このあたりは SICP で勉強したのだけど、クロージャを使って状態を持つオブジェクト作成するあたりは非常に感心した。 大学時代に先生が「クロージャ関数型言語オブジェクト指向的な機能を実装するために利用できる」と言っていて、当時は理解できなかったのだが今なら理解できる。こういうこともあってか、理解した時はプログラマとして自分がひとつ賢くなった気がした。

第2章もすべて書いてから投稿しようと思ったが疲れたので一度投稿して満足感を得ることにする。

第2章:変数のスコープ

項目8:グローバルオブジェクトを使うのは、最小限にとどめる

グローバル名前空間に変数を作成するのは最小限にとどめる(コンポーネントを他の部分から利用できるようにするためには避けられない場合もあるが)。

JavaScript では変数の再宣言ができるのでグローバル名前空間に変数があることに気づかずに、ローカル変数として利用しようとしてしまうケースがある。

var i = 1; // グローバル変数として宣言し初期化
function f() {
    var i = 0; // グローバル変数の i に代入しているだけ。ローカル変数ではない。
    return i;
}

JavaScript のグローバル名前空間はグローバルオブジェクトとして公開される。グローバルオブジェクトはグローバルスコープでは this として参照できる。Web ブラウザでは window オブジェクトにもバインドされている。

グローバル変数を作成する方法は以下の方法がある。

  • グローバルスコープで var 宣言
  • グローバルオブジェクトに追加
this.foo; // => undefined
var foo = "global foo"; // var 宣言
this.foo; // => "global foo" グローバルオブジェクトに追加されている
this.foo = "changed"
foo; // => changed

グローバルオブジェクトの利用は避けるべきだが、プラットフォームで実行できる機能を問い合わせるために利用することが多々ある。

項目9:ローカル変数は、かならず宣言しよう

グローバル変数は避けるべきだが、偶発的にグローバル変数を宣言してしまう場合がある。 var を使わないで変数を宣言すると暗黙的にグローバルオブジェクトに追加されてしまう。

function f() {
  local = "local"; // グローバルオブジェクトに追加されている
}
f(); // local を宣言して代入している
local; // => "local"

項目10:with を避けよう

JavaScript の with を避ける(そもそも with って始めて知った)。

with ブロックでは変数のルックアップが with オブジェクト(with ブロックに与えられるオブジェクト)から優先的に行われる。 そのため、「内側のスコープから優先的に解決」されるという JavaScript の原則から外れるし、with オブジェクトの prototype オブジェクトまでルックアップするので遅くなる。

Common Lisp に with-slots というマクロがあるようだがそれに影響を受けた機能なのだろうか?

項目11:クロージャと仲良くしよう

クロージャの話。この本に書かれているクロージャのポイントは以下の通り。

第1の事実:JavaScript では現在の関数の外側で定義された変数を参照できる。

第2の事実:関数は、その外側の関すがリターンした後になっても、まだ外側の関数内で定義された変数を参照できる

第3の事実:クロージャは外側の変数を更新できる。クロージャは、外側の変数へのリファレンスを保存するのであって値をコピーするわけではない。

特に第3の事実は大事だ。最近、「第3の事実」を忘れてしまって、実装中に混乱することがあった。 SICP で学習しているときは、変数の値が Immulable なので「値を保持している」と勘違いしてしまったようだ。その後の「代入」の章では Mutable Object もきちんと扱っていたのになぁ…。

JavaScript では無名関数を作成するために関数式(funciton expression)を利用する。

funciton makeFunction (x) {
    return function (y) { return x + y; } // 関数式
}
var a = makeFunction(1);
a(2); // => 3

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

JavaScript は構文スコープ(lexical scoping)をサポートする。しかし、一部の例外を除いてブロックスコープ(block scoping)をサポートしない。変数定義は、それを最も近く囲んでいるステートメントやブロックではなく、それを囲んでいる関数をスコープとする。

function (a, b) {
  if (a) {
     var x = 1;
     if (b) {
         var y = 2;
     }
  }
  x; // => 1 評価できる
  y; // => 2 評価できる
}

この効果は「変数の巻き上げ」と呼ばれる。JavaScript は宣言部分を暗黙のうちにそれを囲む関数の冒頭に「巻き上げる」のだ。

// 巻き上げ前
function f(a, b) {
  if (a) {
     var x = 1;
     if (b) {
         var y = 2;
     }
  }
}

// 巻き上げ後
function f(a, b) {
  var x; // 巻き上げ
  var y; // 巻き上げ
  if (a) {
     x = 1;
     if (b) {
         y = 2;
     }
  }
}

唯一ブロックスコープになるのは例外(exception)の場合。try catch で補足される例外はその catch ブロックだけをスコープとする変数に束縛される。

手動巻き上げ(変数は必ず関数の最初で宣言する)の手法を使ってもよい。何はともあれ、この挙動を覚えて置かないと確実にプログラムがバグる