サイトトップ

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

HTML5テクニカルノート

JavaScript: クラスにゲッター/セッターでプロパティを定める


ゲッターやセッターを使うと、値の取得あるいは設定をメソッドとして定めつつ、プロパティのように扱うことができます。JavaScriptには、ゲッターとセッターの加え方がいくつかあります。それらを簡単なクラスのサンプルでご説明します。

01 オブジェクトにゲッターを定めてプロトタイプオブジェクトに代入する

ゲッターやセッターを定める前のサンプルのクラスが以下のコード001です。このクラス(Point)はxy座標値をプロパティにもち、ふたつのメソッド(getLength()とgetAngle())でそれぞれ原点からの距離およびx軸正方向となす角度を返します[*001]。つぎのテスト用のJavaScriptコードは、座標値の引数に(1, √3)を与えてオブジェクト(obj)をつくっています。この場合、距離は2、角度は60度になります。なお、角度はラジアン値が返されるので、変換比率(180/π)を掛け合わせて度数に直しました。


var obj = new Point(1, Math.sqrt(3));
console.log(obj.getLength(), obj.getAngle() * 180 / Math.PI);
// 1.9999999999999998 59.99999999999999
console.log(obj);
// Point

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


function Point(x, y) {
	this.x = isNaN(x) ? 0 : x;
	this.y = isNaN(y) ? 0 : y;
}
Point.prototype.getLength = function() {
		var square = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
};
Point.prototype.getAngle = function() {
		return Math.atan2(this.y, this.x);
};

ここで、ふたつのメソッドをプロパティのかたちで扱えるように、ゲッターとして定めましょう。このとき使うのがget構文で、オブジェクトの中につぎのように記述します。関数本体は、かならず値を返さなければなりません。ゲッターが複数ある場合には、カンマ区切りで書き加えます。

{get プロパティ() {/* 値を返す関数本体の処理 */}}

ゲッターをクラスのメソッドとして定めるには、つぎのようにプロトタイプオブジェクト(prototypeプロパティ)にオブジェクトを代入します。


/*
Point.prototype.getLength = function() {
		var square = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
};
Point.prototype.getAngle = function() {
		return Math.atan2(this.y, this.x);
};
*/
Point.prototype = {
	get length() {
		var square = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	},
	get angle() {
		return Math.atan2(this.y, this.x);
	}
};

ただし、気をつけなければならないのは、プロトタイプオブジェクトをオブジェクトで上書きしたことです。そのため、オブジェクトをconsole.log()メソッドで調べたとき、コンストラクタとしてObjectが示されます。これによって具体的な問題が起こることは多くはありません(「JavaScript: オブジェクト複製のメソッドを継承する ー Object.prototype.constructorプロパティ」注[*001]参照)。けれども、できれば直したいところです。


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

オブジェクトのコンストラクタ関数は、プロトタイプオブジェクトのObject.prototype.constructorプロパティから継承します。そこで、つぎにまとめたコード002では、このプロパティに改めてコンストラクタ関数(Point)を与えました。この設定は、プロトタイプオブジェクトにゲッターのオブジェクトを代入した後で行うことに注意してください。

コード002■クラスにゲッターでプロパティを定める


function Point(x, y) {
	this.x = isNaN(x) ? 0 : x;
	this.y = isNaN(y) ? 0 : y;
}
Point.prototype = {
	get length() {
		var square = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	},
	get angle() {
		return Math.atan2(this.y, this.x);
	}
};
Point.prototype.constructor = Point;

[*001] 原点から座標(x, y)までの距離は、三平方の定理√(x2 + y2)で求められます。また、Math.atan2()メソッドは、x軸正方向となす角度をラジアン値で返します(引数はy、xの順であることにご注意ください)。

02 Object.defineProperty()メソッドで書き替えできないプロパティを定める

