JavaScript のブロックスコープと名前空間

Web 開発や拡張機能開発で JavaScript のコードを書いていると、誰もが一度は次のようなことで悩むかと思います。

  • ブロックスコープと名前空間 (グローバル変数汚染の回避)
  • 読み書きしやすくデバッグしやすいコードスタイル
  • コールバック関数と this オブジェクトの取り扱い
  • デバッグ方法とデバッグ支援モジュール
  • 非同期処理の書き方

いずれも解決方法は人によって様々で、これが常にベストと言えるものがなさそうですが、私なりにそれぞれ検討したことなどを書いてみようかと思います。もっと良い方法があるとか色々皆さんのご意見やツッコミをいただければ幸いです。

ブロックスコープと名前空間

JavaScript では名前空間は言語仕様でサポートされておらず、ライブラリや拡張機能などのコードを書くときにはグローバル変数の使用を最小限に抑える必要があります。先日の Mozilla 勉強会@東京 3rd でも佐藤さんと守山さんの発表で話題になっていましたが、今回はまずブロックスコープ周りのいろいろなコーディングスタイルを比較検討してみようかと思います。


前提条件は次の通りとします:

  • グローバル変数には「名前空間オブジェクト」 1 つしか使ってはならない
  • プロトタイプ拡張など既存のグローバルオブジェクトは一切書き換えてはならない
  • 外からアクセスできる public な「共有変数」は名前空間オブジェクトのプロパティとする
  • 特定ブロック外部からアクセスできない private な「モジュール変数」を作りたい
  • モジュール内のコードでは共有変数やモジュール変数に prefix なしでアクセスしたい
  • コールバック関数内で this が指すオブジェクトの変化を気にしたくない
  • デバッグ時にブレークポイントを設定しやすくしたい(関数の入れ子は避けたい)
  • できれば getter/setter を用いた変数も prefix なしでアクセスしたい
  • できれば IE や古いブラウザでも動作する互換性のあるコードにしたい

変数に prefix なしでアクセスすることを重視している理由は以下の通りです。

  • ついうっかり prefix なしでコードを書いてエラーになることが多々ある
  • 汎用関数やデバッグサポート関数や定数などは特に prefix なしで書きたい
  • コールバック関数の内外で this が切り替わることを気にせずアクセスしたい

なお、名前空間オブジェクトの定義方法自体、myGlobalObject などと他と重複しない十分に長い名前を使う方法、jp.mozilla.dev のようにドメイン名に対応する名前空間オブジェクトツリーを定義する方法、window["https://dev.mozilla.jp/"] のように URI 名のグローバルプロパティを設定する方法、名前空間をハンドリングするコンストラクタを使う方法など色々あります。とはいえ、いずれも名前空間オブジェクトの定義部分だけ置き換えれば済む話ですので、ここでは単に myGlobalObject 方式で説明します。

同様に、関数名を付ける方がデバッグしやすいといった話についても、ブロックスコープの書き方に依らない話なので簡単のため割愛して単純な書式で書いています。

a. グローバルオブジェクトのプロパティを使う (基本)

var myGlobalObject = { // グローバル名前空間オブジェクト
  _private: "private", // private 変数
  _func: function() { ... }, // private 関数
   ...
  public: "public", // public 変数
  method: function() { ... }, // public メソッド
  work: function() {
    alert(this._private); // this. prefix が必要
    alert(this.public);
    this._func(); // this. prefix が必要
    this.method();
    setTimeout(function() {
      alert(myGlobalObject._private); // コールバック内では this 使えない
      alert(myGlobalObject.public);
    }, 1000);
  },
   ...
}
  • 利点: 単純
  • 欠点: 前提条件を殆ど満たしておらず、複雑なモジュールになると面倒
    • モジュール変数や共有変数 (private/public) の区別が命名規則に依存する
    • 常に prefix として this. を書く必要があり、コールバックでは特に面倒になる
    • 変数の定義が全てプロパティになるため行末の “,” 書き忘れに注意が必要

要するに、単純だけど制約も多くコードが大きくなってくると面倒な方式です。個人的には一時的または局所的なコードでのみ採用しています。

b. 無名関数の関数スコープ変数をモジュール変数として利用する (古典)

