Secrets of the JavaScript Ninja 4章を読んで来た

4章を読みました.読んだのは5/12だったのですが,復習した内容をブログに書くまでにこんなに時間がかかってしまいました.昨日は5章を読んで来たので,また復習です!

クロージャとは

Wikipediaによる説明はコチラ
自分なりの理解としては,クラスベースのオブジェクト指向言語でいうところのprivateなクラスのようなものを実現できる仕組み!?
という感じです.

プライベート変数の話

thisを使うとパブリックなプロパティになり,そとからオブジェクト.プロパティで呼べるが,varで定義された変数は外からアクセス不可になります.

function Ninja(){ 
  var slices = 0; 
  
  this.getSlices = function(){ 
    return slices; 
  }; 
  this.slice = function(){ 
    slices++; 
  }; 
} 
var ninja = new Ninja(); 
ninja.slice(); 
assert( ninja.getSlices() == 1, 
  "We're able to access the internal slice data." ); 
assert( ninja.slices === undefined, 
  "And the private data is inaccessible to us." );

コールバックとタイマーの話

1つはjQueryを例として,ajax関数を使ったときのsuccessというキー内の無名関数は外からアクセスできないのでクロージャです.
もうひとつはsetInterval関数などのタイマーをつかったときの第一引数に指定する関数はクロージャという例.
ちなみに,ここまでがクロージャの導入部分だそうです.
この後から難しくなりました><

関数のコンテキストを強化(Enforcing Function Context)

イベント関係のプログラムでは,実行時のコンテキストがイベントを設定した要素になってしまうという例

<button id="test">Click Me!</button> 
<script> 
var Button = { 
  click: function(){ 
    this.clicked = true; 
  } 
}; 
var elem = document.getElementById("test"); 
elem.addEventListener("click", Button.click, false); 
// triggerという関数はないので,elem.click();とか書いてイベントを発生させます
trigger( elem, "click" ); 
assert( elem.clicked, 
  "The clicked property was accidentally set on the element" ); 
</script>

FirebugとかでButtonオブジェクトのclickメソッドのthisを見てみると分かるのです
が,testというidを持つbutton要素になっています.これをbindという関数を使って,
強制的にButtonオブジェクトにしています.

<button id="test">Click Me!</button> 
<script> 
function bind(context, name){ 
  return function(){
    // Buttonオブジェクトのコンテキストで
    // Buttonオブジェクトのclickメソッドを実行!
    return context[name].apply(context, arguments); 
  }; 
} 
var Button = { 
  click: function(){ 
    this.clicked = true; 
  } 
}; 
var elem = document.getElementById("test"); 
elem.addEventListener("click", bind(Button, "click"), false); 
trigger( elem, "click" ); 
assert( Button.clicked, 
  "The clicked property was set on our object" ); 
</script> 

同じような意味になるように書き換えてみると,こんな感じでしょうか.

function bind(context, name){
  return function(){
    return context[name].apply(context, arguments);
  };
}

var Button = {
  click: function(){
    this.clicked = true;
  }
};

var elem = document.getElementById("test");
//elem.addEventListener("click", bind(Button, "click"), false);
// ↑を書き換えて,↓のようにしてみました.
elem.addEventListener("click", function(){
  Button["click"].apply(Button);
  assert( Button.clicked,
    "The clicked property was set on our object" );
}, false);

同じように動作するようにコードを書き換えてみるという事は,読書会中にid:cheesepieさんが自分が書いてあるコードが今ひとつ理解できないときに説明する為にやってくれたことで,自分はFirebugでやみくもにブレークポイント付けたりしてうんうんしていたのですが,復習するときにやってみたら,すごく理解が進むのでとても良い方法だなと感じました!

あと,

hoge["fuga"]

hoge.fuga

シンタックスシュガーです.

bind関数の中ではメソッド名が文字列で渡ってくるので最初の書き方をしてるんですね.

なお,bind関数はPrototype.jsで実装されて広まったそうです.
今までの例で出てきたbind関数は簡易版で,Prototype.jsのbind関数は

Function.prototype.bind = function(){ 
  var fn = this, args = Array.prototype.slice.call(arguments), 
    object = args.shift(); 
  
  return function(){ 
    return fn.apply(object, 
      args.concat(Array.prototype.slice.call(arguments))); 
  }; 
}; 

のようになっていて,もっと汎用的です.

関数に部分的に適用(Partially Applying Functions)

ここでは,実行前に引数を埋めておく関数が出てきます.