ラジアンから度数への変換比率をクラスに定めましょう。オブジェクトごとに値が異なることのない定数ですから、クラス(Point)そのものにプロパティとして加えます。つぎのように、コンストラクタ関数に設定すれば簡単です。


Point.RAD_TO_DEG = 180 / Math.PI;

その代わり、つぎのように値もたやすく書き替えられてしまいます。


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

Object.defineProperty()メソッドを使えば、値の書き替えられないプロパティが定められます。構文はつぎのとおりです。今回の対象オブジェクトはPointで、プロパティ名は文字列で"RAD_TO_DEG"とします。ディスクリプタはプロパティの詳細を、オブジェクトの中にフィールド(プロパティ)と値で与えます。以下のコード003では、valueフィールドにプロパティ値を納めました。フィールドにはほかに、configurable(ディスクリプタ変更可能)やenumerable(列挙可能)およびwritable(書き替え可能)があり、値はブール(論理)値です。そして、いずれもデフォルト値はfalseであるため、省けば書き替えはできなくなります。

Object.defineProperty(対象オブジェクト, プロパティ名, ディスクリプタ)

以下のコード003で前掲のテスト用コードを試すと、つぎのようにプロパティ値は書き替わりません。つまり、定数を定めることができたのです。


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

コード003■クラスに書き替えのできない定数を定める


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

03 Object.defineProperties()メソッドでクラスに複数のプロパティを加える

Object.defineProperties()メソッドを使うと、オブジェクトに複数のプロパティが加えられます。ゲッターのプロパティも、つぎの構文で定めることができます。複数のプロパティは、カンマ区切りで書き加えます。ゲッターのメソッドは引数は受け取らず、かならず値を返さなければなりません。

Object.defineProperties(対象オブジェクト, {プロパティ: {get: function() {/* 値を返す関数本体の処理 */}}})

そこで、前掲コード002でプロトタイプオブジェクトに加えたゲッターのメソッドを、つぎのようにObject.defineProperties()メソッドで書き替えましょう。


/*
Point.prototype = {
	get length() {
		var square = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	},
	get angle() {
		return Math.atan2(this.y, this.x);
	}
};
*/
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);
	}}
});

この書き替えをしても、ゲッターの働きは前掲コード002と変わりません。ただし、プロトタイプオブジェクトは上書きされずに、プロパティが加えられます。したがって、Object.prototype.constructorプロパティにコンストラクタ関数を定め直さなくても済みます。


// Point.prototype.constructor = Point;

つぎのようなテスト用のコードで、ゲッターは正しく定められたことが確かめられます。また、コンストラクタの参照も変わっていません。ここまでの書き替えをまとめたのが、以下のコード004です。


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

コード004■プロトタイプオブジェクトにゲッターをメソッドで加える


function Point(x, y) {
	this.x = isNaN(x) ? 0 : x;
	this.y = isNaN(y) ? 0 : 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
});

04 ゲッターとセッターで読み書きできるプロパティを定める

ゲッターに加えてセッターを定めると、読み書きできるプロパティになります。セッターは、値がプロパティ値として適切かどうか確かめる役割も果たせます。Object.defineProperties()メソッドでは、セッターはゲッターに続けてカンマ区切りで、キーsetの値としてメソッドの関数を与えます。セッターのメソッドは引数をひとつだけ受け取り、プロパティ値として処理します。

ここで注意しなければならないのは、ゲッターとセッターの扱う値は別のプロパティに納めるということです。メソッド本体で自身のプロパティにアクセスしてしまうと、またゲッターやセッターが呼び出されて循環してしまうからです。そこで、xy座標のプロパティにはつぎのように別のプロパティ(_xと_y)を設け、Object.defineProperties()メソッドにゲッターとセッターを加えました。セッターは定められようとしている引数値を調べ、正しく数値であるときのみ設定し、そうでないときは値は変えません。