(function() { // 無名関数でスコープを形成する
  var private = "private"; // 関数内スコープで private 変数を定義
  var func = function() { ... }; // private 関数
   ...
  myGlobalObject = { // グローバル名前空間オブジェクト
    public: "public", // public 変数
    method: function() { ... }, // public メソッド
    work: function() {
      alert(private); // private 変数は prefix 不要!
      alert(this.public);
      func(); // private 関数は prefix 不要!
      this.method();
      setTimeout(function() {
        alert(private); // コールバック内でも prefix 不要!
        alert(myGlobalObject.public); // コールバック内では this 使えない
      }, 1000);
    },
     ...
  };
})(); // 無名関数を即時実行
  • 利点: 比較的一般に使われている読みやすい方法
    • グローバル変数を定義しなければ、グローバル汚染一切無しで単発的な処理を行うことも可能
    • 無名関数の関数スコープで定義された private 変数/関数は this を付ける必要がなくブロックスコープ変数のように扱える
    • private 変数は var 文で書けるため文末の “;” を忘れても構文エラーとならない
  • 欠点: 大規模開発を行う上で望ましい前提条件を満たさない
    • モジュール内の全ての関数が関数内関数となりデバッグ時にブレークポイントを設定しにくい
    • モジュール用名前空間の public 変数は this を付けてアクセスする必要がある

要するに、単純で広く知られてて使いやすいけど制約もあり、コードが大きくなってくると不便な方式です。個人的には全ての関数が関数内関数となりデバッグしにくいのが致命的だし、次の手法の方が書きやすいのでほとんど採用していません。

var 無しでグローバル変数を定義する方法は避けたい場合、window.myGlobalObject = { … } のように書くこともありますが、サンドボックスなどグローバルスコープオブジェクトが window ではない場合などには注意が必要です。

より明確に、グローバル名前空間オブジェクトの定義を分離すると同時に、モジュール内でアクセスしやすく書き直すバリエーションもあります:

var myGlobalObject = (function() { // 返り値をグローバル名前空間オブジェクトに設定
  var private = "private"; // 関数内スコープで private 変数を定義
   ...
  var global = {
    public: "public",
     ...
  }; // 名前空間オブジェクトを private オブジェクトで定義
  return global; // 名前空間オブジェクトを return する
})();

あるいは、private 変数のうち定数やパラメータ的なものを明確に分離する場合には次のように書くこともあります:

(function(private, param, id, max) {
  myGlobalObject = {
    public: "public",
     ...
    work: function() {
      alert(private);
      alert(param.a);
       ...
    }
  };
})("private", {a:1, b:2}, "ID", 100);

いずれでも本質的には変わりありません。個人的には、ブロック外で最初に書くことでグローバル変数が分かりやすいとか、グローバル名前空間オブジェクト名にかかわらず簡単な private 変数名でアクセスできるので return global するのが好みです。

c. 無名コンストラクタの関数スコープ変数をモジュール変数として利用する

new function() { // 無名関数でスコープを形成する
  // 中の書き方は先ほどと同じ
  var private = "private"; // 関数内スコープで private 変数を定義
  var func = function() { ... }; // private 関数
   ...
  myGlobalObject = { // グローバル名前空間オブジェクト
    public: "public", // public 変数
    method: function() { ... }, // public メソッド
    work: function() {
      alert(private); // private 変数は prefix 不要!
      alert(this.public);
      func(); // private 関数は prefix 不要!
      this.method();
      setTimeout(function() {
        alert(private); // コールバック内でも prefix 不要!
        alert(myGlobalObject.public); // コールバック内では this 使えない
      }, 1000);
    },
     ...
  }
} // コンストラクタを即時実行
  • 利点: 基本的には先ほどと同じだが更に書きやすくなった
  • 欠点: 欠点も基本的には先ほどと同じ

jQuery 内部でもよく使われている方式です。ブロックの前に new function() 付けるだけで済むので非常に読み書きしやすくなっています。個人的にはあまりデバッグの必要ない単純な小規模コードでのみ採用しています。

コンストラクタなので初期化したオブジェクトを返してグローバル名前空間オブジェクトに代入することもできます:

var myGlobalObject = new function() { // 返り値をグローバル名前空間オブジェクトに
  var private = "private"; // 関数内スコープで private 変数を定義
   ...
  this.public = "public"; // public 変数
   ...
  this.work = function() {
    alert(private); // private 変数は prefix 不要!
    alert(this.public);
     ...
  };
} // コンストラクタを即時実行 (インスタンスオブジェクト this が返される)

ただしこの書き方は public 変数を 1 つずつ代入するため getter などを多用する場合には不便ですし、返されるオブジェクトのプロトタイプチェインに無名コンストラクタが追加されてしまいます。そのため私はコンストラクタを使う場合も適当なオブジェクト global を作って return global; する方が好みです。

