サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

JavaScript: オブジェクト複製のメソッドを継承する ー Object.prototype.constructorプロパティ


オブジェクトを複製するメソッドが求められることは少なくありません。基本的な機能ですので、スーパークラスに定めてサブクラスで継承したいこともあるでしょう。けれど、複製するオブジェクトをコンストラクタの呼び出しでつくろうとすれば、関数をどのようにして参照すればよいのかが問われます。

01 座標を管理する親クラスとベクトル演算する子クラス

まず、親クラス(Point)は以下のコード001のとおり、xy座標をプロパティにもちます。そして、ふたつのゲッターのプロパティ(lengthとangle)が、それぞれ原点からの距離およびx軸正方向となす角度を返します。このクラスは「JavaScript: クラスにゲッター/セッターでプロパティを定める」でつくりました(コード006)ので、詳しくはその解説をお読みください。 つぎのテスト用のJavaScriptコードは、座標値の引数に(1, √3)を与えてオブジェクト(obj)をつくっています。この場合、距離は2、角度は60度になります。なお、角度はラジアン値が返されるので、変換比率のクラス定数(Point.RAD_TO_DEG)を掛け合わせて度数に直しました。


var obj = new Point(1, Math.sqrt(3));
console.log(obj.length, obj.angle * Point.RAD_TO_DEG);
// 1.9999999999999998 59.99999999999999

コード001■オブジェクトに与えた座標の原点からの距離およびx軸となす角をゲッターで返す


function Point(x, y) {
	var _x = 0;
	var _y = 0;
	Object.defineProperties(this, {
		x: {
			get: function() {
				return _x;
			},
			set: function(x) {
				_x = isNaN(x) ? _x : x;
			}
		},
		y: {
			get: function() {
				return _y;
			},
			set: function(y) {
				_y = isNaN(y) ? _y : y;
			}
		}
	});
	this.x = x;
	this.y = y;
}
Object.defineProperties(Point.prototype, {
	length: {
		get: function() {
			var square = this.x * this.x + this.y * this.y;
			return Math.sqrt(square);
		}
	},
	angle: {
		get: function() {
			return Math.atan2(this.y, this.x);
		}
	}
});
Object.defineProperty(Point, "RAD_TO_DEG", {
	value: 180 / Math.PI
});

つぎに、子クラス(Vector)は、以下のコード002のとおり、親クラス(Point)の座標にベクトルの加算や伸縮のメソッド(add()とscale())を加えます。Object.create()メソッドでつくった親クラスのオブジェクトをプロトタイプオブジェクトに与えて継承を行いました。つぎのテスト用のコードは、座標に加算や伸縮を加えたうえで、親クラスから継承した距離(length)と角度(angle)を確かめています。


var obj = new Vector(0.5, 0);
console.log(obj.length, obj.angle * Point.RAD_TO_DEG);
// 0.5 0
obj.scale(2);
obj.add(0, Math.sqrt(3));
console.log(obj.length, obj.angle * Point.RAD_TO_DEG);
// 1.9999999999999998 59.99999999999999

コード002■ベクトルの加算と伸縮のメソッドを備えたサブクラス


function Vector(x, y) {
	Point.call(this, x, y)
}
Vector.prototype = Object.create(Point.prototype);
Vector.prototype.constructor = Vector;
Vector.prototype.add = function(x, y) {
	this.x += x;
	this.y += y;
};
Vector.prototype.scale = function(scale) {
	this.x *= scale;
	this.y *= scale;
}

02 親クラスに定めたオブジェクト複製のメソッドを子クラスが継承する

ここで、ベクトル演算の子クラス(Vector)に新たなメソッドを加えて、ふたつのベクトルの距離が返されるようにしましょう。計算は簡単です。ふたつのベクトルの差を求めれば、その長さが距離になります。ベクトルの差は、加算のメソッド(add())に渡す座標値を正負逆にすれば得られます。ベクトルの長さは親クラスのゲッター(length)で調べればよいでしょう。

ただし、加算のメソッドはそのオブジェクトのxy座標を変えてしまいます。ふたつのベクトルの距離を求めたいというとき、各ベクトルの値そのものは変えたくないことが多いでしょう。すると、オブジェクトを複製するメソッドが求められます。もちろん、ベクトルのクラス(Vector)にメソッドを加えればとくに問題ありません。けれど、こうした基本的な機能は親クラス(Point)に定めて、子クラスが継承したいこともあります。普通なら、オブジェクト複製のメソッドはつぎのように書き加えるでしょう。