String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = ("John, Resig, Boston").csv(); // resultsには,[John, Resig, Boston]という配列が返ってきます

のような渡した文字列がカンマ区切りで配列になって返ってくるような関数です.partial関数は,

Function.prototype.partial = function(){ 
  var fn = this, args = Array.prototype.slice.call(arguments); 
  return function(){ 
    var arg = 0; 
    for ( var i = 0; i < args.length && arg < arguments.length; i++ ) 
      if ( args[i] === undefined ) 
        args[i] = arguments[arg++]; 
    return fn.apply(this, args); 
  }; 
}; 

のような実装になっていて,最初,

String.prototype.split.partial(/,\s*/);

の時点で,csvメソッドには,下のような関数が設定されています.

// args = /,\s*/, fn = split() が設定されています
function(){ 
    var arg = 0; 
    for ( var i = 0; i < args.length && arg < arguments.length; i++ ) 
      if ( args[i] === undefined ) 
        args[i] = arguments[arg++]; 
    return fn.apply(this, args); 
};

ここで("John, Resig, Boston").csv()を実行すると,
args.lengthは1, arg < arguments.lengthは 0 < 0の為,偽になるので,forループは実行されず,returnとして

 String.prototype.split.call(("John, Resig, Boston"), /,\s*/);

の結果が返され,文字列を引数に渡した正規表現を分割する条件とした結果が,配列で返ってくることがわかります.この例ではforループは使ってませんが,csvメソッドに引数が設定されていて,partialメソッドの引数にundefinedが設定されているとき,同じインデックスを持つ引数がpartialメソッドに設定されることになりますね!

なので,

Function.prototype.partial = function(){
  // 関数オブジェクトをコンテキスト
  // 引数は配列として扱えるようにする
  var fn = this, args = Array.prototype.slice.call(arguments);
  // クロージャを返す
  return function(){
    var arg = 0;
    for ( var i = 0; i < args.length && arg < arguments.length; i++ )
      if ( args[i] === undefined )
        args[i] = arguments[arg++];
    return fn.apply(this, args);
  };
};

String.prototype.csv = String.prototype.split.partial(undefined); // undefinedで設定!
var results = ("John, Resig, Boston").csv(/,\s*/); // デリミタを後から設定

としても同じ結果になります!

こういう後から実行する為に,ある引数をもった関数を保持しておくことをカリー化というそうです.
カリー化しておくと,以下のようなことができるようになります.

var delay = setTimeout.partial(undefined, 10);
delay(function(){ 
  assert( true, 
  "A call to this function will be temporarily delayed." ); 
});

setTimeout関数を10ms後に実行する関数delayを定義しておいて,
後から使いたい時に呼び出しています.イベントを簡単に追加できて便利ですね!

関数の振る舞いを上書きする話

この節では,メモ化という概念が出てきました.

Function.prototype.memoized = function(key){ 
  this._values = this._values || {}; 
  return this._values[key] !== undefined ? 
    this._values[key] : 
    this._values[key] = this.apply(this, arguments); 
}; 
function isPrime( num ) { 
  var prime = num != 1;
  for ( var i = 2; i < num; i++ ) { 
    if ( num % i == 0 ) { 
      prime = false; 
      break; 
    } 
  } 
  return prime; 
} 
assert( isPrime.memoized(5), 
  "Make sure the function works, 5 is prime." ); 
assert( isPrime._values[5], "Make sure the answer is cached." );

memoizedメソッドは,isPrime関数に_valuesという変数があれば,そのまま返し,なければ,isPrime関数に_valueというプロパティを設定します.初期値は空のオブジェクトですね.
返り値は,memoizedメソッドに渡した引数をisPrime関数の引数として実行した結果になります.
5は素数なので,_values[5]にはtrueが格納されることになります.
その結果,2番目のassertで最初なかった_values[5]がisPrime関数のプロパティとして設定され,値はtrueなのでテストが通る事になります.
memoizedメソッドは,キャッシュを実現してますね!

次にもっと簡単にキャッシュをできるようにした例が,

Function.prototype.memoize = function(){ 
  var fn = this; 
  return function(){ 
    return fn.memoized.apply( fn, arguments ); 
  }; 
}; 
var isPrime = (function( num ) { 
  var prime = num != 1; 
  for ( var i = 2; i < num; i++ ) {
    if ( num % i == 0 ) { 
      prime = false; 
      break; 
    } 
  } 
  return prime; 
}).memoize(); 
assert( isPrime(5), "Make sure the function works, 5 is prime." ); 
assert( isPrime._values[5], "Make sure the answer is cached." );