d. 無名関数に名前空間オブジェクトを apply して with する

var myGlobalObject = {}; // グローバル名前空間オブジェクト
(function() { with(this) { // this を名前空間として使う
  var private = "private"; // private 変数
  var func = function() { ... }; // private 関数
  this.public = "public"; // public 変数
  this.method = function() { ... }; // public メソッド
   ...
  this.work = function() {
    alert(private);
    alert(public); // public 変数も prefix 不要!
    func();
    method(); // public メソッドも prefix 不要!
    setTimeout(function() {
      alert(private);
      alert(public); // コールバック内でも prefix 不要!
      }, 1000);
    },
     ...
  }
}}).apply(myGlobalObject); // グローバルオブジェクトを this として使う
  • 利点: private/public 問わずに prefix 無しでアクセス可能な読み書きしやすい方法
    • private/public 共に prefix なしでアクセス可能
    • private/public 共に文末の “;” を忘れても構文エラーとならない
  • 欠点: 大規模開発を行う上で望ましい前提条件を満たさない
    • モジュール内の全ての関数が関数内関数となりデバッグ時にブレークポイントを設定しにくい
    • スコープが 2 段なのでグローバル変数へのアクセスが少し遅くなる

amachang さんが提案されていた方法です。書式が統一されてとても書きやすくなりますが、全体が関数に囲まれて個別メソッドにブレークポイントを設定しにくい問題が解決しないので個人的には採用していません。

e. 無名オブジェクトのプロパティにモジュール変数を定義して with する

with({ // private 変数/関数を定義する無名スコープオブジェクト
  private: "private", // private 変数
  func: function() { ... }, // private 関数
  anotherFunc: function() {
    alert(this.private); // private 関数の定義中では this. prefix が必要
    this.func();
  },
   ...
}) { // 本体部分では private 変数/関数 に prefix 不要
  var myGlobalObject = { // グローバル名前空間オブジェクト
    public: "public", // public 変数
    method: function() { ... }, // public メソッド
     ...
    work: function() {
      alert(private); // private 変数は prefix 不要
      alert(this.public); // public 変数は prefix 必要
      func(); // private 関数は prefix 不要
      this.method(); // public メソッドは prefix 必要
      setTimeout(function() {
        alert(private);
        alert(myGlobalObject.public); // コールバック内では this 使えない
      }, 1000);
    },
     ...
  }
}
  • 利点: 全体が関数で囲まれないのでブレークポイントが設定しやすい
    • private 変数は prefix なしでアクセス可能
  • 欠点: prefix や文末の “,” などに注意が必要
    • private 関数の定義中では private 変数アクセスに prefix が必要
    • public 変数のアクセスには prefix が必要
    • 変数の定義が全てプロパティになるため行末の “,” 書き忘れに注意が必要

JavaScript でスコープに影響を与えるのは関数と with 文 (と catch 節) だけであり、関数を使うとデバッグしにくいなら with を使おうという発想のコードです。個人的には、関数内でループしながら関数を定義するときにクロージャ変数が共通化されないようにする場合など局所的に使うことがあります。

catch もスコープに影響を与えるため try { throw { private: “private”, … }; } catch (scope) { … } などとしても private 変数の定義が可能ですが、常識的に考えてしないし with を使う場合に対する利点が思いつかないので割愛します。

f. 自身を返すメソッドを持つ無名オブジェクトを定義して with する

with ({ // private 変数/関数を定義する無名スコープオブジェクト
  scope: function() { return this; } // スコープオブジェクト自身を返す
}) {
  scope().private = "private"; // private 変数
  scope().func = function() { ... }; // private 関数
  scope().anotherFunc = function() {
    alert(private); // 初期化時にも prefix 不要
    func();
  };
  var myGlobalObject = { // グローバル名前空間オブジェクト
    public: "public", // public 変数
    method: function() { ... }, // public メソッド
     ...
    work: function() {
      alert(private);
      alert(this.public); // public 変数は prefix 必要
      func();
      this.method(); // public メソッドは prefix 必要
      setTimeout(function() {
        alert(private);
        alert(myGlobalObject.public); // コールバック内では this 使えない
      }, 1000);
    },
     ...
  }
}
  • 利点: 基本的には先ほどと同じだが private 変数を定義しやすくなった
    • private 変数は prefix なしでアクセス可能
  • 欠点: prefix や文末の “,” などに注意が必要
    • 変数定義の前に scope(). というのは特殊な記法
    • public 変数のアクセスには prefix が必要
    • 変数の定義が全てプロパティになるため行末の “,” 書き忘れに注意が必要

