Kokudoriing

技術系与太話ブログ

スコープチェーンとは何か。そもそもスコープとは何か

2012/09/28 追記
すいません、この記事かなり間違った情報を載せてしまってます。
詳しくはこちらを参照ください。


JavaScript には大きく4つの鬼門が存在します。
頑張りすぎる暗黙の型変換。
馴染みのない prototype チェーン。
やたら参照が変わる this。
何故か存在しないブロックスコープ。
今回は主に4番目の スコープ に関係するお話。

はい、JavaScript にはブロックスコープがありません。糞ですね。
しかし当たり前ですが JavaScript にもスコープは存在しています。
どういう機構で JavaScript はスコープを実現しているのでしょうか。
これを知るには JavaScript と関数とコンテキストの関係性を知らねばなりません。

関数は JavaScript において非常に重要な意味を持ちます。
関数が実行されるとき、JavaScript エンジンはその関数に紐付いた特別なオブジェクトを暗黙的に生成します。
これは関数を呼び出す(Call)と生成されることから Callオブジェクト*1 と呼ばれています。

つまり以下の様な感じ。

var func1 = function() {
  // func1 関数の Callオブジェクトが生成された
  var func2 = function() {
   // func2 関数の Callオブジェクトが生成された
  };
  func2();
};
func1();

さて、ここで func1 を実行している場所で func2 を実行すると、
「ReferenceError: func2 is not defined」とエラーが発生します。
これで func2 は違うスコープに存在することがわかりました。
より適切に言うならば、func2 は違うスコープに存在するのではなく、
func1 に紐付いた Callオブジェクト のプロパティとして生成されたのです。

プロパティであるならば、(func1のCallオブジェクト).func2() という呼び出しをしなければ、
名前解決が出来ず ReferenceError が発生してしまうのもうなずけます。

しかし、JavaScript からは原則としていかなる Callオブジェクト へも参照できないようになっています。
つまり、(func1のCallオブジェクト).func2() といった呼び出し方は出来ないのです。
そして、Callオブジェクト 内では変数の名前解決として、
Callオブジェクト のプロパティであるかどうかが探索されます。
つまり、Callオブジェクト のプロパティはその Callオブジェクト 内からしか呼び出せない訳です。
JavaScript ではこの仕組みを利用し、スコープを実現しています。
(これは言語レベルの話であり、実装がどうなっているかは関係しません。)

関数は呼ばれるとその関数の Callオブジェクト を暗黙的に生成します。
そして、実行コンテキストを生成し、
そのコンテキスト内でローカル変数(Callオブジェクト のプロパティ)が初期化されます。

しかし、ここで疑問が浮かびます。
func1 を呼び出している場所はトップレベルの場所であり、どの関数とも関係しません。
トップレベルは Callオブジェクト と無関係なのでしょうか?
そうではなく、トップレベルの Callオブジェクト の事をグローバルオブジェクト*2と呼びます。
グローバルオブジェクトは JavaScript エンジンが最初に暗黙的に生成します。

ですので、イメージとしては以下のようなものになります。

// グローバルオブジェクト(と実行コンテキスト)が生成された
var func1 = function() {
  // func1 関数の Callオブジェクト(と実行コンテキスト)が生成された
  var func2 = function() {
   // func2 関数の Callオブジェクト(と実行コンテキスト)が生成された
  };
  func2();
};
func1();

重要なこととして、その関数を呼び出さなければ Callオブジェクト も実行コンテキストも生成されません。
また、同じ関数でも呼び出すたびに新しく Callオブジェクト と実行コンテキストは生成されます。

そして、関数内でのみ使える不思議な arguments 変数ですが、
これもお察しの通り Callオブジェクト のプロパティです。(キーワードでない)
また、関数の仮引数なんかも Callオブジェクト プロパティです。

最後に、グローバルオブジェクトは window でアクセス可能です。
ですが、グローバルオブジェクトは Callオブジェクト なので、
JavaScript 側からアクセスできるのはおかしい気がします。
グローバルオブジェクトは JavaScript 側からアクセスできる唯一の Callオブジェクトです。
トップレベルでいちいち var window = this; とやるのは面倒だからあるだけです。

ここで今までのまとめ。

