第28章 組み込み型のサブクラス化
目次
書籍を購入する
(広告です。ブロックしないでください。)

第28章 組み込み型のサブクラス化

JavaScriptの組み込みコンストラクタは、サブクラス化が困難です。 この章では、その理由と解決策を説明します。

用語

JavaScriptでは組み込み型を拡張するという用語が別の意味で使用されているため、組み込み型のサブクラスを作成するという表現を使用します。

組み込み型 `A` のサブクラスを作成する
指定された組み込みコンストラクタ `A` のサブコンストラクタ `B` を作成すること。 `B` のインスタンスは `A` のインスタンスでもあります。
オブジェクト `obj` を拡張する
あるオブジェクトのプロパティを別のオブジェクトにコピーすること。Underscore.jsはこの用語を使用しており、Prototypeフレームワークによって確立された伝統を継承しています。

組み込み型のサブクラス化には、内部プロパティを持つインスタンスと、関数として呼び出すことができないコンストラクタという2つの障害があります。

障害1: 内部プロパティを持つインスタンス

ほとんどの組み込みコンストラクタは、内部プロパティと呼ばれるプロパティを持つインスタンスを持ちます(プロパティの種類を参照)。内部プロパティの名前は、`[[PrimitiveValue]]` のように二重角括弧で囲まれています。内部プロパティはJavaScriptエンジンによって管理され、通常はJavaScriptから直接アクセスできません。JavaScriptにおける通常のサブクラス化の手法は、サブコンストラクタの `this` を使用して、スーパークラスコンストラクタを関数として呼び出すことです(レイヤー4: コンストラクタ間の継承を参照)。

function Super(x, y) {
    this.x = x;  // (1)
    this.y = y;  // (1)
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (2)
    // Add subproperty
    this.z = z;
}

ほとんどの組み込み型は、`this` として渡されたサブインスタンス(2)を無視します。これは次のセクションで説明する障害です。さらに、既存のインスタンス(1)に内部プロパティを追加することは、一般的に不可能です。なぜなら、内部プロパティはインスタンスの性質を根本的に変えてしまう傾向があるからです。したがって、(2)の呼び出しは内部プロパティを追加するために使用できません。以下のコンストラクタは、内部プロパティを持つインスタンスを持ちます。

ラッパーコンストラクタ

`Boolean`、`Number`、`String` のインスタンスはプリミティブ値をラップします。これらはすべて、`valueOf()` によって値が返される内部プロパティ `[[PrimitiveValue]]` を持ちます。 `String` はさらに2つのインスタンスプロパティを持ちます。

  • `Boolean`: 内部インスタンスプロパティ `[[PrimitiveValue]]`。
  • `Number`: 内部インスタンスプロパティ `[[PrimitiveValue]]`。
  • `String`: 内部インスタンスプロパティ `[[PrimitiveValue]]`、カスタム内部インスタンスメソッド `[[GetOwnProperty]]`、通常のインスタンスプロパティ `length`。 `[[GetOwnProperty]]` は、配列インデックスが使用されたときに、ラップされた文字列から読み取ることで、文字のインデックス付きアクセスを可能にします。
Array
カスタム内部インスタンスメソッド `[[DefineOwnProperty]]` は、設定されるプロパティをインターセプトします。配列要素が追加されたときに `length` を最新の状態に保ち、`length` が小さくされたときに余分な要素を削除することで、`length` プロパティが正しく機能することを保証します。
Date
内部インスタンスプロパティ `[[PrimitiveValue]]` は、日付インスタンスによって表される時刻を(1970年1月1日00:00:00 UTCからのミリ秒数として)格納します。
Function
内部インスタンスプロパティ `[[Call]]`(インスタンスが呼び出されたときに実行されるコード)など。
RegExp

内部インスタンスプロパティ `[[Match]]` と、2つの非内部インスタンスプロパティ。ECMAScript仕様書より:

`[[Match]]` 内部プロパティの値は、`RegExp` オブジェクトのパターンの実装依存の表現です。

内部プロパティを持たない組み込みコンストラクタは、`Error` と `Object` のみです。

障害1の回避策

`MyArray` は `Array` のサブクラスです。空の要素( `length` プロパティでは空の要素とみなされる)を無視して、配列内の実際の要素数を返すゲッター `size` を持っています。 `MyArray` を実装するために使用されるトリックは、配列インスタンスを作成し、そのメソッドを `MyArray` インスタンスにコピーすることです:[22]

function MyArray(/*arguments*/) {
    var arr = [];
    // Don’t use Array constructor to set up elements (doesn’t always work)
    Array.prototype.push.apply(arr, arguments);  // (1)
    copyOwnPropertiesFrom(arr, MyArray.methods);
    return arr;
}
MyArray.methods = {
    get size() {
        var size = 0;
        for (var i=0; i < this.length; i++) {
            if (i in this) size++;
        }
        return size;
    }
}

