JavaScriptでgetterとsetterのどちらか一方のみをオーバーライドする
JavaScript でオブジェクトに accessor property を定義したとき、継承先でそのプロパティの getter, setter のどちらか一方のみを override するのは一筋縄にはいかない。
例として、長方形を表す Rectangle
と、正方形を表す Square
を定義することを考える。Square
は Rectangle
を継承する。
function Rectangle(width, height) { this.width = width; this.height = height; } Rectangle.prototype = { get width() { return this._width; }, set width(value) { this._width = +value; }, get height() { return this._height; }, set height(value) { this._height = +value; }, get area() { return this.width * this.height; } }; function Square(sideLength) { this.width = sideLength; } Square.prototype = { __proto__: Rectangle.prototype, // widthのsetterのみoverrideしたつもり set width(value) { this._width = this._height = +value; }, // heightのsetterのみoverrideしたつもり set height(value) { this._width = this._height = +value; } }; // console.log('Rectangle --------'); log(new Rectangle(10, 20)); console.log('Square -----------'); log(new Square(10)); function log(rect) { console.log('width : ' + rect.width); console.log('height : ' + rect.height); console.log('area : ' + rect.area); }
Rectangle.prototype
では、width
, height
は常に数値となるように setter で渡された値を型変換している。Square.prototype
ではそれに加えて、width
と height
が常に一致するように setter を上書きしている……?
コンソールへの出力は以下の通り。
"Rectangle --------" "width : 10" "height : 20" "area : 200" "Square -----------" "width : undefined" "height : undefined" "area : NaN"
……あれ? Square
の width
, height
を取得できていない。そのせいで area
も undefined * undefined
となり、NaN
を返している。
accessor property は getter, setter の2つで1つだ。Square.prototype
に setter のみを定義しているが getter は定義していない。これにより getter はないものとして扱われ、継承元([[Prototype]]
)に定義されている getter は隠蔽される。
コードを簡潔にするためにオブジェクト初期化子で accessor property を定義しているが、Object.defineProperty
や Object.prototype.__defineSetter__
を使おうとも結果は同じである。
accessor property ではなく accessor method として定義したとしたらどうだろう。
function Rectangle(width, height) { this.setWidth(width || 0); this.setHeight(height || 0); } Rectangle.prototype = { getWidth: function () { return this._width; }, setWidht: function (value) { this._width = +value; }, getHeight: function () { return this._height; }, setHeight: function (value) { this._height = +value; }, getArea: function () { return this.getWidth() * this.getHeight(); } }; function Square(sideLength) { this.setWidth(sideLength || 0); } Square.prototype = { __proto__: Rectangle.prototype, setWidth: function (value) { this._width = this._height = +value; }, setHeight: function (value) { this._width = this._height = +value; } }; // console.log('Rectangle --------'); log(new Rectangle(10, 20)); console.log('Square -----------'); log(new Square(10)); function log(rect) { console.log('width : ' + rect.getWidth()); console.log('height : ' + rect.getHeight()); console.log('area : ' + rect.getArea()); }
コンソールへの出力は以下の通り。
"Rectangle --------" "width : 10" "height : 20" "area : 200" "Square -----------" "width : 10" "height : 10" "area : 100"
getter と setter が別のプロパティとして定義されるため、単純に片方を上書きすればいい。
メソッド呼び出しになる点に目をつむれば、これで十分だ。実際、ES3 では accessor property を定義する方法がなかったため、こうするしかなかった。
accessor property の使いやすさと、accessor method の上書きのしやすさを両立したい。そこで、実際の処理は accessor method に定義し、accessor property はその accessor method に処理を委譲することにする。
function Rectangle(width, height) { this.width = width; this.height = height; } Rectangle.prototype = { get width() { return this.get_width(); }, get_width: function () { return this._width; }, set width(value) { this.set_width(value); }, set_width: function (value) { this._width = +value; }, get height() { return this.get_height(); }, get_height: function () { return this._height; }, set height(value) { this.set_height(value); }, set_height: function (value) { this._height = +value; }, get area() { return this.width * this.height; } }; function Square(sideLength) { this.width = sideLength; } Square.prototype = { __proto__: Rectangle.prototype, // setter method を上書きする set_width: function (value) { this._width = this._height = +value; }, // setter method を上書きする set_height: function (value) { this._width = this._height = +value; } }; // console.log('Rectangle --------'); log(new Rectangle(10, 20)); console.log('Square -----------'); log(new Square(10)); function log(rect) { console.log('width : ' + rect.width); console.log('height : ' + rect.height); console.log('area : ' + rect.area); }
コンソールへの出力は以下の通り。
"Rectangle --------" "width : 10" "height : 20" "area : 200" "Square -----------" "width : 10" "height : 10" "area : 100"
継承元の Rectangle.prototype
の定義が少し複雑になるが、出力は問題ない。
毎回委譲するだけの accessor property を書くのは面倒なので、ヘルパー関数を用意する。ついでにまだ標準化されていない __proto__
を使うのをやめ、ES5 準拠のコードに書き直す。
(function (_) { var Rectangle = this.Rectangle = new RectanglePrototype().constructor; function RectanglePrototype() { _.constructor(this, function Rectangle(width, height) { this.width = width; this.height = height; }); _.accessor(this, 'width', { get: function () { return this._width; }, set: function (value) { this._width = +value; } }); _.accessor(this, 'height', { get: function () { return this._height; }, set: function (value) { this._height = value; } }); _.accessor(this, 'area', { get: function () { return this.width * this.height; } }); }; SquarePrototype.prototype = Rectangle.prototype; var Square = this.Square = new SquarePrototype().constructor; function SquarePrototype() { _.constructor(this, function Square(sideLength) { this.width = sideLength; }); _.accessor(this, 'width', { set: setLength }); _.accessor(this, 'height', { set: setLength }); function setLength(value) { this._width = this._height = value; } } }.call(this, new function Underbar() { this.constructor = function (object, constr) { constr.prototype = object; Object.defineProperty(object, 'constructor', { value: constr, configurable: true, enumerable: false, writable: true }); }; this.accessor = function (object, name, accessor) { var getter = 'get_' + name; var setter = 'set_' + name; if (accessor.get) object[getter] = accessor.get; if (accessor.set) object[setter] = accessor.set; Object.defineProperty(object, name, { get: function () { return this[getter](); }, set: function (value) { this[setter](value); }, configurable: true, enumerable: true }); }; })); // console.log('Rectangle --------'); log(new Rectangle(10, 20)); console.log('Square -----------'); log(new Square(10)); function log(rect) { console.log('width : ' + rect.width); console.log('height : ' + rect.height); console.log('area : ' + rect.area); }
コンソールへの出力は以下の通り。
"Rectangle --------" "width : 10" "height : 20" "area : 200" "Square -----------" "width : 10" "height : 10" "area : 100"
うまくいった。
aObject.width
が存在せず、aObject.get_width
メソッドが定義されている場合、aObject.width
としたときにはその get_width
メソッドが呼び出される、みたいなシンプルな仕組みがあればなあと思う。ES6 の Proxy
を使えばできそう。