getter/setterも使えるエコ楽なクラス定義 - もちろん継承も - private変数も
ゴール:
-
getter/setter
を含むクラス定義をエコ楽に記述できる(get
/set
) - クラスの継承もエコ楽に正しく記述できる(
extend
) - とにかく長いキーワードや余計なものはプログラマには書かせない
-
constructor
の代わりに、new
を使える様にする -
prototype
も__proto__
もプログラマには見せない書かせない -
Object.defineProperty
やObject.setPrototypeOf
なども見せない書かせない
-
- ついでに外部からアクセスできない
private
な変数も使える様にする
※npmにbase-class-extendとして登録しました。
※参考: [JavaScript] そんな継承はイヤだ - クラス定義 - オブジェクト作成
この記事のゴールは、以下の様にクラス定義がエコ楽にできること。
// animal-ex.js
'use strict';
var BaseClass = require('base-class-extend');
var Animal = BaseClass.extend({
new: function Animal(name) {
this.name = name;
},
get name() { return this._name; },
set name(name) { this._name = name; },
introduce: function () {
console.log('私の名前は' + this.name + 'です。' +
'私は' + this.constructor.name + 'です。');
},
});
var a1 = new Animal('Annie');
a1.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
var Bear = Animal.extend('Bear');
var b1 = new Bear('Pooh');
b1.introduce(); // -> 私の名前はPoohです。私はBearです。
[JavaScriptのObject.definePropertyを使ってみよう - SONICMOOV LAB]
(http://lab.sonicmoov.com/development/javascript-object-defineproperty/)より
// vector-ex.js
'use strict';
var BaseClass = require('base-class-extend');
// JavaScriptのObject.definePropertyを使ってみよう - SONICMOOV LAB
// http://lab.sonicmoov.com/development/javascript-object-defineproperty/
// 上記を参照してサンプルを作ってみました。
var Vector2D = BaseClass.extend({
new: function Vector2D(x, y) {
this._length = 0;
this._changed = true;
this._x = x;
this._y = y;
},
get x() { return this._x; },
set x(x) { this._x = x; this._changed = true; },
get y() { return this._y; },
set y(y) { this._y = y; this._changed = true; },
get length() {
if (this._changed) {
this._length = Math.sqrt(this._x * this._x + this._y * this._y);
this._changed = false;
}
return this._length;
},
set: function (x, y) { this._x = x; this._y = y; this._changed = true; },
});
var v2 = new Vector2D(3, 4);
console.log('V2D(3, 4):', v2.length);
v2.set(1, 2);
console.log('V2D(1, 2):', v2.length);
v2.set(1, 1);
console.log('V2D(1, 1):', v2.length);
var Vector3D = Vector2D.extend({
new: function Vector3D(x, y, z) {
Vector2D.call(this, x, y);
this._z = z;
},
get length() {
if (this._changed) {
this._length = Math.sqrt(this._x * this._x + this._y * this._y + this._z * this._z);
this._changed = false;
}
return this._length;
},
set: function (x, y, z) { this._x = x; this._y = y; this._z = z; this._changed = true; },
});
var v3 = new Vector3D(3, 4, 5);
console.log('V3D(3, 4, 5):', v3.length);
getter/setterのあるオブジェクト
getter/setterのあるオブジェクトとは、いったいどんなモノかというと、
get
とかset
を使ってアクセサを定義するオブジェクトの事です。
// obj-get-set-ex.js
'use strict';
var obj1 = { _name: 'My Name',
get name() { return this._name; },
set name(name) { this._name = name },
};
console.log(obj1.name); // -> My Name
obj1.name = 'New Name';
console.log(obj1.name); // -> New Name
これ、使い勝手は良さそうだけど、たくさん使うと効率は悪そうですよね。
やっぱりnew Class()
って感じで作りたいし。
Object.defineProperty
を使うとできるんだけどキーワード多過ぎだし。
関数の名前を取得 Function.name
Object.name
とかFunction.name
など関数の名前が表示できると便利なので、
FirefoxやChrome以外では、以下のおまじないを実行します。特にIEね。
// function-name.js
'use strict';
var fnameRegExp = /^\s*function\s*\**\s*([^\(\s]*)[\S\s]+$/im;
// fname: get function name
function fname() {
return ('' + this).replace(fnameRegExp, '$1');
}
// Function.prototype.name
if (!Function.prototype.hasOwnProperty('name')) {
if (Object.defineProperty)
Object.defineProperty(Function.prototype, 'name', {get: fname});
else if (Object.prototype.__defineGetter__)
Function.prototype.__defineGetter__('name', fname);
}
newClass
という関数を作ってみよう。
とりあえず継承とか考えずに、getter/setterを簡単に記述できるnewClass
関数を考えてみよう。
JavaScriptで正しくクラスを定義するにはコンストラクタ関数のprototype
属性に、プロトタイプオブジェクトを置いて、クラス共通のインスタンスメソッドを並べれば良いよね。そしてそのプロトタイプオブジェクトのconstructor
属性からコンストラクタ関数を指せばいいよね。ループしてるんだね。
正しいクラス定義の方法は以下のリンクを参考にしてください。
[[JavaScript] そんな継承はイヤだ - クラス定義 - オブジェクト作成 - Qiita]
(http://qiita.com/LightSpeedC/items/d307d809ecf2710bd957)
じゃ、簡単に以下の様に定義してみる。
// new-class-ex.js
'use strict';
function newClass(proto) {
var ctor = proto.constructor = proto.new;
ctor.prototype = proto;
return ctor;
}
var Animal = newClass({
new: function Animal(name) {
this.name = name;
},
get name() { return this._name; },
set name(name) { this._name = name; },
introduce: function () {
console.log('私の名前は' + this.name + 'です。' +
'私は' + this.constructor.name + 'です。');
},
});
var a1 = new Animal('Annie');
a1.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
newClass
は3行でできた。
たった3行だけど、実はこれで8割完成です。
(注: 実はprototype.constructor
がenumerable
だ。後で直すよ)
今までのObject.defineProperty
を使った作り方だと、
// obj-def-prop-ex.js
'use strict';
function Animal(name) {
this.name = name;
}
Animal.prototype.introduce = function () {
console.log('私の名前は' + this.name + 'です。' +
'私は' + this.constructor.name + 'です。');
};
Object.defineProperty(Animal.prototype, 'name', {
get: function () { return this._name; },
set: function (name) { this._name = name; },
});
var a1 = new Animal('Annie');
a1.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
どう見てもわかりにくい。
読者を煙に巻いてるとしか思えない。
余計なキーワードが多過ぎるんだね。
prototype
とか、
Object.defineProperty
とか、
get: function () {}
とか、
非常にわかりにくい。
newClass
の改良版として、継承機能を追加したextend
を作ってみよう。
継承するためのスーパークラスを指す引数がいるけど、クラスメソッドにすることで引数じゃなくてthis
でクラスを指す事にすると良いかな。
// extend1-ex.js
'use strict';
function extend(proto) {
var ctor = proto.constructor = proto.new;
ctor.prototype = proto;
proto.__proto__ = this.prototype; // inherits
return ctor;
}
function BaseClass() {}
BaseClass.extend = extend;
var Animal = BaseClass.extend({
new: function Animal(name) {
this.name = name;
},
get name() { return this._name; },
set name(name) { this._name = name; },
introduce: function () {
console.log('私の名前は' + this.name + 'です。' +
'私は' + this.constructor.name + 'です。');
},
});
var a1 = new Animal('Annie');
a1.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
Animal.extend = extend; // これはジャマ(後でネ)
var Bear = Animal.extend({
new: function Bear() {
Animal.apply(this, arguments);
}});
var b1 = new Bear('Pooh');
b1.introduce(); // -> 私の名前はPoohです。私はBearです。
extend
関数は4行でできました。
たった4行だけど、もう9割完成です。
(非標準の__proto__
が出てきた。後で直すよ)
欠点が見えてきた。
- クラスメソッドなので、いきなり
extend
は呼べない - 簡単に継承したいのに
Animal.extend
等をいちいち定義しないといけない - 他にもクラスメソッドを追加したい事もある
- たまにはコンストラクタも省略できる様にしたい
クラスメソッドの継承も拡張しちゃえ
本格的に使える様にしていこう。
-
extend
関数をいちいちクラスメソッドに定義しないといけないのは面倒だ。
そのままextend()
で呼び出したらObject
クラスからの継承にしよう。 - クラスメソッド群を2番目の引数に追加しよう。
- クラスメソッドも継承できる様に、コンストラクタ関数のプロトタイプをいじって継承できるようにしちゃうか。
プロパティ群をコピーするのが面倒だからそのままプロトタイプ・チェインさせるか。 -
util.inherits
との互換性も考えて、コンストラクタのsuper_
属性も定義しちゃうか。
// extend2-ex.js
'use strict';
function extend(proto, classProps) {
var superCtor = this;
if (typeof superCtor !== 'function')
superCtor = Object;
var ctor = proto.constructor = proto.new;
ctor.prototype = proto;
// inherits
proto.__proto__ = superCtor.prototype;
// inherits class methods
ctor.__proto__ = superCtor;
if (classProps) {
ctor.__proto__ = classProps;
classProps.__proto__ = superCtor;
}
ctor.super_ = superCtor;
return ctor;
}
var BaseClass = extend(
{new: function BaseClass() {}},
{extend: extend});
var Animal = BaseClass.extend({
new: function Animal(name) {
this.name = name;
},
get name() { return this._name; },
set name(name) { this._name = name; },
introduce: function () {
console.log('私の名前は' + this.name + 'です。' +
'私は' + this.constructor.name + 'です。');
},
});
var a1 = new Animal('Annie');
a1.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
var Bear = Animal.extend({
new: function Bear() {
Bear.super_.apply(this, arguments);
}});
var b1 = new Bear('Pooh');
b1.introduce(); // -> 私の名前はPoohです。私はBearです。
ほぼ完成に近いよ。
まだnew
というかconstructor
が省略できないな。
new
またはconstructor
が省略できるバージョン
-
new
またはconstructor
を省略すると、コンストラクタ関数が無いので自動で作成するよ。
その関数名(クラス名)は最初のオプショナルな引数で文字列で指定できる様にした。 - ついでに
new
というRuby風のクラスメソッドも定義してみるか。
これがあると継承する時にapply
で可変引数のままnew
できるよ。
こういうことをやりだすとゴチャゴチャしてくるね。
もう深く追う必要はないよね。
// extend3-ex.js
'use strict';
function extend(name, proto, classProps) {
if (typeof name !== 'string') {
classProps = proto;
proto = name;
name = '$NoName$';
}
var superCtor = this;
if (typeof superCtor !== 'function')
superCtor = Object;
if (!proto) proto = {};
var ctor = proto.hasOwnProperty('new') && proto.new ||
proto.hasOwnProperty('constructor') && proto.constructor ||
Function('proto, superCtor, new_',
'return function ' + name + '() {\n' +
' if (!(this instanceof proto.constructor)) \n' +
' return new_.apply(proto.constructor, arguments) \n' +
' superCtor.apply(this, arguments); }')
(proto, superCtor, new_);
if (typeof ctor !== 'function')
throw new TypeError('constructor must be a function');
if (!ctor.name) {
ctor = Function('proto, ctor, new_',
'return function ' + name + '() {\n' +
' if (!(this instanceof proto.constructor)) \n' +
' return new_.apply(proto.constructor, arguments) \n' +
' ctor.apply(this, arguments); }')
(proto, ctor, new_);
}
proto.constructor = ctor;
proto.new = ctor;
ctor.prototype = proto;
// inherits
proto.__proto__ = superCtor.prototype;
// inherits class methods
ctor.__proto__ = superCtor;
if (classProps) {
ctor.__proto__ = classProps;
classProps.__proto__ = superCtor;
}
ctor.super_ = superCtor;
return ctor;
}
function new_() {
var obj = Object.create(this.prototype);
return this.apply(obj, arguments), obj;
}
var BaseClass = extend(
{new: function BaseClass() {}},
{extend: extend, new: new_});
var Animal = BaseClass.extend({
new: function Animal(name) {
if (!(this instanceof Animal))
return Animal.new.apply(Animal, arguments);
this.name = name;
},
get name() { return this._name; },
set name(name) { this._name = name; },
introduce: function () {
console.log('私の名前は' + this.name + 'です。' +
'私は' + this.constructor.name + 'です。');
},
});
var a1 = new Animal('Annie');
a1.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
var a2 = Animal('Annie');
a2.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
var a3 = Animal.new('Annie');
a3.introduce(); // -> 私の名前はAnnieです。私はAnimalです。
var Bear = Animal.extend({
new: function Bear(name) {
if (!(this instanceof Bear))
return Bear.new.apply(Bear, arguments);
Bear.super_.apply(this, arguments);
}});
var b1 = new Bear('Pooh');
b1.introduce(); // -> 私の名前はPoohです。私はBearです。
var b2 = Bear('Pooh');
b2.introduce(); // -> 私の名前はPoohです。私はBearです。
var b3 = Bear.new('Pooh');
b3.introduce(); // -> 私の名前はPoohです。私はBearです。
var Cat = Animal.extend();
var c1 = new Cat('Kitty');
c1.introduce(); // -> 私の名前はKittyです。私は$NoName$です。
var c2 = Cat('Kitty');
c2.introduce(); // -> 私の名前はKittyです。私は$NoName$です。
var c3 = Cat.new('Kitty');
c3.introduce(); // -> 私の名前はKittyです。私は$NoName$です。
var Dog = Animal.extend('Dog');
var d1 = new Dog('Hachi');
d1.introduce(); // -> 私の名前はHachiです。私はDogです。
var d2 = Dog('Hachi');
d2.introduce(); // -> 私の名前はHachiです。私はDogです。
var d3 = Dog.new('Hachi');
d3.introduce(); // -> 私の名前はHachiです。私はDogです。
var Elephant = Animal.extend('Elephant', {
new: function () {
if (!(this instanceof Elephant))
return Elephant.new.apply(Elephant, arguments);
Elephant.super_.apply(this, arguments);
}});
var e1 = new Elephant('Dumbo');
e1.introduce(); // -> 私の名前はDumboです。私はElephantです。
var e2 = Elephant('Dumbo');
e2.introduce(); // -> 私の名前はDumboです。私はElephantです。
var e3 = Elephant.new('Dumbo');
e3.introduce(); // -> 私の名前はDumboです。私はElephantです。
npm に base-class-extend を登録した
というわけで、npm に登録した。
https://www.npmjs.org/package/base-class-extend
似たようなのがたくさんあるけど、他よりちょっと便利だと思う。
base-class-extend の使い方
BaseClass
にはextend
メソッドとnew
メソッドがあります。
インスタンスメソッドにprivate
メソッドがあります。
いろいろなサンプルを以下に示します。
var BaseClass = require('base-class-extend');
// 何もしないクラス
var MeanLessClass1 = BaseClass.extend();
var MeanLessClass2 = BaseClass.extend('MeanLessClass2');
var m11 = new MeanLessClass1();
var m12 = MeanLessClass1(); // new無し
var m13 = MeanLessClass1.new(); // newクラスメソッド
// コンストラクタ関数に関数名がある時
var NewClass1 = BaseClass.extend({
new: function NewClass1() {},
});
// コンストラクタ関数が匿名関数の時は文字列で指定
var NewClass2 = BaseClass.extend('NewClass2', {
new: function () {},
});
// getter/setter, methodなど
var NewClass3 = BaseClass.extend({
new: function NewClass3() { this.prop1 = 123; }, // setter経由
method1: function () { return this.prop1; }, // getter経由
get prop1() { return this._prop1; },
set prop1(val) { this._prop1 = val; },
});
// classのgetter/setterなど(thisはコンストラクタ関数)
var NewClass4 = BaseClass({
new: function NewClass4() { this.prop1 = 123; },
method1: function () { return this.prop1; },
}, {
init: function () { this.classProp1 = 123; }, // 初期化1
initialize: function () { this.classProp2 = 123; }, // 初期化2
classMethod1: function () { return this.classProp2; },
get classProp2() { return this._classProp2; },
set classProp2(val) { this._classProp2 = val; },
});
// 継承の時のクラスメソッド
var NewClass5 = NewCLass4.extend('NewClass5', {}, {
init: function () { this.classProp1 = 123; },
initialize: function () { this.classProp2 = 123; },
classMethod1: function() {
return NewClass5.super_.classMethod1() + this.classProp1;
}
});
// 継承の例
var SuperClass = BaseClass.extend({
new: function SuperClass(x, y) {
if (!(this instanceof SuperClass))
return new SuperClass(x, y); // 引数がはっきりしている場合
this.x = x;
this.y = y;
},
});
// 継承して、追加の属性がある時
var SubClass1 = SuperClass.extend({
new: function SubClass1(x, y, z) {
if (!(this instanceof SubClass1))
return SubClass1.new.apply(SubClass1, arguments);
SubClass1.super_.apply(this, arguments);
this.z = z;
},
});
// 継承して、追加の属性がない時
var SubClass2 = SuperClass.extend({
new: function SubClass2(x, y, z) {
if (!(this instanceof SubClass2))
return SubClass2.new.apply(SubClass2, arguments);
SubClass2.super_.apply(this, arguments);
},
});
// new無しでコンストラクタを呼んだらエラーとする
var SubClass3 = SuperClass.extend({
new: function SubClass3(x, y, z) {
if (!(this instanceof SubClass3))
throw new TypeError('Constructor SubClass3 requires new');
SuperClass.apply(this, arguments);
},
});
// 継承するだけ
var SubClass4 = SuperClass.extend('SubClass4');
// private変数の例
var PrivateClass1 = BaseClass.extend({
new: function PrivateClass1(val) {
if (!(this instanceof PrivateClass1))
return new PrivateClass1(val);
var private1 = val;
this.private({
showPrivate: function showPrivate() {
console.log(private1); },
get private2() { return private1; },
set private2(val) { private1 = val; },
});
}
});
※わざとnew
とかprivate
という予約語をメソッド名にしています。賛否両論ありそう。
おしまい。
npmモジュール
良さそうなやつから記述します。
簡単に記述できるやつ
base-class-extend (npm)
SubClass = BaseClass.extend(prototype, classProps);
SubClass = BaseClass.extend('name', prototype, classProps);
prototype以外にclassPropsあり
Classメソッドの継承あり(コピーしないので動的)
getter/setterサポート
constructor
の代わりにnew
も使える(constructor
のままでも良い)
Class.new(...)
はnew Class(...)
と同じ意味
this.private({})
でprivate
な変数にもアクセスできるメソッドが定義できる
※この記事を書きながらリリースしました。
js-class (npm)
使い方が私のパッケージと違うけどかなりイケてる
Class(BaseClass, prototype, options);
getter/setterサポート
constructor
のまま
options.implements: [EventEmitter, Clearable] // mixin
options.statics: staticProps // getter/setterもOK
Class.is(object).typeOf(Type)
Class.is(object).a(Type)
Class.is(object).an(Object)
John Resigさんのコード
Blog: ejohn.org/blog/simple-javascript-inheritance
John Resigさんのコード
constructor
は指定できないが初期化コードはinit
に記述する
prototypeのみ, prototypeの属性をコピー
this._super
の実装が肝だけどちょっとやばい(例外発生時など)
arguments.calleeを使ってるね→もう使っちゃダメ(Strictモードでは動かない)
以下の本に書いてあります。
JavaScript Ninjaの極意 ライブラリ開発のための知識とコーディング (Programmers’ SELECTION)
洋書: Secrets of the JavaScript Ninja
Blog: State of Secrets
define-class (npm)
John Resigさんのコード
constructor
→ init
prototype以外にstaticPropsあり
DefineClass(proto, staticProps);
DefineClass(BaseClass, proto, staticProps);
node.class (npm)
John Resigさんのコード
constructor
→ init
Class.inject
という機能がある(mixin)
class.extend (npm)
John Resigさんのコード
constructor
→ init
prototypeのみ, prototypeの属性をコピー
extend.class (npm)
John Resigさんのコード
constructor
→ init
prototypeのみ, prototypeの属性をコピー
node-base-class (npm)
John Resigさんのコード
constructor
→ init
prototypeのみ, prototypeの属性をコピー
prototypejs.org/learn/class-inheritance (prototype)
prototypeのコード
Class.create
, prototype.initialize
, Object.extend
, ...
(prototypeはjQueryより前からあるprototype拡張しまくりのライブラリ)
code.google.com/p/base2 (base2)
base2のコード
よくわからん。
Backboneのコード
class-extend (npm)
Backboneのコード
prototype以外にstaticPropsあり。
constructor
のまま
Class.__super__
== SuperClass.prototype
依存: lodash
compose-extend (npm)
Backboneのコード
Base.extend(proto, staticProps)
prototypeをコピー(.extendで)
staticPropsをコピー(.extendで)
simple-extend (npm)
Backboneのコード
prototype以外にclassPropsあり。
依存: lodash
backbone-class (npm)
Backboneのコード
依存: underscore, backbone, *
ampersand-class-extend (npm)
Backboneのコード
Class.extend(proto, proto2, proto3, ...);
mixin
依存: extend-object
baseclass (npm)
Backboneのコード
BaseClass.extend(proto, staticProps);
constructor
base-class (npm)
依存: underscore
prototypeのみ
prototypeをコピー(_.extendで)
constructor, init, defaults, ..., on/once
なんか最初からEventEmitterを継承してる
obstruct (npm)
prototypeのみ
prototypeの属性をコピー
Class (npm)
Class.create({initialize: ...
Class.create(SuperClass, {initialize: ...
underscore.extend系 - オブジェクトコピー
extend-object (npm)
1つ目の引数に2番目以降のオブジェクトの属性をコピー
exto (npm)
_.extend
exto(to, from, ...)
yiwn-extend (npm)
_.extend
extend(to, from, ...)
f-extend (npm)
extend(to, from, isDeep);
extend-shallow (npm)
extend(to, from);
util.inherits系 - 継承の支援
inherits (npm)
これぞinheritsの原点
util.inheritsと同じ
Browserでも互換性のある実装 (古いブラウザもOK)
isaacsさんのコード
modelo (npm)
util.inheritsの様なやつ
modelo.inherits(SubClass, BaseClass);
modelo.inherits(Combined, MixinOne, MixinTwo);
tea-inherits (npm)
util.inheritsの様なやつ
inherits = require('tea-inherits');
inherits(MyConstructor, EventEmitter);
複雑系
stdclass (npm)
複雑
extend
, implement
, mixin
あり
Class.neo()
→ new CLass()
class-extender (npm)
なんか複雑
cextend (npm)
なんか複雑で使いにくそう
metaphorjs-class (npm)
依存: metaphorjs-namespace
結構複雑
node-class (npm)
依存: function-enhancements, array-enhancements, object-enhancements
高機能だけど、なんか複雑そう
class (npm)
なんか複雑そう (new, class, subclass, includeとか)
nodeBase (npm)
なんか高機能過ぎるが、簡単には使えない
logging, options, defaults, EventEmitter
ee-class (npm)
new Class({inherits: BaseClass, init:...})
なんか相当複雑
obj-extend (npm)
prototypeとか出ていて例を見ても意味不明で使えない
mixin-class (npm)
中国語readme