17 Jul 2010

Learning Advanced JavaScript のメモ

Learning Advanced JavaScript

John Resig さんによる JavaScript のチュートリアルサイトです. 中身は彼が現在執筆中の本 "The Secret of JavaScript Ninja" の内容からきています. 全ページほぼコードだけなんですが, これが意外とわかりやすい. またコードをその場で編集・実行できるのがかっこいいです.

JavaScript Ninja の Closure の章の中で, prototype.js の bind() の実装を解説している部分があります. こういうコードです.

// The .bind method from Prototype.js
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)));
  };
};

ここは, 本ではわりとさらっと流されてるんですが, なかなか難しい. このチュートリアルでは, 最終的にこのコードを理解することを目的として, 前提知識の部分からコードを1ステップずつ説明していってくれています. 本でわかりづらいなと思った部分があればこのチュートリアルを見てみればいいと思います. また過程で関数宣言やクロージャ, プロトタイプにも触れていくので, 逆にこのチュートリアルが面白いなと思った人はぜひ本のほうも読んでみるといいと思います. とても面白い本です.

Secrets of the JavaScript Ninja: John Resig, Bear Bibeault: 8601400825082: Amazon.com: Books

自分も最近2回参加させてもらっている若手IT勉強会でも, いま JavaScript Ninja の読書会をやっています.

no title

というわけで, 以下メモです.

p13, 14: 名前付き無名関数

13と14の意味が一瞬わからなかったんですが, よく見ると ninja.yell に入っている関数が, 13だと無名関数, 14だと名前付き無名関数になっています. なので, yell のなかで自分を参照したい時, 13の場合 ninja.yell としなければいけないので, ninja オブジェクトに null を代入すると yell が見えなくなってしまうという仕組みです.

p15: arguments.callee

上記をふまえて, 無名関数から自分自身を参照したいときは arguments.callee が使えるよという話.

arguments.callee - JavaScript | MDN

ただ, arguments.callee (とcaller) は ECMAScript 5 では使えなくなる?との話が.

John Resig - ECMAScript 5 Strict Mode, JSON, and More

この記事(これも resig さん)によると, calleeの代替として名前付き無名関数で対処する方法が紹介されています. チュートリアルだと p14 と同じ話です.

setTimeout(function later(){
  // do stuff...
  setTimeout( later, 1000 );
}, 1000 );

ただまだ問題があって, たまたま見つけたこのエントリでは, IEだと名前付き無名関数がグローバルの名前空間を汚染してしまうということを指摘しています.

Web Reflection: [ECMAScript 5] Do Not Remove arguments.callee !

例えばこんなコード:

var a = function abc() {
     alert("in abc()");
}

a();   // "in abc()"

alert(window.abc);   // undefined を期待

当然ですが, fxなどのブラウザでこのコードを動かしてみると2つ目のアラートでは undefined が表示されます. しかしIEだとなぜか abc() がグローバルスコープになってしまいます (少なくともIE7では確認. 8もだめで9+でOKとどこかで見たが未検証). これだと名前が衝突しちゃうかもしれないのでよろしくない. こういうケースでは確かに callee を使いたいですね.

p23: 変な刀

一度切ると切れ味が悪くなり, もう一度切るとよくなる謎の刀.

p26: call() と apply()

call()とapply()の違いは, 引数をひとつひとつ渡すかまとめて1つの配列として渡すかだけ.

p43, 44, 45: arguments を array に変換

arguments は array っぽいけど array じゃない array-like なオブジェクトです. 添字でのアクセスや length プロパティはあるけれど, array にあるメソッドを持っていません. 今回は arguments を配列として扱いたいので, array に変換する必要がある. そんなときは Array.slice() を使うと一発で変換できて便利です.

Array.prototype.slice.call(arguments);

arguments に適用させたいので prototype から呼びます. 実はこのテクニックは MDC でも紹介されていたりします.

arguments - JavaScript | MDN

p46, 47: 用途不明関数

配列の最初の要素と最大の要素を掛け合わせる関数. 何に使うんだろう.

p56, 59: (function(){})()

これは結構有名な話. 以下のコードは, プログラマの意図としては 1, 2, 3 とアラートされて欲しいところですが, 実際は毎回 3 がアラートされます. setTimeout() で3つの無名関数が変数 i への参照を持ってそれぞれ js の実行キューに積まれますが, それらの関数が実行されるタイミングでは i はループが終わって値が 3 になっているので, すべて 3 がアラートされてしまいます.

for ( var i = 0; i < 3; i++ ) {
 setTimeout(function(){
   alert(i);   // 全部3
 }, 100);
}

こんな時は (function(){})() を使うのが常套手段. ちなみに (function(){})() のことをなんと呼べばいいんだろう. チュートリアルには "Self-executing, temporary, function" と書いてありました.

for ( var i = 0; i < 3; i++ ) (function(i){  // temporary scope
 setTimeout(function(){
   alert(i);  // 0, 1, 2 とアラートされる
 }, 100);
})(i);

p72: constructor

constructor() とすればコンストラクタにアクセスできる. new foo.constructor() == new foo() なので, 2通りの方法でインスタンスを作成できる.

function Ninja(){}
var ninja = new Ninja();
var ninjaB = new ninja.constructor();

Object.prototype.constructor - JavaScript | MDN

p76: prototypeベースの継承

プロトタイプベースの継承を実現するには, 親クラスのインスタンスを小クラスのprototypeに代入.

Ninja.prototype = new Person(); 

p81: Object の prototype を拡張するときの注意

Object のプロトタイプに独自のプロパティを追加すると, object 全体をイテレートしたときに, その独自追加プロパティも出てきてしまいます. この動作がたまにミスにつながってしまって, 例えば下のようなコードにすると, "key" もobjの要素としてカウントされてしまうので注意.

Object.prototype.keys = function(){
  var keys = [];
  for ( var i in this )
    keys.push( i );
  return keys;
};

var obj = { a: 1, b: 2, c: 3 };


alert(obj.keys().length);   //  4になる

p86: bind()

ここまでくれば bind() のコードも読めるはずです.

Function.prototype.bind = function(){
  // fn は bind() が呼ばれた時のコンテキスト
  // args は bind() に渡された引数を配列に変換したもの
  // object は bind() の第一引数, つまりある関数をバインドしたい対象のコンテキスト
  // object には args を1回 shift して代入しているので, 
  // args にはもとの関数に渡したい引数(bind()の第二引数以降)が入っていることになる
  var fn = this, args = Array.prototype.slice.call(arguments), object = args.shift();
  return function(){
      // ここでのargumentsはbind()を呼び出した時点の引数ではなく, 
      //"bind()が適用された関数"が呼び出された時の引数が入っている
      // concat でその引数と, bind() の時点での引数 (args) をつなげて,
      // まとめて関数に渡している
    return fn.apply(object,
      args.concat(Array.prototype.slice.call(arguments)));
  };
};

p90: arguments.length

arguments.length やこれまでのテクニックを用いて, 引数の渡し方によって呼び出す関数を変更するポリモフィズムのようなことを実現しています. これはかっこいいです.

function addMethod(object, name, fn){
  // Save a reference to the old method
  var old = object[ name ];

  // Overwrite the method with our new one
  object[ name ] = function(){
    // Check the number of incoming arguments,
    // compared to our overloaded function
    if ( fn.length == arguments.length )
      // If there was a match, run the function
      return fn.apply( this, arguments );

    // Otherwise, fallback to the old method
    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" );

参考

別の方のエントリも発見したのでメモ