スコープオブジェクトを返すことで with ブロック本体側で private 変数を定義可能にしたものです。これにより private 関数まわりの prefix 取り扱いがし易くなりましたが、書き方が特殊すぎるので使いません。

g. 無名オブジェクトを用いた多段 with 文で prefix を不要にする

var myGlobalObject = {}; // グローバル名前空間オブジェクト
with (myGlobalObject) { // 名前空間オブジェクトの変数アクセスに prefix を不要に
with ({ scope: function() { return this; } }) { // 無名スコープオブジェクト
with ({ scope: scope(), ns: myGlobalObject }) { // 各オブジェクトのアクセスを簡単に
  scope.private = "private"; // private 変数
  scope.func = function() { ... }; // private 関数
  ns.public = "public"; // public 変数
  ns.method = function() { ... }; // public メソッド
  ns.work = function() {
    alert(private);
    alert(public); // public 変数も prefix 不要
    func();
    method(); // public メソッドも prefix 不要
    setTimeout(function() {
      alert(private);
      alert(public); // コールバック内でも prefix 不要
    }, 1000);
  };
}}}
  • 利点: 最初に要求した前提条件をほぼ全て満たす
    • private/public 共に prefix なしでアクセス可能
    • private/public 共に文末の “;” を忘れても構文エラーとならない
    • 名前空間オブジェクト名に依らずコードが書ける
    • デバッグ時のブレークポイントも設定しやすい
  • 欠点: 書き方がちょっと特殊すぎるか
    • 独自かつ特殊な方法なので他の人が読めない可能性がある
    • スコープが 3 段なのでグローバル変数へのアクセスが少し遅くなる

with をガンガン使って記法を簡単化していった場合です。名前空間オブジェクトと無名スコープオブジェクトの両方に対して with することで private/public 共に prefix なしで使用可能になっています。また、無名スコープオブジェクトをもう一つ挟むことで、スコープオブジェクトへのアクセスを関数呼び出しではなく、高速かつ簡単なスコープチェイン先頭のプロパティアクセスにしています。個人的には現在この書式を試してみています。

なお、IE との互換性を捨てて getter を使ってよい場合は次のように 2 段 with で済ませられます。

var myGlobalObject = {}; // グローバル名前空間オブジェクト
with (myGlobalObject) { // 名前空間オブジェクトの変数アクセスに prefix を不要に
with ({ get scope() { return this; }, ns: myGlobalObject }) { // 無名スコープオブジェクト
   ... // 中のコードは同じ
}}

this はグローバル変数名ではなくキーワードであるため、with だけでは名前空間オブジェクトを this で参照できるようにすることはできない(だから ns という名前を使っている)ことには注意が必要です。

h. let によるブロックスコープを用いる (正攻法)

{ // ブロックスコープ
  let private = "private"; // private 変数
  let func = function() { ... }; // private 関数
  var myGlobalObject = { // グローバル名前空間オブジェクト
    public: "public", // public 変数
    method: function() { ... }, // public メソッド
    work: function() {
      alert(private);
      alert(this.public); // public 変数は prefix 必要
      func();
      this.method(); // public メソッドは prefix 必要
      setTimeout(function() {
        alert(private);
        alert(myGlobalObject.public); // コールバック内では this 使えない
      }, 1000);
    },
     ...
  };
}
  • 利点: まっとうな方法
    • hack ではなく非常にシンプル
    • private 変数のアクセスには prefix 不要
  • 欠点: let によるブロックスコープをサポートするブラウザ限定
    • Firefox 2.0 からサポートされているので拡張機能開発では問題ない
    • public 変数のアクセスには prefix が必要

Hack ではなく普通に言語仕様として追加されたブロックスコープ機能を使うというパターンです。拡張機能などでは普通これでよいかと思います。

i. let によるブロックスコープと with を組み合わせる

var myGlobalObject = {}; // グローバル名前空間オブジェクト
with (myGlobalObject) { // 名前空間オブジェクトの変数アクセスに prefix を不要に
  let ns = myGlobalObject; // shorthand (必須ではない)
  let private = "private"; // private 変数
  let func = function() { ... }; // private 関数
  ns.public = "public"; // public 変数
  ns.method = function() { ... }; // public メソッド
  ns.work = function() {
    alert(private);
    alert(public); // public 変数も prefix 不要
    func();
    method(); // public メソッドも prefix 不要
    setTimeout(function() {
      alert(private);
      alert(public); // コールバック内でも prefix 不要
    }, 1000);
  };
}
  • 利点: 簡単な書式で前提条件をほぼ全て満たす
    • 多段 with とほぼ同じだかこちらの方がシンプル
  • 欠点: let によるブロックスコープをサポートするブラウザ限定
    • Firefox だと 2.0 からサポートしてるので拡張機能開発には問題ない
    • Strict モードでは with は使えなくなる