このコードは、オブジェクトのコピーで示され、説明されているヘルパー関数 `copyOwnPropertiesFrom()` を使用します。

数値を1つだけパラメータとして `Array` コンストラクタを呼び出すと、その数値は要素にならず、空の配列の長さを決定するという癖があるため、(1)行目では `Array` コンストラクタを呼び出しません(要素を持つ配列の初期化 (避けるべき!)を参照)。

以下はインタラクションです。

> var a = new MyArray('a', 'b')
> a.length = 4;
> a.length
4
> a.size
2

注意点

インスタンスにメソッドをコピーすると、プロトタイプを使用できる場合は避けられる冗長性が生じます。さらに、`MyArray` は、`MyArray`のインスタンスではないオブジェクトを作成します。

> a instanceof MyArray
false
> a instanceof Array
true

障害2: 関数として呼び出すことができないコンストラクタ

`Error` とそのサブクラスは内部プロパティを持つインスタンスを持たない場合でも、標準的なサブクラス化のパターンが機能しないため、簡単にサブクラス化できません(前述の内容を繰り返します)。

function Super(x, y) {
    this.x = x;
    this.y = y;
}
function Sub(x, y, z) {
    // Add superproperties to subinstance
    Super.call(this, x, y);  // (1)
    // Add subproperty
    this.z = z;
}

問題は、`Error` は関数として呼び出された場合でも(1)、常に新しいインスタンスを生成することです。つまり、`call()` を介して渡されたパラメータ `this` を無視します。

> var e = {};
> Object.getOwnPropertyNames(Error.call(e)) // new instance
[ 'stack', 'arguments', 'type' ]
> Object.getOwnPropertyNames(e) // unchanged
[]

上記のインタラクションでは、`Error` は独自のプロパティを持つインスタンスを返しますが、それは `e` ではなく、新しいインスタンスです。サブクラス化のパターンは、`Error` が独自のプロパティを `this`(上記のケースでは `e`)に追加する場合にのみ機能します。

障害2の回避策

サブコンストラクタ内で、新しいスーパークラスのインスタンスを作成し、その独自のプロパティをサブインスタンスにコピーします。

function MyError() {
    // Use Error as a function
    var superInstance = Error.apply(null, arguments);
    copyOwnPropertiesFrom(this, superInstance);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

ヘルパー関数 `copyOwnPropertiesFrom()` は、オブジェクトのコピーに示されています。 `MyError` を試してみると:

try {
    throw new MyError('Something happened');
} catch (e) {
    console.log('Properties: '+Object.getOwnPropertyNames(e));
}

Node.jsでの出力は次のとおりです。

Properties: stack,arguments,message,type

`instanceof` の関係は、期待どおりです。

> new MyError() instanceof Error
true
> new MyError() instanceof MyError
true

別の解決策: 委譲

委譲は、サブクラス化に代わる非常にクリーンな方法です。 たとえば、独自の配列コンストラクタを作成するには、プロパティに配列を保持します。

function MyArray(/*arguments*/) {
    this.array = [];
    Array.prototype.push.apply(this.array, arguments);
}
Object.defineProperties(MyArray.prototype, {
    size: {
        get: function () {
            var size = 0;
            for (var i=0; i < this.array.length; i++) {
                if (i in this.array) size++;
            }
            return size;
        }
    },
    length: {
        get: function () {
            return this.array.length;
        },
        set: function (value) {
            return this.array.length = value;
        }
    }
});

明らかな制限は、角括弧を使用して `MyArray` の要素にアクセスできないことです。要素にアクセスするには、メソッドを使用する必要があります。

MyArray.prototype.get = function (index) {
    return this.array[index];
}
MyArray.prototype.set = function (index, value) {
    return this.array[index] = value;
}

`Array.prototype` の通常のメソッドは、以下のメタプログラミングによって転送できます。

[ 'toString', 'push', 'pop' ].forEach(function (key) {
    MyArray.prototype[key] = function () {
        return Array.prototype[key].apply(this.array, arguments);
    }
});

`MyArray` のインスタンスに格納されている配列 `this.array` で `Array` メソッドを呼び出すことによって、`Array` メソッドから `MyArray` メソッドを派生させます。

`MyArray` の使用

> var a = new MyArray('a', 'b');
> a.length = 4;
> a.push('c')
5
> a.length
5
> a.size
3
> a.set(0, 'x');
> a.toString()
'x,b,,,c'



[22] Ben Nadelのブログ記事に触発されました。

次: 29. JSDoc: APIドキュメントの生成