読者です 読者をやめる 読者になる 読者になる

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

先々週からd:id:cheesepieさんと読み始めたJavaScript Ninjaですが,
週1ペースで読もうという事で今回は3章 Functionsです.

匿名関数(無名関数)の話

jsは関数型言語らしい.schemeというlispの仲間からたくさんインスピレーション受けてる.
匿名関数を理解することはきれいなコードを書いたり再利用可能なコードを書く事につながるよ!
というようなことが書いてありました.

再帰の話

ここの例は割と普通の再帰でした.
知らなかったのはarguments.calleeというものですね.
これは実行中のオブジェクトそれ自身を表すものだそうです.
親オブジェクトが変更になっても,arguments.calleeを使っておけば,
変更無しで動作するのでメンテ性が良いです.
あとは匿名関数に名前が付けられるのも知りませんでした^^;
再帰の時に便利ですね!

オブジェクトとしての関数の話

オブジェクトは

var obj = {};
var fn  = function(){};

のように定義できます.
どちらもオブジェクトなのでプロパティが追加できます

obj.prop = "hoge";
fn.prop  = "hoge"; // obj.prop == fn.propです

ちなみに、

return !!(hoge);

のような記述が出てきたのですが,
これは

hoge === true

シンタックスシュガーです!1人で読んでたときは否定の演算子が二つあるから
結局true?みたいにあんまり意識してなかったんですが,式を評価して返す形になってたんですね。。

自身の中身を記憶しておく関数の話

オブジェクトのプロパティに値を設定しておくと,次に呼び出した時にも前に設定した値が
返ってくるという例.
これを利用してキャッシュ機構を実装できます.

function getElements( name ) {
	return getElements.cache[ name ] = getElements.cache[ name ] || document.getElementsByTagName( name );
}
getElements.cache = {};

一度目はdocuments...の方が返されるのですが,2回目以降ではgetElements.cache[ name ]
が返されるようになります.これはよく使うということでした.深い階層にあるDOM要素を
取得しようとすると,内部的には階層をどんどんたどって要素を探しに行く処理が走るので
キャッシュはパフォーマンス的に有効な手段です.Firefox 3によるベンチでは,10倍近くの
差が出ていました.どのブラウザでもこのくらいの差になるとのこと.

コンテキストの話

2章でconsole.log.applyとかArray.prototype.join.callとかいきなり出てきて,
丁寧に説明してもらってようやくイメージだけは出来たのですが,ここの例を実行したり,
Firebugでthisに何が入っているかを見てみたりして大分理解できました.

下の例だと,
thisはグローバルではWindowオブジェクト,関数の中のthisは関数自身ということをふまえると,

var katana = { 
	isSharp: true, 
	use: function(){ 
		this.isSharp = !this.isSharp; 
	} 
}; 
katana.use() 
assert( !katana.isSharp, 
	"Verify the value of isSharp has been changed." );

katanaオブジェクト中のthis.isSharpは初期値はtrueですが,katana.use()でuseメソッド
が実行され,isSharpプロパティがfalseになります.その後,グローバルな領域で
katana.isSharpを参照することは,katanaオブジェクト内のthis.isSharpを
参照していることと等価なので,このテストが通る事になります.
次の例とかもっと分かりやすいですね.

function katana(){
  this.isSharp = true; 
}
katana();
assert( isSharp === true,
"A global object now exists with that name and value." );

var shuriken = {
	toss: function(){
		this.isSharp = true;
	}
};
shuriken.toss(); 
assert( shuriken.isSharp === true, 
"It's an object property, the value is set within the object." );

katana関数はグローバルなので,thisはwindowオブジェクトを指します.
shurikenオブジェクトの中のthisは自身なので,shriken.isSharpはtrueを返します.

ちなみに,assertという関数は,2章に出てきたのですが,簡単なテスト用関数です.
第一引数がtrueだと第二引数に設定されている文字が緑,falseだと赤になります.
テストではお決まりの色ですね!
3章以降は分かってるものとして説明なしで出てきます.

callとapplyメソッド!

callとapplyは第一引数は共に実行対象のオブジェクト,第二引数にはapplyは配列,
callは配列以外のものがとれるという違いがあります.

var object = {};
function fn(){
	return this;
}

assert( fn() == this, "The context is the global object." );
// objectオブジェクトからfn関数をcall
// objectオブジェクト内のthisはオブジェクト!
assert( fn.call(object) == object,
	"The context is changed to a specific object." );

fnはthisを返すだけの関数です.最初のアサーションでは,グローバル空間での呼び出しなので,
fnのthisはwindowオブジェクト,右辺のthisもグローバル空間内なのでwindowオブジェクトで
trueになります。
次のアサーションは,fnメソッドをobjectオブジェクト内で実行します.
objectオブジェクトのthisはobjectオブジェクトなので,
fn.call(object)はオブジェクトになりtrueになるという感じです.
この挙動は初め見たときちんぷんかんぷんでしたが,やっと慣れてきました!

