Extensible Meta Objects (xmo.js) は、JS クラスとミックスインを作成するための軽量のクラス ライブラリです。
xmo.js
JS クラスとミックスインを作成するための軽量の JavaScript ライブラリです。同じ問題を解決しようとする他の多くのライブラリとは異なり、 xmo.js
は拡張可能であるように設計されており、ユーザー定義の拡張機能と init ハンドラーを登録できる新しいクラスまたはミックスインを作成するメカニズムのみを提供します。
従来の JavaScript クラスにプロパティ、イベント、その他の機能を追加する、ある種のクラス サブシステムを提供する UI ツールキットや JS フレームワークが多数あります。このアプローチでは、クラス サブシステムがフレームワークに依存するようになり、フレームワークの外では使用できなくなります。
xmo.js
で使用されるアプローチは異なります。これにより、初期化ハンドラーと拡張機能をクラス自体に登録できるため、それを継承するクラスは基本クラスと同じ機能を提供できるようになります。これは、理論的には、多くの既存のクラス フレームワークをxmo.js
自体の上に実装できることを意味します。
では、JavaScript クラスを作成するにはどうすればよいでしょうか? xmo
ライブラリをインポートし (またはクライアント側で作業する場合はそれを含め)、 xmo
関数として使用して独自のクラスを作成するだけです。この関数は引数def
1 つだけ受け入れます。この引数は、メンバー、静的プロパティ、初期化ハンドラー、および拡張機能の定義に使用されます。 new
キーワードによってインスタンス化できる新しいClass
オブジェクトが返されます。簡単な例を以下に示します。
// Create a new class `Class` that doesn't inherit from any object. It
// inherits internally from a pure JavaScript `Object`, which most JS objects
// do. A `constructor` property defines the constructor and other properties
// define class members (that will be added to the prototype object).
const Class = xmo ( {
constructor ( ) {
this . text = "Hi!"
} ,
toString ( ) {
return this . text ;
}
} ) ;
constructor
プロパティはクラス コンストラクターを定義し、他のプロパティはクラス メンバーを定義します。次の例に示すように、 new
演算子を使用するだけでClass
インスタンス化できます。
// Create an instance of `Class` defined in previous example.
const instance = new Class ( ) ;
// `instance` is now an instance of `Class`, we can check if the inheritance
// is working as expected by calling `toString` method, which should return
// "Hi!". Another expected behavior is using `instanceof` keyword which should
// return `true` if tested against `Class`.
console . log ( instance . toString ( ) ) ; // Outputs `Hi!`.
console . log ( instance instanceof Class ) ; // Outputs `true`.
console . log ( instance instanceof Object ) ; // Outputs `true`.
基本クラスは$extend
プロパティで指定できます。
// `Point` is a base class.
const Point = xmo ( {
constructor ( x , y ) {
this . x = x ;
this . y = y ;
} ,
translate ( x , y ) {
this . x += x ;
this . y += y ;
} ,
toString ( ) {
return `Point { x: ${ this . x } , y: ${ this . y } }` ;
}
} ) ;
// `Circle` extends `Point`.
const Circle = xmo ( {
$extend : Point ,
constructor ( x , y , radius ) {
// Has to call superclass constructor.
Point . call ( this , x , y ) ;
this . radius = radius ;
} ,
// Overrides `toString` of `Point`.
toString ( ) {
return `Circle { x: ${ this . x } , y: ${ this . y } , radius: ${ this . radius } }`
}
} ) ;
// Create instances of `Point` and `Circle` classes.
const point = new Point ( 1 , 1 ) ;
const circle = new Circle ( 10 , 10 , 5 ) ;
console . log ( point . toString ( ) ) ; // Outputs `Point { x: 1, y: 1 }`.
console . log ( circle . toString ( ) ) ; // Outputs `Circle { x: 10, y: 10, radius: 5 }`.
// `point` is an instance of `Point`, but not `Circle`.
console . log ( point instanceof Point ) ; // Outputs `true`.
console . log ( point instanceof Circle ) ; // Outputs `false`.
// `circle` is an instance of both `Point` and `Circle`.
console . log ( circle instanceof Point ) ; // Outputs `true`.
console . log ( circle instanceof Circle ) ; // Outputs `true`.
静的 (クラス) メンバーは$statics
プロパティで定義できます。
const Class = xmo ( {
constructor ( ) {
this . status = Class . Ready ;
} ,
$statics : {
Ready : 0 ,
Running : 1
}
} ) ;
console . log ( Class . Ready ) ; // Outputs `0`.
console . log ( Class . Running ) ; // Outputs `1`.
const instance = new Class ( ) ;
console . log ( instance . status ) ; // Outputs `0`.
console . log ( instance . Ready ) ; // Outputs `undefined` (not a member of `instance`).
多くの JS フレームワーク、特に UI テイクイット用に設計されたフレームワークは、使用するオブジェクト モデルに固定の拡張セットを提供します。たとえば、Qooxdoo はミックスイン、インターフェイスをサポートし、プロパティとイベントを定義できます。 xmo.js
の目的は、考えられるすべての機能を提供することではなく、特定のクラスに拡張機能を追加し、そのクラスを継承するすべてのクラスに拡張機能を追加するための基盤を提供することです。これにより、実行時にクラスシステムを拡張し、実質的にユーザーのニーズをすべてサポートできるようになります。
現時点でxmo.js
でサポートされている概念は 2 つあります。
$preInit
および$postInit
プロパティによって定義される初期化ハンドラー。$extensions
プロパティで定義された拡張機能。新しいクラスは常に次の手順で作成されます。
$preInit
ハンドラーを呼び出します。$extensions
も処理します)$postInit
ハンドラーを呼び出します。次の例では、 $extensions
プロパティを使用して$property
拡張機能を定義します。
const Point = xmo ( {
constructor ( x , y ) {
this . x = x ;
this . y = y ;
} ,
$extensions : {
// Define extension `$properties`.
//
// This function will be called every time when `$properties` is used in
// class definition that directly or indirectly inherits `Point`. It is
// also called if `Point` itself uses `$properties` extension.
//
// `this` - Class object (`Point` in our case).
// `k` - Property key ("$properties" string).
// `v` - Property value (`$properties` content).
$properties ( k , v ) {
// Iterate over all keys in `$properties`.
Object . keys ( v ) . forEach ( function ( name ) {
const upper = name . charAt ( 0 ) . toUpperCase ( ) + name . substr ( 1 ) ;
// Create getter and setter for a given `name`.
this . prototype [ `get ${ upper } ` ] = function ( ) { return this [ name ] ; } ;
this . prototype [ `set ${ upper } ` ] = function ( value ) { this [ name ] = value ; } ;
} , this /* binds `this` to the callback. */ ) ;
}
} ,
// In our case this will use the defined `$properties` extension.
$properties : {
x : true ,
y : true
}
} ) ;
// Create an instance of `Point` and call the generated functions.
const point = new Point ( 1 , 2 ) ;
console . log ( point . getX ( ) ) ; // Outputs `1`.
console . log ( point . getY ( ) ) ; // Outputs `2`.
point . setX ( 10 ) ;
point . setY ( 20 ) ;
console . log ( point . getX ( ) ) ; // Outputs `10`.
console . log ( point . getY ( ) ) ; // Outputs `20`.
拡張機能は、クラス定義に指定されたkey
が存在する場合に呼び出される関数を定義します。この概念は適切であり、一般に便利であるため、クラス定義のdef
オブジェクトにどのプロパティが存在するかに関係なく、クラスのセットアップの前後またはその両方でハンドラーを呼び出すことができると便利な場合があります。 $preInit
ハンドラーと$postInit
ハンドラーの両方がサポートされており、単一または複数のハンドラーを一度に追加するために使用できます。
次の例は、 $properties
拡張機能と同じことを行いますが、代わりに$postInit
ハンドラーを使用します。
const Point = xmo ( {
constructor ( x , y ) {
this . x = x ;
this . y = y ;
} ,
// Add a preInit handler.
$preInit ( def ) {
// Does nothing here, just to document the syntax.
} ,
// Add a postInit handler, called once on Point and all classes that inherit it.
//
// `this` - Class object (`Point` in our case).
// `def` - The whole `def` object passed to `xmo(...)`.
$postInit ( def ) {
if ( ! def . $properties )
return ;
// Iterate over all keys in `$properties`.
Object . keys ( def . $properties ) . forEach ( function ( name ) {
const upper = name . charAt ( 0 ) . toUpperCase ( ) + name . substr ( 1 ) ;
// Create getter and setter for a given `key`.
this . prototype [ `get ${ upper } ` ] = function ( ) { return this [ name ] ; } ;
this . prototype [ `set ${ upper } ` ] = function ( value ) { this [ name ] = value ; } ;
} , this /* binds `this` to the callback. */ ) ;
} ,
// This is not necessary. Null extensions are only used to make
// a certain property ignored (won't be copied to the prototype).
$extensions : {
$properties : null
} ,
// Will be used by the hook defined above.
$properties : {
x : true ,
y : true
}
} ) ;
// Create an instance of `Point` and use functions created by the
// `property` extension.
const point = new Point ( 1 , 2 ) ;
console . log ( point . getX ( ) ) ; // Outputs `1`.
console . log ( point . getY ( ) ) ; // Outputs `2`.
point . setX ( 10 ) ;
point . setY ( 20 ) ;
console . log ( point . getX ( ) ) ; // Outputs `10`.
console . log ( point . getY ( ) ) ; // Outputs `20`.
Init ハンドラーは拡張機能に非常に似ていますが、プロパティを定義する必要はなく、常にクラスごとに 1 回呼び出されます。 Init ハンドラーは、任意のプロパティまたは複数のプロパティを使用してクラス自体に要素を追加できるため、一般的により強力です。
ミックスインは、別のクラスまたはミックスインに含めることができる関数のセットです。ミックスインはxmo.mixin(def)
使用して定義されます。 def
xmo.js
自体と互換性のある同様の定義ですが、 constructor
サポートはありません (ミックスインはインスタンス化できません)。ミックスインは$extensions
および$preinit/$postInit
ハンドラーも理解するため、これらをミックスインで定義し、他のクラスに含めることができます。
// Create a mixin that provides `translate(x, y)` function.
const MTranslate = xmo . mixin ( {
translate ( x , y ) {
this . x += x ;
this . y += y ;
return this ;
}
} ) ;
// Create a Point class that includes MTranslate mixin.
const Point = xmo ( {
$mixins : [ MTranslate ] ,
constructor ( x , y ) {
this . x = x ;
this . y = y ;
} ,
toString ( ) {
return `[ ${ this . x } , ${ this . y } ]` ;
}
} ) ;
// Create a Rect class that includes MTranslate mixin.
const Rect = xmo ( {
$mixins : [ MTranslate ] ,
constructor ( x , y , w , h ) {
this . x = x ;
this . y = y ;
this . w = w ;
this . h = h ;
} ,
toString ( ) {
return `[ ${ this . x } , ${ this . y } , ${ this . w } , ${ this . h } ]` ;
}
} ) ;
// The translate() functions are provided to both classes.
const p = new Point ( 0 , 0 ) ;
const r = new Rect ( 0 , 0 , 33 , 67 ) ;
p . translate ( 1 , 2 ) ;
r . translate ( 1 , 2 ) ;
console . log ( p . toString ( ) ) ; // Outputs `[1, 2]`.
console . log ( r . toString ( ) ) ; // Outputs `[1, 2, 33, 67]`.
より多くのミックスインを 1 つのミックスインに結合する:
// Create two mixins MTranslate and MScale.
const MTranslate = xmo . mixin ( {
translate ( x , y ) {
this . x += x ;
this . y += y ;
return this ;
}
} ) ;
const MScale = xmo . mixin ( {
scale ( x , y ) {
if ( y == null )
y = x ;
this . x *= x ;
this . y *= y ;
return this ;
}
} ) ;
// If a combined mixin is needed, it can be created simply by
// including MTranslate and MScale into another mixin.
const MCombined = xmo . mixin ( {
$mixins : [ MTranslate , MScale ]
} ) ;
xmo.js
によって作成された各クラスには、MetaInfo と呼ばれる列挙不可能なプロパティが含まれています。これには、クラス自体 (および継承) に関する重要な情報が含まれており、拡張機能に必要な追加情報を格納するために使用できます。
MetaInfo の基本を示してみましょう。
const Class = xmo ( {
$preInit ( ) {
console . log ( "PreInit()" ) ;
} ,
$postInit ( ) {
console . log ( "PostInit()" ) ;
} ,
$extensions : {
$ignoredField : null ,
$customField ( k , v ) {
console . log ( `CustomField(): ' ${ k } ' with data ${ JSON . stringify ( v ) } ` ) ;
}
} ,
$customField : {
test : [ ]
} ,
$statics : {
SomeConst : 0
}
} ) ;
// Firstly, try to instantiate the class:
// PreInit()
// CustomField(): '$customField' with data { test: [] }
// PostInit()
const instance = new Class ( ) ;
// Access MetaInfo of the class.
// (Alternatively `instance.constructor.$metaInfo`)
const MetaInfo = Class . $metaInfo ;
// Boolean value indicating a mixin:
// false
console . log ( MetaInfo . isMixin ) ;
// Super class:
// null (would link to super class if the class was inherited)
console . log ( MetaInfo . super ) ;
// Map of ignored properties:
// { $ignoredField: true, $customField: true }
console . log ( MetaInfo . ignored ) ;
// Map of all static properties:
// { SomeConst: true }
console . log ( MetaInfo . statics ) ;
// PreInit handlers:
// [function(...)]
console . log ( MetaInfo . preInit ) ;
// PostInit handlers:
// [function(...)]
console . log ( MetaInfo . postInit ) ;
// Extensions:
// { $customField: function(...) }
console . log ( MetaInfo . extensions ) ;
//
MetaClass のすべてのメンバーはフリーズ (不変) されており、クラスの作成後 (すべての postInit ハンドラーが呼び出された後) に変更できないことに言及することが重要です。 MetaInfo は、初期化ハンドラーとクラス拡張機能によるクラスの作成中にのみ変更できます。 getMutable()
メンバー関数を使用して、MetaInfo のプロパティを一時的に変更可能にします。
次の例は、カスタム リフレクション情報を MetaInfo に追加する方法を示しています。
// Creates some base class that defines a property system.
const Base = xmo ( {
$preInit ( ) {
const meta = this . $metaInfo ;
// Add `properties` property to MetaInfo object.
if ( ! meta . properties )
meta . properties = Object . create ( null ) ;
} ,
$extensions : {
$properties ( k , v ) {
const defProperties = v ;
const metaProperties = this . $metaInfo