Point.prototype.clone = function() {
	var clone = new Point(this.x, this.y);
	return clone;
};

子クラス(Vector)は、もちろんこの複製のメソッド(clone())を継承します。けれども、返されるのは親クラス(Point)のインスタンスです。したがって、つぎのように子クラスのメソッドが使えなくなってしまいます。


var obj = new Vector(1, Math.sqrt(3));
var clone = obj.clone();
console.log(clone.length, clone.angle * Point.RAD_TO_DEG);
// 1.9999999999999998 59.99999999999999
console.log(clone.add);
// undefined
console.log(clone);
// Point

オブジェクトのコンストラクタ関数はconstructorプロパティで得られます。そして、このプロパティの参照は、プロトタイプオブジェクトのObject.prototype.constructorプロパティから継承します。そこで、親クラス(Point)のオブジェクトを複製するメソッドはつぎのように定めます。


Point.prototype.clone = function() {
	// var clone = new Point(this.x, this.y);
	var clone = new this.constructor(this.x, this.y);
	return clone;
};

すると、ベクトルのクラス(Vector)に、ふたつのベクトルの距離を求めるメソッド(getDistance())がつぎのように加えられます。


Vector.prototype.getDistance = function(vector) {
	var point = this.clone();
	point.add(-vector.x, -vector.y);
	return point.length;
}

このメソッドの中で差を求めるオブジェクトは複製しましたので、もとのふたつのベクトルの座標値は演算後もつぎのように変わりません。座標を扱う親クラス(Point)とベクトル演算を定めた子クラス(Vector)の定義は、以下のコード003にまとめました[*001]


var obj1 = new Vector(1);
var obj2 = new Vector(0, Math.sqrt(3));
console.log(obj1.getDistance(obj2));
// 1.9999999999999998
console.log(obj1.x, obj1.y, obj2.x, obj2.y);
// 1 0 0 1.7320508075688772

コード003■座標を扱う親クラスとベクトル演算を定めた子クラス

Pointクラス

function Point(x, y) {
	var _x = 0;
	var _y = 0;
	Object.defineProperties(this, {
		x: {
			get: function() {
				return _x;
			},
			set: function(x) {
				_x = isNaN(x) ? _x : x;
			}
		},
		y: {
			get: function() {
				return _y;
			},
			set: function(y) {
				_y = isNaN(y) ? _y : y;
			}
		}
	});
	this.x = x;
	this.y = y;
}
Object.defineProperties(Point.prototype, {
	length: {
		get: function() {
			var square = this.x * this.x + this.y * this.y;
			return Math.sqrt(square);
		}
	},
	angle: {
		get: function() {
			return Math.atan2(this.y, this.x);
		}
	}
});
Point.prototype.clone = function() {
	var clone = new this.constructor(this.x, this.y);
	return clone;
};
Object.defineProperty(Point, "RAD_TO_DEG", {
	value: 180 / Math.PI
});

Vectorクラス

function Vector(x, y) {
	Point.call(this, x, y)
}
Vector.prototype = Object.create(Point.prototype);
Vector.prototype.constructor = Vector;
Vector.prototype.add = function(x, y) {
	this.x += x;
	this.y += y;
};
Vector.prototype.scale = function(scale) {
	this.x *= scale;
	this.y *= scale;
}
Vector.prototype.getDistance = function(vector) {
	var clone = this.clone();
	clone.add(-vector.x, -vector.y);
	return clone.length;
}

[*001] 子クラス(Vector)には、プロトタイプオブジェクト(Object.prototypeプロパティ)にObject.create()メソッドでつくった親クラス(Point)のオブジェクトを代入することにより、継承が定められています。けれど、このままでは子クラスのObject.prototype.constructorプロパティは、親クラスのコンストラクタを参照してしまいます。そのため前掲コード003の子クラスは、このプロパティに改めて自らのコンストラクタ関数を定めました。


作成者: 野中文雄
作成日: 2016年7月2日


Copyright © 2001-2016 Fumio Nonaka.  All rights reserved.