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

『Effective JavaScript』を読んで(項目37~42)

Effective JavaScript シリーズ。

項目37:this の暗黙的な結合を理解しよう

this のスコープは呼び出した関数の文脈によって決定される。

function Obj(array, replaced) = {
    this.array = array;
    this.replaced = replaced;
}
Obj.prototype.count = function() {
    return array.map(function(el) {
        return this.replaced;
    });
}
var obj = new Obj([1, 2, 3], 10);
obj.count(); // => [undefined, undefined, undefined]

上記のコードにおいて、obj.count() は [10, 10, 10] を返すように思えるが、実際には異なる。

内側の this.replaced の this は、map に渡している無名関数が呼び出された際のレシーバに結合される。この場合、レシーバは指定されていないのでグローバルオブジェクトに束縛される。グローバルオブジェクトには replaced プロパティが存在しないので、結果的にすべての要素が undefined に置き換えられている。

幸い、map メソッドは第二引数で第一引数の関数のレシーバを明示的に指定することが出来る。他にも、外側のスコープで selft という変数に this を束縛しておくという方法、bind を用いて this を束縛したラッパー関数を与えるという方法がある。

// 明示的な this の指定を使う方法
Obj.prototype.count = function() {
    return array.map(function(el) {
        return this.replaced;
    }, this); // 明示的な this の指定
}

// 外側のスコープで self に束縛する方法
Obj.prototype.count = function() {
    var self = this; // self に束縛
    return array.map(function(el) {
        return self.replaced;
    });
}

// bind を使って外側の this を内部の this に束縛したラッパー関数を渡す方法
Obj.prototype.count = function() {
    return array.map(function(el) {
        return this.replaced;
    }.bind(this)); 
}

項目38:スーパークラスコンストラクタは、サブクラスのコンストラクタから呼び出す

JavaScript で継承を実装するには二つの作業が必要である。

コードにすると以下の通り。

function Super(x, y) {
   this.x = x;
   this.y = y;
}
function Sub(a, x, y) {
   Super.call(this, x, y);
   this.a = a;
}
Super.prototype.superMethod = function () { return "superMethod"; };
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.subMethod = function () { return "subMethod"; };

var obj = new Sub(1, 2, 3)
obj.x // => 2
obj.superMethod() // => "superMethod"

項目39:スーパークラスのプロパティ名は、決して再利用しない

スーパークラスのプロパティ名と同様のプロパティ名を持つプロパティをサブクラスで再利用しないこと。スーパークラスのプロパティは一般的にはプライベートではないので、上書きしてしまい、バグの温床になりやすい。

function Object(objId) {
   this.id = objId;
}
funciton User(userId, objId) {
   Object.call(this, objId);
   this.id = userId; // Object のコンストラクタで設定した id を上書きしている。
}

項目40:標準クラスからの継承を避ける

ECMAScript の標準ライブラリは小規模だが Array, Function などの重要なクラスが含まれている。これらをサブクラスで拡張したくなるが、これは避けた方が良い。

実は、これらのクラス定義には特別な振る舞いが含まれていて正しくサブクラスを書くことが出来ないからだ。

JavaScript のクラスには Class というクラスの種類を示す特別なタグが存在する。 Array や Function などの組み込みのクラスは Class に特別な値を保持していて、これに応じて特別な処理が行われるように実装されているのだ。

たとえば、length プロパティは内部プロパティ Class が "Array" のオブジェクトについては、「そのオブジェクトでインデックスを付けられているプロパティの数と同期する」という特別な動作が定義されている。 Array を拡張したサブクラスの場合、この Class の値が "Object" になってしまうので、length プロパティの特別な動作ができないのである。

Class の値は Object.prototype.toString を呼び出すことで調べることができる。

Object.prototype.toString.call({}); // => "[object Object]"
Object.prototype.toString.call([]); // => "[object Array]"

Array に限らずその他の標準クラス、Boolean, Date, Function, Number, RegExp, String についてもほぼ同様の事情があるため、これらのクラスから継承するのは避けるべきである。

項目41:プロトタイプを「実装の詳細」として扱おう

JavaScriptJavaC++ などと比べ、内部構造を露出する機構を多く備えている。

  • プロパティを列挙する機構がある(Object.prototype.hasOwnProperty)
  • プロトタイプチェーンをたどる機構がある(Object.getPrototypeOf)
  • プライベートプロパティがない

これらをやみくもに使って実装の詳細に依存したプログラムを書くのはやめよう。 ライブラリにドキュメント化されていない実装に依存したプログラムを書くと、実装が変更された際に痛い目をみる。

項目42:やみくもなモンキーパッチを避ける

項目41で述べたように JavaScript にはプロトタイプチェーンをたどる機構があるが、さらに既存クラスへのプロトタイプオブジェクトに対するプロパティの追加が可能である。

Array.prototype.split = function(i) { 
    return [this.slice(0, i), this.slice(i)];
};

これでどんな配列でも便利な split が使えるようになったぞ!と喜んではいけない。 こうやって自分の責務外のオブジェクトにプロパティを追加することをモンキーパッチング(Monkey-Patching)と呼ぶ。 この例ではうまく動くようにみえるのだが、他の人がさらにセマンティクスの異なる split をモンキーパッチングしているかもしれない。

ただし、最新の処理系に追加されているような便利な関数を過去の処理系に対して追加するポリフィル(polyfill)と呼ばれる用途では、このモンキーパッチが正当化される。

例えば ES5 で追加された forEach や map、filter などをポリフィルする場合などである。

if (typeof Array.prototype.map !== "fucntion") {
    Array.prototype.map = function(f, thisArg) {
        var result = [];
        for (var i = 0, n = this.length; i < n; i++) { 
            result[i] = f.call(thisArg, this[i], i);          
        }
        return result;
    } 
}

また、ポリフィルする場合には文書化し、場合によってユーザーにポリフィルするかを洗濯させる手法を採るのも効果的。

function addArrayMethods() {
    Array.prototype.split = function(i) {
        return [this.slice(0, i), this.slice(i)];
    } 
}