JavaScriptの組み込みコンストラクタは、サブクラス化が困難です。 この章では、その理由と解決策を説明します。
JavaScriptでは組み込み型を拡張するという用語が別の意味で使用されているため、組み込み型のサブクラスを作成するという表現を使用します。
組み込み型のサブクラス化には、内部プロパティを持つインスタンスと、関数として呼び出すことができないコンストラクタという2つの障害があります。
ほとんどの組み込みコンストラクタは、内部プロパティと呼ばれるプロパティを持つインスタンスを持ちます(プロパティの種類を参照)。内部プロパティの名前は、`[[PrimitiveValue]]` のように二重角括弧で囲まれています。内部プロパティはJavaScriptエンジンによって管理され、通常はJavaScriptから直接アクセスできません。JavaScriptにおける通常のサブクラス化の手法は、サブコンストラクタの `this` を使用して、スーパークラスコンストラクタを関数として呼び出すことです(レイヤー4: コンストラクタ間の継承を参照)。
functionSuper(x,y){this.x=x;// (1)this.y=y;// (1)}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (2)// Add subpropertythis.z=z;}
ほとんどの組み込み型は、`this` として渡されたサブインスタンス(2)を無視します。これは次のセクションで説明する障害です。さらに、既存のインスタンス(1)に内部プロパティを追加することは、一般的に不可能です。なぜなら、内部プロパティはインスタンスの性質を根本的に変えてしまう傾向があるからです。したがって、(2)の呼び出しは内部プロパティを追加するために使用できません。以下のコンストラクタは、内部プロパティを持つインスタンスを持ちます。
`Boolean`、`Number`、`String` のインスタンスはプリミティブ値をラップします。これらはすべて、`valueOf()` によって値が返される内部プロパティ `[[PrimitiveValue]]` を持ちます。 `String` はさらに2つのインスタンスプロパティを持ちます。
Array
Date
Function
RegExp
内部インスタンスプロパティ `[[Match]]` と、2つの非内部インスタンスプロパティ。ECMAScript仕様書より:
`[[Match]]` 内部プロパティの値は、`RegExp` オブジェクトのパターンの実装依存の表現です。
内部プロパティを持たない組み込みコンストラクタは、`Error` と `Object` のみです。
`MyArray` は `Array` のサブクラスです。空の要素( `length` プロパティでは空の要素とみなされる)を無視して、配列内の実際の要素数を返すゲッター `size` を持っています。 `MyArray` を実装するために使用されるトリックは、配列インスタンスを作成し、そのメソッドを `MyArray` インスタンスにコピーすることです:[22]
functionMyArray(/*arguments*/){vararr=[];// Don’t use Array constructor to set up elements (doesn’t always work)Array.prototype.push.apply(arr,arguments);// (1)copyOwnPropertiesFrom(arr,MyArray.methods);returnarr;}MyArray.methods={getsize(){varsize=0;for(vari=0;i<this.length;i++){if(iinthis)size++;}returnsize;}}
このコードは、オブジェクトのコピーで示され、説明されているヘルパー関数 `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
`Error` とそのサブクラスは内部プロパティを持つインスタンスを持たない場合でも、標準的なサブクラス化のパターンが機能しないため、簡単にサブクラス化できません(前述の内容を繰り返します)。
functionSuper(x,y){this.x=x;this.y=y;}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (1)// Add subpropertythis.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`)に追加する場合にのみ機能します。
サブコンストラクタ内で、新しいスーパークラスのインスタンスを作成し、その独自のプロパティをサブインスタンスにコピーします。
functionMyError(){// Use Error as a functionvarsuperInstance=Error.apply(null,arguments);copyOwnPropertiesFrom(this,superInstance);}MyError.prototype=Object.create(Error.prototype);MyError.prototype.constructor=MyError;
ヘルパー関数 `copyOwnPropertiesFrom()` は、オブジェクトのコピーに示されています。 `MyError` を試してみると:
try{thrownewMyError('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
委譲は、サブクラス化に代わる非常にクリーンな方法です。 たとえば、独自の配列コンストラクタを作成するには、プロパティに配列を保持します。
functionMyArray(/*arguments*/){this.array=[];Array.prototype.push.apply(this.array,arguments);}Object.defineProperties(MyArray.prototype,{size:{get:function(){varsize=0;for(vari=0;i<this.array.length;i++){if(iinthis.array)size++;}returnsize;}},length:{get:function(){returnthis.array.length;},set:function(value){returnthis.array.length=value;}}});
明らかな制限は、角括弧を使用して `MyArray` の要素にアクセスできないことです。要素にアクセスするには、メソッドを使用する必要があります。
MyArray.prototype.get=function(index){returnthis.array[index];}MyArray.prototype.set=function(index,value){returnthis.array[index]=value;}
`Array.prototype` の通常のメソッドは、以下のメタプログラミングによって転送できます。
['toString','push','pop'].forEach(function(key){MyArray.prototype[key]=function(){returnArray.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'