JavaScript:with構文によるブロックスコープ再考

Posted by Hiraku on 2011-04-25

変態文法と聞いて胸がときめく人なら、ぜひマスターしておきたいのがJavaScriptのwith構文です。スピード狂やuse strict信奉者に蔑まれ、そのうち黒歴史として消滅しそうな哀れな構文ですが、消えるには惜しい。ちょっと光を当ててみましょう。

以下、パフォーマンス無視の文章でいきますのでよろしくお願いしますm(_ _)m

with文の教科書的な説明

もともとwithはオブジェクトのメンバを展開するための構文でした。たとえばdocument.getElementById()などのDOMのメソッド類は多用すると思いますが、名前が長いですよね。せめてdocument.を省略できないものか、と誰もが思うでしょう。ちまちまやるなら、

var getElementById = document.getElementById,
    getElementsByName = document.getElementsByName
    ...

とこういう風に書いていけばいいんですが、手動でやると面倒くさい。これを自動でやってくれる(のと同じ効果がある)のがwith構文なのです。

with (document) {
  //このブロックの中ではdocument.を省略できる
  var hoge = getElementById('hoge');
  ...
}

withの仕組み

with構文は、実際に変数を展開しているわけではなく、スコープチェーンに割り込むことでこの挙動を実現しています。通常、変数が実際に何を指しているかは、以下の順番で探索されます。ここでいうCallオブジェクトとは、varで定義された変数が格納されている内部オブジェクトの名称です。

  1. Callオブジェクトのメンバ
  2. 一つ外側のCallオブジェクトのメンバ
  3. (中略)
  4. グローバルオブジェクトのメンバ

with構文はこの探索順序に割り込みをかけて、常にwithで宣言したオブジェクトが最優先で探索されるようにします。

  1. withで宣言されたオブジェクトのメンバ
  2. Callオブジェクトのメンバ
  3. …中略…
  4. グローバルオブジェクトのメンバ

この辺、詳しくは「Callオブジェクト」や「スコープチェーン」などでググってください。あとは参考文献のJavaScript第5版 4章「変数」あたりが詳しい。Callオブジェクトの解説はここでは省略します。

ブロックスコープ構文の"発見"

さて、ただの変数展開の構文にすぎなかったwithですが、無名オブジェクトを組み合わせることで、ブロックスコープを実現する使い方が発見されました。

with ({a: 5}) {
  console.log(a); //このブロックの中だけ、a==5
}
console.log(a); //undefined

参考:JavaScript でブロックスコープを実現する: Days on the Moon

{a: 5}はどこにも代入されておらず、名前がないので、そのままでは道端の石ころのごとくメモリー上から捨てられるのみです。しかしwithを併用すると、スコープチェーンへの割り込みという形でブロック内だけ、この無名オブジェクトが使えるようになるのです。

withの解説が終わったところで本題

このwithによるブロックスコープは便利です。何より可読性がすこぶる高い。ちょっとネストすると即時関数を使ったときとの差は歴然です。セミコロンが必要ない、というのも大きいですね。

with ({a: 1}) {
  with ({b: 2}) {
    with ({c: 3}) {
      console.log(a);
      console.log(b);
      console.log(c);
    }
  }
}

ただ、これだとあらかじめ{…}の中で宣言しておいた変数しか使えません。動的にどんどん変数を追加したい場合、どうすればいいのでしょう?

こちらの記事の最後の方に、いくつか書き方が載っています。
JavaScript のブロックスコープと名前空間 ≪ Mozilla Developer Street (modest)

with ({scope: function(){return this}}) {
with ({scope: scope()}) {
  scope.a = 'local';//ローカル変数の宣言

  console.log(a); //宣言したらあとはprefixなしでアクセス可能

}}

うーん。withが2段で結構ややこしいですね。そこでもう一つやり方を考えてみました。これならwith一段で済みます。

with (new function(){this.my=this}) {
  my.a = 'local'; //ローカル変数の定義

  console.log(a); //宣言したらあとはprefixなしでアクセス可能
}

さらに、原文だとmyGlobalObjectにprefixなしでアクセスするためにwithを2段にしていますが、こういう関数を作るとwithは一段で済むようになります。

function createScope(obj) {
  function f(){this.my=this}
  f.prototype = obj;
  return new f;
}

使い方はこちら。引数に任意のオブジェクトを渡すと、それを__proto__へ押し込んだ新しいスコープオブジェクトを返します。これによりスコープオブジェクトが一つでも、複数のスコープを探索することができるようになります。

var a = 'global';

with (createScope()) {
  console.log(a); //'global'
  my.a = 'local1';
  console.log(a); //'local1'

  with (createScope(my)) {
    console.log(a); //'local1'
    my.a = 'local2';
    console.log(a); //'local2'
  }

  console.log(a); //'local1'
}

console.log(a);// 'global'

この関数はスコープチェーンをプロトタイプチェーンへ切り替える役割を持ちます。単純にwithを多段にすると、ネストの遠いところへの変数のアクセスが遅くなっていきますが、大抵の処理系ではプロトタイプチェーンの探索の方が速いので、遠いところへのアクセスが少し速くなる効果が期待できます。

いろいろやり残したところ

もうちょっといろいろ書きたかったんですが、気力がなくなったので今日はここまでです。

  • ちゃんとパフォーマンス計る
  • スコープチェーンよりプロトタイプチェーンの方が速いって本当かな(spidermonkeyとV8(node.js)しか試してない)
  • this.my=thisみたいな循環参照を作ってもメモリリークしないか
  • createScope()の解説をもう少し書きたい

参考

JavaScript 第5版 [大型本] / David Flanagan (著); 村上 列 (翻訳); オライリー・ジャパン (刊)" title="JavaScript 第5版 [大型本] / David Flanagan (著); 村上 列 (翻訳); オライリー・ジャパン (刊)
JavaScript 第5版 [大型本] / David Flanagan (著); 村上 列 (翻訳); オライリー・ジャパン (刊)

keyword: javascript

JavaScriptの最新記事