概ねシンプルな書式になりますが、getter/setter が面倒です。私はたまに拡張機能などでこの書式を使っています。

余談とか

いろいろな書式が考えられるので場合に応じて使いやすいものを使えばよいと思います。きっともっと良い方法があると思うので知ってる方は是非教えてください(この投稿のコメント欄または @dynamitter まで)。

各方式のパフォーマンス比較は行っていません。特に多段 with ではグローバルスコープ変数へのアクセスのペナルティが大きくなると予想されますが、グローバル変数アクセスは比較的少ない場合が多いこと、本当に速度が必要な場面ではいずれにしてもローカル変数にキャッシュするのが最速になるはずだろうとあまり気にしていません。

無名関数と無名コンストラクタについても、当然コンストラクタの方が this 初期化処理のペナルティがありますが、モジュール全体を囲むような大きなブロックにおいてその差は無視できるのであまり気にしていません。

apply して with する場合、多段 with を使う場合、let &ブロックスコープを使う場合のコードは、変数定義するときの prefix (var / scope. / let と this. / ns. / ns.)以外の本体のコードは同一になるため、相互変換が簡単だという特徴もあります。拡張機能用と Web 用で使い分ける場合でも頭の切り替えが楽です。

public 変数を prefix なしでアクセスしたいとか this の切り替えに困らないようにしたい問題については、private 変数さえ prefix なしでアクセスできる書式であれば public 変数を同名の private 変数に代入したり、var self = this したりする事でも回避できます。個人的には場合に応じて this/self 使い分けするのが何かイヤなのですが比較的わかりやすくよく行われています。

with の使用を避けるべき理由は性能、注意しないとミスしやすくなる、Strict モードで使えないとかいろいろ言われますが、モジュールのコード全体を囲む用途では書式統一されるし、Strict モード使えるならそもそも普通は let 使う書き方を選ぶハズなので、分かって使う分にはいいかと思っています。

getter を使った遅延初期化 (lazyInitialization) を行う private 変数を prefix なしでアクセスしたい場合など getter/setter を利用する場合には、各変数をオブジェクトリテラルのプロパティとして記述する書式の方が書きやすいです。各変数を var, let あるいは プロパティへの代入式で記述する場合に getter/setter とするには __defineGetter__ (あるいは defineProperty) を使う必要があり少し書きにくいです。これについては getter/setter プロパティだけスコープオブジェクトや名前空間オブジェクトの宣言時に含めておくのもありです。変数定義箇所や順序が乱れるケースでは個人的には避けたいですが。

var myGlobalObject = {}; // グローバル名前空間オブジェクト
with (myGlobalObject) { // 名前空間オブジェクトの変数アクセスに prefix を不要に
with ({
  get scope() { return this; },
  ns: myGlobalObject,
  get heavyObj() { // ここでは this
    delete this.heavyObj;
    return this.heavyOjb = /* XPCOM など重たい初期化処理 */ ;
  }
}) {
  scope.__defineGetter__("fatObj", function() {
    delete this.fatObj;
    return this.fatObj = /* XPCOM など重たい初期化処理 */ ;
  });
  ...
}}

4 件のコメント

  1. mitsugu :

     ちょっと教えていただきたいのですが、h の 「let によるブロックスコープを用いる」を用いる方法は、Firefox に overlay する JavaScript ファイルでも有効でしょうか。
     自作の拡張で使ったところ、他の拡張と conflict を起こす、という理由で公開申請が却下されたのですが。
     もちろん他の方法に書き換えるのはいとわないのですが、実際はどうなのかを知っておきたいのです。

  2. mitsugu :

    すみません。先の質問ですが、他の AMO レビューワの方から問題ない、と返事をもらいました。

  3. jog2 :

    大変分かりやすくて参考にさせていただきました。ありがとうございます。
    一つ気になったのですがbの1つ目のコード内「var func: function …」は、正しくは「var func = function …」では?

  4. dynamis :

    jog2 さん、ご指摘ありがとうございます&修正しました。m(_ _)m