変態文法と聞いて胸がときめく人なら、ぜひマスターしておきたいのが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で定義された変数が格納されている内部オブジェクトの名称です。
- Callオブジェクトのメンバ
- 一つ外側のCallオブジェクトのメンバ
- (中略)
- グローバルオブジェクトのメンバ
with構文はこの探索順序に割り込みをかけて、常にwithで宣言したオブジェクトが最優先で探索されるようにします。
- withで宣言されたオブジェクトのメンバ
- Callオブジェクトのメンバ
- …中略…
- グローバルオブジェクトのメンバ
この辺、詳しくは「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 (著); 村上 列 (翻訳); オライリー・ジャパン (刊)
keyword: javascript