function Point(x, y) {
	this._x = 0;
	this._y = 0;
	// this.x = isNaN(x) ? 0 : x;
	// this.y = isNaN(y) ? 0 : y;
	this.x = x;
	this.y = y;
}
Object.defineProperties(Point.prototype, {
	x: {
		get: function() {
			return this._x;
		},
		set: function(x) {
			this._x = isNaN(x) ? this._x : x;
		}
	},
	y: {
		get: function() {
			return this._y;
		},
		set: function(y) {
			this._y = isNaN(y) ? this._y : y;
		}
	},

});

こうすると、コンストラクタ関数に正しい引数値が渡されなかった場合だけではなく、プロパティに数値として適切でない値が定められようとしたときも、もとの数値を保つことができます。つぎのテスト用コードの結果をご覧ください。ここまでの書き替えをまとめたのが、以下のコード005です。


var obj = new Point();
console.log(obj.x, obj.y);
// 0 0
obj.x = 1;
obj.y = Math.sqrt(3);
console.log(obj.x, obj.y);
// 1 1.7320508075688772
obj.x = 'test';
obj.y = NaN;
console.log(obj.x, obj.y);
// 1 1.7320508075688772

コード005■ゲッターとセッターでプロパティ値を正しく保つ


function Point(x, y) {
	this._x = 0;
	this._y = 0;
	this.x = x;
	this.y = y;
}
Object.defineProperties(Point.prototype, {
	x: {
		get: function() {
			return this._x;
		},
		set: function(x) {
			this._x = isNaN(x) ? this._x : x;
		}
	},
	y: {
		get: function() {
			return this._y;
		},
		set: function(y) {
			this._y = isNaN(y) ? this._y : y;
		}
	},
	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
});

05 プロパティ値をローカル変数に入れて外からのアクセスを断つ

前項04のゲッターやセッターを設けても、内部的に値をもっているプロパティには直接アクセスできてしまいます。つまり、ゲッターやセッターを介さずに、値の読み書きができるということです。たとえば、前掲コード005もつぎのような結果になってしまいます。


var obj = new Point(1, 2);
console.log(obj.x, obj.y);
// 1 2
obj._x = 'test';
obj._y = NaN;
console.log(obj.x, obj.y);
// test NaN

値をプロパティにもたせるかぎり、直接の読み書きは避けられません。けれど、ローカル変数なら、関数の外から参照できません。ただし、ゲッターとセッターについても、関数の外に定めたのでは変数が扱えないということです。したがって、ゲッターとセッターは、つぎのようにコンストラクタ関数の中に定めればよいのです。なお、通常ローカル変数は、関数の実行が終わるとメモリから消えます。けれど、オブジェクトにゲッターとセッターが加えられ、その関数本体からローカル変数を参照していると、そのメモリは保たれるのです。


function Point(x, y) {
	// this._x = 0;
	// this._y = 0;
	var _x;
	var _y;
	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, {
	/*
	x: {
		get: function() {
			return this._x;
		},
		set: function(x) {
			this._x = isNaN(x) ? this._x : x;
		}
	},
	y: {
		get: function() {
			return this._y;
		},
		set: function(y) {
			this._y = isNaN(y) ? this._y : y;
		}
	},
	*/

});

注意しなければならないのは、コンストラクタの中でオブジェクトに定めたゲッターとセッターのメソッドは、コンストラクタが生成するオブジェクトごとにつくられるということです。したがって、その分メモリは費やされます。値の保護にそこまで重きを置かない場合には、前掲コード005のやり方を採るのがよいでしょう。つぎのコード006に、書き替えた内容をまとめました。

コード006■プロパティ値をローカル変数に納めて直接のアクセスを断つ


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
});


作成者: 野中文雄
更新日: 2016年7月2日 本文に「JavaScript: オブジェクト複製のメソッドを継承する ー Object.prototype.constructorプロパティ」への参照を追加。
更新日: 2016年7月1日 コード006の誤りを修正。
作成日: 2016年6月29日


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