[JavaScript] getter/setterも使えるエコ楽なクラス定義 - もちろん継承も - private変数も

  • 43
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

getter/setterも使えるエコ楽なクラス定義 - もちろん継承も - private変数も

ゴール:

  • getter/setterを含むクラス定義をエコ楽に記述できる(get/set)
  • クラスの継承もエコ楽に正しく記述できる(extend)
  • とにかく長いキーワードや余計なものはプログラマには書かせない
    • constructorの代わりに、newを使える様にする
    • prototype__proto__もプログラマには見せない書かせない
    • Object.definePropertyObject.setPrototypeOfなども見せない書かせない
  • ついでに外部からアクセスできないprivateな変数も使える様にする

base-class.jpg

※npmにbase-class-extendとして登録しました。
※参考: [JavaScript] そんな継承はイヤだ - クラス定義 - オブジェクト作成

この記事のゴールは、以下の様にクラス定義がエコ楽にできること。

animal-ex.js
  // 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より

vector-ex.js
  // 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
  // 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
  // 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

じゃ、簡単に以下の様に定義してみる。

new-class-ex.js
  // 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.constructorenumerableだ。後で直すよ)

今までのObject.definePropertyを使った作り方だと、

obj-def-prop-ex.js
  // 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
  // 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
  // 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
  // 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さんのコード
constructorinit
prototype以外にstaticPropsあり
DefineClass(proto, staticProps);
DefineClass(BaseClass, proto, staticProps);

node.class (npm)

John Resigさんのコード
constructorinit
Class.injectという機能がある(mixin)

class.extend (npm)

John Resigさんのコード
constructorinit
prototypeのみ, prototypeの属性をコピー

extend.class (npm)

John Resigさんのコード
constructorinit
prototypeのみ, prototypeの属性をコピー

node-base-class (npm)

John Resigさんのコード
constructorinit
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