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

『Effective JavaScript』を読んで(項目23~29)

『Effective JavaScript』を読んでシリーズ。

項目23:arguments オブジェクトを書き換えない

arguments オブジェクトは配列のように見えるが、標準 Array 型のインスタンスではないので、shift() などのメソッドを直接呼び出すことはできない。PerlUNIXシェルスクリプトになれているプログラマは注意が必要。

call メソッドを利用して、Array 型のインスタンスから無理矢理 shift を抽出して利用しようとしてもうまくいかない。 名前のついた引数は、arguments の各要素への別名(alias)として実装されているからだ。

function callMethod(obj, method) {
    var shift = [].shift;
    shift.call(arguments);
    shift.call(arguments);
    return obj[method].apply(obj, arguments);
}
var obj = {
    add: function(x, y) { return x + y; }
};
callMethod(obj, "add", 17, 25);

上記の例で callMethod の引数である obj と method はそれぞれ arguments[0] と arguments[1] の別名として解釈される。そのため、callMethod 内の obj[method] は、(argument[0])([arguments[1]]) と解釈される。すでに shift メソッドによって arguments の最初の二つの要素は削除されているので、実際には (17)[(25)] と解釈され、暗黙的な型変換などを得て複雑なエラーになってしまう。

strict モードを利用した場合、arguments の書き換えは禁止される。

このように環境によって挙動が推測しずらいので、一般的に arguments の書き換えは避けた方がよい。もし、arguments を更新する必要がある場合には、配列としてコピーしてから利用する。イディオムは以下の通り。

var args = [].slice.call(arguments);

項目24:arguments へのリファレンスは変数に保存する

arguments はいつでも「関数の引数の配列」に結合されるので注意しよう。

以下の values.internal() 内の arguments は values() の arguments とは異なるので注意。利用したい場合には、別途ローカル変数へ値を保存しておく。

function values() {
  var args = arguments // values 関数の arguments
  return {
     internal: function() {
        args; // values 関数の arguments へのリファレンス
        arguments; // internal 関数の arguments
     }
  }
}

項目25:固定レシーバを持つメソッドを抽出するには bind を使う

抽出した関数のレシーバーは、それを取り出したオブジェクトと結合されていない。高階関数やコールバックとして関数を利用する際に忘れがちな事実である。

var buffer = {
    entries: [],
    add: function(s) {
        this.entroies.push(s);
    }
};
var source = [1, 2, 3]
source.forEach(buffer.add); // => add 内の this を解決できないのでエラー

この場合、forEach 内で buffer.add 関数を単に呼び出すことになるので、this にはグローバルオブジェクトが結合される。

もし、buffer をレシーバとして add 内の this に結合したい場合には、forEach にレシーバを指定する方法と、buffer をレシーバとして add を呼び出す高階関数を作成する方法の二種類がある。

var source = [1, 2, 3];
source.forEach(buffer.add, buffer); // buffer.add を呼び出す際のレシーバとして buffer を指定

var source = [1, 2, 3];
source.forEach(function(s) { 
    buffer.add(s); // 明示的に buffer をレシーバとして add を呼び出す
});

このように、関数に対してそのレシーバを特定のオブジェクトに結合(bind)するバージョンを作成するのは一般的に行われるため、ES5 では bind という関数をライブラリでサポートしている。

bind はレシーバオブジェクトを受け取り、そのレシーバのメソッドとして関数を呼び出すラッパー関数を作成する。bind によって作成された関数はラッパー関数なのでもとの関数とは異なるオブジェクトである点に注意する。

var soruce = [1, 2, 3];
var wrapper = buffer.add.bind(buffer); // buffer.add のレシーバとして buffer が結合されている関数
source.forEach(wrapper); 

buffer.add === buffer.add.bind(buffer); // => false。同じ関数ではない

項目26:関数を部分適用するには、bind を使う

関数を部分適用するために bind が使える。

function add(x, y) { 
    return x + y;
}
var add2 = addThreeVal.bind(null, 2) // x = 2 で束縛された add 関数を返す
add2(1); // => 3

前項で述べたとおり bind の第一引数には固定化したいレシーバを与えるが、関数内で this を参照しない場合には null か undefined を与えるのが定石。第一引数以降は束縛したい引数値を与える。

このように関数に対して、その引数の部分集合を結合して、その値がすでに束縛された関数を生成するテクニックを部分適用と呼ぶ。

さて、『Effective JavaScript』内ではこのテクニックを『カリー化』と呼んでいるのだが、以下のエントリを見るとこれは『部分適用』と呼んだ方が正しそうだ。

  • カリー化:複数の引数をとる関数 A を、引数:「A の最初の引数」、戻り値:「A の最初の引数以外を取る関数」という関数 A' にすること。
  • 部分適用:特定の引数をある値に束縛させた状態の新たな関数を返すこと。カリー化の結果を利用して、部分適用することもできる。

というような違いがある。

JavaScript でカリー化は以下のようなコードになるんだろうと思う。

// カリー化された add 関数
var curriedAdd = function(x) { 
    return function(y) {
        return x + y;
    } 
};
var add2 = curriedAdd(2); // カリー化された関数を利用して部分適用。
add2(1); // => 3

項目27:コードをカプセル化するには、文字列ではなくクロージャを使う

コードを再利用した場合、文字列としてコードを書いてそれを eval で評価するという方法はやめてクロージャを使いましょうねという話。

理由は、

  • 文字列はスコープを保持できない。クロージャはできる。
  • 最適化が効きにくい

という感じ。

大学の時、Ruby で eval を使いすぎて教授に「これはよくないですね~」と注意を受けたことを思い出した。

項目28:関数の toString() メソッドに依存するのはやめよう

JavaScript にはソースコードを文字列として復元する機能がある(初めて知った)。

ただし、ECMAScript の規格ではなにも言及されていないので処理系によって動作が異なるし、正確性も保証されていない。

たとえば、bind 関数で束縛された関数はネイティブコードで実装されていることが多いので、toString() を使っても何のあてにもならない。

// Firefox 34.0.5 上
function getName() {
   return this.name; 
};
var obj = { 
    name: "hajime" 
};
(getName).toString() // => "function getName() { return this.name }" 
(getName.bind(obj)).toString() // => "function getName() { [native code] }"

以上の理由からこの機能に依存したコードを書くのは避けた方が良い。何となくセキュリティ上の問題もありそう気がする。

項目29:非標準のスタック調査プロパティを使うのは避けよう

一部の古い環境では、呼び出された関数自体を参照する arguments.callee、呼び出した関数を参照する arguments.caller という二つの変数をサポートしている。これらを使ってスタックトレースを出力するという機能が作成できなくはないが、再帰を含んでいたりするとすぐに破綻する。

加えて、非標準のため可搬性が保持されないので基本的には利用しないようにすること。

各環境に用意されたデバッガなどのスタックトレース機能を正しく使いましょう。