ループの話

ここは関数の引数に関数を使ってこんなことができますという感じでした.

function loop(array, fn){
  for ( var i = 0; i < array.length; i++ )
    if ( fn.call( array, array[i], i ) === false )
      break;
}
var num = 0;
loop([0, 1, 2], function(value, i){
  assert(value == num++,
    "Make sure the contents are as we expect it.");
}); 

配列っぽい関数の話

call,applyを使うと配列っぽい関数?(表現が変なような..?)が作れます.

ちなみに,ここの例にあるプログラム、そのままでは動きませんでした。。
本は間違ってなくて自分がどこか間違ってるに違いない!と思ってたのですが,
読書会中に聞いてみたところ,なんとResigさんの間違いでした!

<input id="first"/><input id="second"/> 
<script> 
var elems = { 
	find: function(id){ 
		this.add( document.getElementById(id) ); 
        }, 
	length: 0,
	add: function(elem){ 
		Array.prototype.push.call( this, elem ); 
	} 
}; 
elems.add("first"); // mistake!
assert( elems.length == 1 && elems[0].nodeType, 
	"Verify that we have an element in our stash" ); 
elems.add("second"); // mistake!
assert( elems.length == 2 && elems[1].nodeType, 
	"Verify the other insertion" ); 
</script>

間違っているのはelemsオブジェクトのaddメソッドを呼び出しているところです.
addメソッドを呼び出しただけでは,firstとかsecondとかいう名前がelemsオブジェクトに
追加されるだけでlengthプロパティは作成されるのですが,elems[0].nodeTypeが未定義になり
テストが失敗します.
正しくは,elems.find("first")のようにfindメソッドを使います.
そうすることによってinputタグにDOM要素が追加され,elemsオブジェクトに
lengthプロパティとnodeTypeプロパティが追加されることになりテストが通ります.
jQueryを作った神のような人でも間違えるんですね...

関数のオーバーロードの話

最初の方の例はオーバーロードじゃないのでは!?という感じだったんですが,
Function Lengthの節のところでこんなコードが出てきました.

function addMethod(object, name, fn){ 
  var old = object[ name ]; 
  object[ name ] = function(){ 
    if ( fn.length == arguments.length ) 
      return fn.apply( this, arguments ) 
    else if ( typeof old == 'function' ) 
      return old.apply( this, arguments ); 
  }; 
}
function Ninjas(){ 
  var ninjas = [ "Dean Edwards", "Sam Stephenson", "Alex Russell" ]; 

  addMethod(this, "find", function(){ 
    return ninjas; 
  }); 
  addMethod(this, "find", function(name){ 
    var ret = []; 
    for ( var i = 0; i < ninjas.length; i++ ) 
      if ( ninjas[i].indexOf(name) == 0 ) 
        ret.push( ninjas[i] ); 
    return ret; 
  }); 
  addMethod(this, "find", function(first, last){ 
    var ret = []; 
    for ( var i = 0; i < ninjas.length; i++ ) 
      if ( ninjas[i] == (first + " " + last) ) 
        ret.push( ninjas[i] ); 
    return ret; 
  }); 
} 
var ninjas = new Ninjas(); 
assert( ninjas.find().length == 3, "Finds all ninjas" ); 
assert( ninjas.find("Sam").length == 1, 
  "Finds ninjas by first name" ); 
assert( ninjas.find("Dean", "Edwards").length == 1,
  "Finds ninjas by first and last name" ); 
assert( ninjas.find("Alex", "X", "Russell") == null, "Does nothing" );

短いコードですが,この章でやったこと全部入りです!
再帰,applyメソッド,匿名関数, arguments...
最初意味がわからなくてぽかーんだったのですが,またしてもid:cheesepieさん
に丁寧に解説してもらって,さらにaddMethod関数の最後に

if(old)
  console.log(old.toString());

を付け加えるとすることで構造が理解しやすくなるとアドバイスをもらったりしてようやく分かってきました.
先生!ありがとうございます!
こうすると1回目のoldには何も設定されていなくて(undefined),2回目のoldには1回目の
object[ name ]に設定されている匿名関数が,3回目のoldには2回目のobject[ name ]に
設定されている匿名関数が設定されて再帰構造になっていることが分かります.
これで渡された引数の数に対応したfind関数が呼ばれる仕組みになっています.
キモはold.apply( this, arguments );の部分です.


次(4章)はクロージャです!

2010/06/03修正: id:cheesepieさんのidを間違えて書いていたので直しました。スミマセン...