// グローバルオブジェクト(と実行コンテキスト)が生成された
// トップレベルでの this はグローバルオブジェクトを参照する
// クライアントサイドJavaScript の場合 this == window
var func1 = function() {
  // func1 関数の Callオブジェクト(と実行コンテキスト)が生成された
  // func1 の Callオブジェクトにアクセスする方法はない
  // arguments は func1 の Callオブジェクト のプロパティ
  var func2 = function(arg) {
    // func2 関数の Callオブジェクト(と実行コンテキスト)が生成された
    // func2 の Callオブジェクトにアクセスする方法はない
    // 仮引数 arg は func2 の Callオブジェクト のプロパティ
    // arguments は func2 の Callオブジェクト のプロパティ
  };
  // この func2 は func1 の Callオブジェクト のプロパティ
  func2();
};
func1();


さて本題。
先程も言った通りプロパティアクセスかつ Callオブジェクト への参照がないとなる手前、
Callオブジェクト のプロパティは同一の Callオブジェクト からのみアクセス可能です。
しかし、この場合はどうでしょう。

var name = 'Kokudori'
var func1 = function() {
  console.log(name) // 'Kokudori'
};
func1();

ここで name はグローバルオブジェクトのプロパティです。
つまり、name は func1 の Callオブジェクト でないにも関わらず、アクセスが可能です。

関数が実行されると Callオブジェクト と共に実行コンテキストも生成されることは既に述べました。
実行コンテキストはグローバルオブジェクトから呼び出された関数順に、
その関数の Callオブジェクト を入れたスタックを保持します。
これはコールスタックそのものです。

そして、実行コンテキストでは、
Callオブジェクト のスタックをポップしていきながら変数の名前解決を行います。
これはスタックから順にスコープを解決していく事と同義なので、
Callオブジェクト のスタックの事をスコープチェーンと言います。

これは様々な言語のスコープと同じ挙動をするので非常に自然です。

var hobby = 'Reading books';
var func1 = function() {
  var name = 'Kokudori'
  var func2 = function() {
    var age = 20;
    // 変数は func2 の Callオブジェクト -> func1 の Callオブジェクト -> グローバルオブジェクト と解決されていく

    console.log(name); // 'Kokudori'
    // name は func1 の Callオブジェクト のプロパティであり、スコープチェーン内に存在するので解決される
    console.log(age); // 20
    // age は func2 の Callオブジェクト のプロパティであり、スコープチェーン内に存在するので解決される
    console.log(hobby); // 'Reading books'
    // hobby はグローバルオブジェクトのプロパティであり、スコープチェーン内に存在するので解決される
  };
  func2();
};
func1();


なのでこんなことも出来る。

var func1 = function(num) {
  if (num > 10)
    return num;
  return func1(num * 2); // func1 はグローバルオブジェクトに存在するので、(例え自身だろうと)アクセス可能
};
func1(2); // 16

そして冒頭でも触れましたが、JavaScript にはブロックスコープがありません。
単にブラケット {, } で囲うとオブジェクトリテラルと勘違いされます。
JavaScript でブロックスコープもどきを使いたい場合はこのようにしましょう。

(function() {
  // ここは無名関数の実行コンテキストとなる
  var hoge = 100;
  // hoge はグローバルオブジェクトでなく無名関数の Callオブジェクト のプロパティとして宣言される
  // よってグローバル汚染を防ぐことができる
})();


しかしブロックスコープが無いのは辛いですね。
不満の声がかなり出ているらしく、ES.next(次のバージョンのECMAScript)では
let式/let文 としてブロックスコープが導入されるとかされないとか。


今回、「パーフェクトJavaScript」の p114〜,174〜 と、
JavaScript第5版(サイ本)」の P56〜,141〜 を参考にさせて頂きました。

パーフェクトJavaScript (PERFECT SERIES 4)

パーフェクトJavaScript (PERFECT SERIES 4)


JavaScript 第5版

JavaScript 第5版

*1:ECMAScript では activateオブジェクトと呼ばれています。が、Callオブジェクトの方が広く使用されている言葉なので、ここではCallオブジェクトと呼びます。

*2:グローバルオブジェクト自体はECMAScriptで標準化されています。しかし、グローバルオブジェクトの中身については標準化されていません。例えばクライアントサイドJavaScript(ブラウザ内)でのグローバルオブジェクトはwindowですが、サーバーサイドJavaScript(node.js)等ではプロセスです。