です.これ,個人的には難しくて何度かfirebugとかで途中で止めつつやっと理解できました><
まず,isPrimeには,さっきまでisPrime関数だった関数をオブジェクトとしてmemoizeメソッドを実行すると,()内の素数判定関数をthisとして,memoizedメソッドを引数を5として実行します.すると,isPrime変数のプロパティにキーに5,値trueを持つプロパティ_valuesが設定され,前の例と同じ結果が得られる.
となるはずなのですが,ブラウザで実行してもうまくいきません.
Firebugで追ってみると,isPrimeがグローバルな領域にあるので,windowオブジェクトに対して_valuesが設定されてしまっているようです!??公式フォーラムにも報告されていて,memoizeメソッドで_valuesを設定してあげないと正しく動きません...

// なんかthisだとthisとisPrimeが別々になっちゃって_valuesがキャッシュされない><
Function.prototype.memoized = function(key){
  this._values = this._values || {};
  return this._values[key] != undefined ?
    this._values[key] :
    this._values[key] = this.apply(this, arguments);
 };

// 本ではこうだけど
//Function.prototype.memoize = function(){
//  var fn = this;
//  return function(){
//    return fn.memoized.apply( fn, arguments );
//  };
//};

// forumによると,こう書き換えないと動かない!
// 確かにそうだった
Function.prototype.memoize = function(){
  var fn = this;
  return function(){
    // isPrimeの_valuesはmemoizeにて定義!?
    arguments.callee._values = function(key){
      if(key) return fn._values[key];
      else return fn._values;
    };
    return fn.memoized.apply( fn, arguments );
  };
};

var isPrime = (function( num ) {
  var prime = num != 1;
  for ( var i = 2; i < num; i++ ) {
    if ( num % i == 0 ) {
      prime = false;
      break;
    }
  }
  return prime;
}).memoize();

window.onload = function(){
assert( isPrime(5), "Make sure the function works, 5 is prime." );
// テストケースもisPrime._values[5] から isPrime._values(5)に変わってる
assert( isPrime._values(5), "Make sure the answer is cached." );
}

関数のラッパー

ここでは,もともとある関数に機能追加するテクニックが紹介されています.
readAttributeメソッドクロスブラウザ対応させるため,wrapという関数でOperaでreadAttributeというメソッドによってtitle属性を取得できるようにしています.

function wrap(object, method, wrapper){ 
  var fn = object[method]; 
  return object[method] = function(){ 
    return wrapper.apply(this, [ fn.bind(this) ].concat( 
      Array.prototype.slice.call(arguments))); 
  }; 
} 
// Example adapted from Prototype 
if (Prototype.Browser.Opera) { 
  wrap(Element.Methods, "readAttribute", function(orig, elem, attr){ 
    return attr == "title" ?
      elem.title : 
      orig(elem, attr); 
  }); 
}

Prototype.Browser.Operaの部分はPrototype.jsを使わないと動きません.
ブラウザ判定のための条件分岐ですね.あと,ElementオブジェクトのMethodsプロパティもPrototype.jsの中で定義されていました.
ここで,実行ブラウザがOperaの場合,wrap関数が実行されます.
wrap関数は,Element.Methods.readAttributeをwrap関数の第3引数に指定した関数で上書きしています.上書きされた後のreadAttributeメソッドは,

Element.Methods["readAttribute"] = function(){
  return function(orig, elem, attr) {
    return attr == "title" ?
      elem.title :
      orig(elem, attr);
  }
}

のような内容になると思います.

最近良くみるコレ → (function(){})()

最近良くみます.jQueryとかだと特に良く見ますね.
最初はなんだコレ!?という感じだったのですが,名前空間を汚染せずにクロージャ内にデータを閉じ込めるための書き方だったんですね!
jQueryプラグインとかだと,

(function($){
 // 処理
})(jQuery);

となっているようで,jQueryオブジェクトをクロージャ内で$として使えるようにしています.

(function(){})()を使った例として,画面がクリックされるとカウンタが増えていくようなイベントを登録しておいて,カウンタの値が保持されるようなしくみが紹介されています.

(function(){ 
  var numClicks = 0; 
  
  document.addEventListener("click", function(){ 
    alert( ++numClicks ); 
  }, false); 
})();

同じやり方として,

>|javascript||
document.addEventListener("click", (function(){
var numClicks = 0;

return function(){
alert( ++numClicks );
};
})(), false);
|