Sunday, June 22, 2008

[JavaScript] Closures

今John Resigの「Pro JavaScript Techniques」を読んでいるが、Closuresについてちょっとメモ。

Closuresとは、内部関数が外部関数の変数を参照するというもので、以下にサンプルを書いてみる。

  1. window.onload = function () {  
  2.   var obj = document.getElementById("main");  
  3.   obj.style.border = "1px dashed red";  
  4.   delayedHide(obj, 2000);  
  5. }  
  6.   
  7. function delayedHide(obj, time) {  
  8.   setTimeout(function() {  
  9.     obj.style.display = "none";  
  10.   }, time);  
  11. }  


「idが"main"という要素を赤の点線で囲み、2秒後に消す」という簡単なサンプルだ。これをClosuresを使わないで書くと、window.onload内でsetTimeoutを書くことになり、以下のようなコードになる。

  1. var obj;  
  2. window.onload = function() {  
  3.   obj = document.getElementById("main");  
  4.   obj.style.border = "1px, dashed red";  
  5.   setTimeout("delayedHide()", 2000);  
  6. }  
  7.   
  8. function delayedHide() {  
  9.   obj.style.display = "none";  
  10. }  


この場合は、objをグローバルに宣言する必要があるため、他のライブラリがobjというグローバル変数を使っていた場合にコードが破綻する。また、objを引数にしてもよいが、そうすると、

  1. window.onload = function() {  
  2.   obj = document.getElementById("main");  
  3.   obj.style.border = "1px, dashed red";  
  4.  setTimeout("delayedHide(obj)", 2000);  
  5.  alert(window.obj == obj); // グローバルかを確認  
  6. }  
  7.   
  8. function delayedHide(obj) {  
  9.   obj.style.display = "none";  
  10. }  


と書いてしまいがち。意図したとおりに動くので問題ないように見えるが、実はwindow.onloadで使用されているobjは、グローバル変数になってしまっているので、実際の引数としてobjはdelayedHideに渡っていない。
上の例に書いたように、alert(window.obj == obj);をwindow.onload内で書くか、delayedHideの引数名objから別の名前に変えることで確認することができる。

じゃあ、window.onload内でobjをvarを使って宣言し、この関数内のスコープにおさめようとすると、以下のようになる。

  1. window.onload = function() {  
  2.   var obj = document.getElementById("main"); // local scope  
  3.   obj.style.border = "1px, dashed red";  
  4.   setTimeout("delayedHide(obj)", 2000);  
  5.   alert(window.obj == obj); // グローバルかを確認  
  6. }  
  7.   
  8. function delayedHide(obj) {  
  9.   obj.style.display = "none";  
  10. }  


これだとobjが意図したとおりにdelayedHideに渡らないので、"main"要素が消えない。いろいろ悪あがきをして、以下のように書いたとしても、ブラウザによっては動作したりしなかったりで、もう蟻地獄だ。

  1. setTimeout("delayedHide(" + obj + ")", 2000);  
  2. setTimeout("delayedHide('" + obj + "')", 2000);  
  3. setTimeout("delayedHide(" + this.obj + ")", 2000);  


そもそも、関数を呼び出すのに関数名の文字列と変数を組み合わせて値を作るっていうのは、あり得ない。

ということで、function()がいろんな場所に入るので慣れるまでは少し違和感を感じるかもしれないが、Closuresを使ったコードの記述は、ポータビリティを保証するよい方法なのだ。

No comments: