JavaScript おれおれ Advent Calendar 2011 – 16日目

(※「スルー力」は「するーか」じゃなくて「するーりょく」です。)

.apply()を使って他の関数を実行できる事は昨日示しました。

これを応用して、メソッドが呼ばれた際にそのままスルーして、別のメソッドが呼ばれたかのように振る舞う事ができます。

スルー

例によって配列風のオブジェクトを考えます。これに .join()を実装しましょう。

var obj = {
  "0": "a",
  "1": "b",
  "2": "c",
  length: 3,
  join: function() { /* ??? */ }
};

console.log(obj.join());  // "a,b,c"にしたい
console.log(obj.join("/"));  // "a/b/c"にしたい

はてさて、どういう設計で実装しましょうか。仕様は? 試験は?

いえ、自分で書かずに丸ごと放り投げてしまいましょう。ここで .apply()の出番です。

var obj = {
  "0": "a",
  "1": "b",
  "2": "c",
  length: 3,
  join: function() { return Array.prototype.join.apply(this, arguments); }
};

console.log(obj.join());  // => "a,b,c"
console.log(obj.join("/"));  // => "a/b/c"

これでnative codeで実装されている処理を我が物顔で実行できるようになりました。

この fn.apply(this, arguments)という慣用句(イディオム)は、個人的には結構頻繁に使います。

ちなみに.apply()と似たものに .call()というメソッドもあるのですが、この書き方はできません。

おれおれログ出力

もうひとつ具体例を。

デバッグ用にログを出力する関数です。console.log()が使用可能ならそれで、なければalert()を使って出力します。

function log() {
  // console.logが使えればそれで出力
  if (window.console && console.log) {
    console.log.apply(console, arguments);
  }
  // consoleが使えなければalert()で出力
  else {
    // alert()は引数がひとつだけなので、結合してから渡す
    var text = Array.prototype.join.apply(arguments, [', ']);
    alert(text);
  }
}

log("abc", 1 + 99);

console.log()は引数の数は無制限なので、引数に名前を付ける事ができません。そこでargumentsオブジェクトを利用します。

一方alert()の引数はひとつだけなので、全てまとめて結合してやる必要があります。が、argumentsは配列ではなく配列風のオブジェクトですから、 .join()がありません。ここでも .apply()を使って .join()を実行してやる事ができます。(昨日の記事を参照。)

あ、ちなみにこの関数↑だと、使い方によってはalert()のダイアログがぽこぽこ出て来て使い勝手良くないかなーと思います。(logて名前も妙だし。)まあ、よしなに。

追記(19:08):Hokamuraさんから反応頂きました。

これだと出力されたとき(主にchromeのdev toolsのときの話し)の行数がconsole.log.applyのところになっちゃうので個人的にはこっちのほうがいい。

var log = (function() {
  if (window.console && console.log) {
    return console.log.bind(console);
  }
  else {
    return function() {
      var text = Array.prototype.join.apply(arguments, [', ']);
      alert(text);
    }
  }
})();
log("abc", 1 + 99);

これだとlog()実行したところの行数が出る。

勝手に解説しますと、普通コンソールに出力すると内容だけでなくて、console.log()が実行されたファイル名と行数が表示されるんです。

で、ご指摘の通り、僕のコードだと「console.log()を実行した行」がlog()の中なので、常にそこの行数が表示されてしまうわけです。ああ、せっかくの便利機能が無駄に!

そこでHokamuraさんのコードに注目しますと、log()は直接関数として宣言されず、関数を返す匿名関数の戻り値になっています。最終的にlogは関数になるので、あくまでログ出力が「 log()の中」ではなく「log()を実行した場所」になる、というのが重要な点です。

.bind()については説明が面倒なので省略します。戻り値が関数になるって事だけ理解してください。 _(・ω・`_)⌒)_ ごめんね MDN見てね

ちなみに .bind()はIE 8、Safari 5.1.2では動きませんでした。IE 9では動くみたいです。

(追記ここまで。)

関連