Extensible Meta Objects (xmo.js) ist eine schlanke Klassenbibliothek zum Erstellen von JS-Klassen und Mixins.
xmo.js
ist eine leichte Javascript-Bibliothek zum Erstellen von JS-Klassen und Mixins. Im Gegensatz zu vielen anderen Bibliotheken, die versuchen, das gleiche Problem zu lösen, wurde xmo.js
erweiterbar konzipiert und bietet lediglich den Mechanismus zum Erstellen einer neuen Klasse oder eines neuen Mixins mit der Möglichkeit, benutzerdefinierte Erweiterungen und Init-Handler zu registrieren.
Es gibt viele UI-Toolkits und JS-Frameworks, die eine Art Klassensubsystem bereitstellen, das der traditionellen Javascript-Klasse Eigenschaften, Ereignisse und andere Funktionen hinzufügt. Dieser Ansatz macht das Klassensubsystem vom Framework abhängig und außerhalb desselben unbrauchbar.
Der von xmo.js
verwendete Ansatz ist anders. Es ermöglicht die Registrierung von Init-Handlern und Erweiterungen für die Klasse selbst, sodass jede Klasse, die sie erbt, dieselben Funktionen wie die Basisklasse bereitstellen würde. Das bedeutet, dass viele bestehende Klassen-Frameworks theoretisch auf xmo.js
selbst implementiert werden könnten.
Wie erstellt man also eine JavaScript-Klasse? Importieren Sie einfach xmo
Bibliothek (oder fügen Sie sie ein, wenn Sie clientseitig arbeiten) und verwenden Sie xmo
als Funktion, um Ihre eigene Klasse zu erstellen. Die Funktion akzeptiert nur ein Argument def
, das zum Definieren von Mitgliedern, statischen Eigenschaften, Init-Handlern und Erweiterungen verwendet wird; Es wird ein neues Class
zurückgegeben, das durch ein new
Schlüsselwort instanziiert werden kann. Ein einfaches Beispiel ist unten dargestellt:
// 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 ;
}
} ) ;
Die Eigenschaft constructor
definiert den Klassenkonstruktor und andere Eigenschaften definieren Klassenmitglieder. Die Class
kann einfach durch die Verwendung new
Operators instanziiert werden, wie im folgenden Beispiel gezeigt:
// 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`.
Die Basisklasse kann durch die Eigenschaft $extend
angegeben werden:
// `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`.
Statische (Klassen-)Mitglieder können durch die Eigenschaft $statics
definiert werden.
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`).
Viele JS-Frameworks, insbesondere solche, die für UI-Takeits entwickelt wurden, bieten einen festen Satz von Erweiterungen für das von ihnen verwendete Objektmodell. Qooxdoo unterstützt beispielsweise Mixins und Schnittstellen und ermöglicht die Definition von Eigenschaften und Ereignissen. Der Zweck von xmo.js
besteht nicht darin, alle erdenklichen Funktionen bereitzustellen, sondern eine Grundlage zum Hinzufügen von Erweiterungen zu einer bestimmten Klasse bereitzustellen, die dann von allen Klassen, die sie erben, einbezogen werden. Dies ermöglicht es, das Klassensystem zur Laufzeit zu erweitern und praktisch alle Benutzeranforderungen zu unterstützen.
Derzeit werden zwei Konzepte von xmo.js
unterstützt:
$preInit
und $postInit
definiert werden.$extensions
definierte Erweiterungen.Eine neue Klasse wird immer durch die folgenden Schritte erstellt:
$preInit
Handler auf.$extensions
)$postInit
Handler auf. Im folgenden Beispiel wird die Eigenschaft $extensions
verwendet, um eine $property
Erweiterung zu definieren:
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`.
Erweiterungen definieren eine Funktion, die aufgerufen wird, wenn ein bestimmter key
in der Klassendefinition vorhanden ist. Da dieses Konzept in Ordnung und im Allgemeinen nützlich ist, ist es manchmal praktisch, einen Handler vor und/oder nach dem Einrichten der Klasse aufrufen zu können, unabhängig davon, welche Eigenschaften in den Klassendefinitionen def
Objekts vorhanden sind. Sowohl die Handler $preInit
als auch $postInit
werden unterstützt und können zum gleichzeitigen Hinzufügen eines einzelnen oder mehrerer Handler verwendet werden.
Das folgende Beispiel macht dasselbe wie die Erweiterung $properties
, verwendet jedoch stattdessen den Handler $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-Handler sind Erweiterungen sehr ähnlich, benötigen jedoch keine definierten Eigenschaften und werden immer einmal pro Klasse aufgerufen. Init-Handler sind im Allgemeinen leistungsfähiger, da sie jede oder mehrere Eigenschaften verwenden können, um der Klasse selbst Dinge hinzuzufügen.
Ein Mixin ist eine Reihe von Funktionen, die in eine andere Klasse oder ein anderes Mixin eingebunden werden können. Mixins werden mithilfe von xmo.mixin(def)
definiert, wobei def
ähnlich definitionkompatibel zu xmo.js
selbst ist, jedoch ohne constructor
(Mixins können nicht instanziiert werden). Mixins verstehen auch $extensions
und $preinit/$postInit
Handler, daher ist es möglich, diese in Mixin zu definieren, das dann in andere Klassen eingebunden wird.
// 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]`.
Mehrere Mixins zu einem einzigen Mixin kombinieren:
// 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 ]
} ) ;
Jede von xmo.js
erstellte Klasse enthält eine nicht aufzählbare Eigenschaft namens MetaInfo. Es enthält wesentliche Informationen über die Klasse selbst (und die Vererbung) und kann zum Speichern zusätzlicher Informationen verwendet werden, die für Erweiterungen erforderlich sind.
Lassen Sie uns die Grundlagen von MetaInfo demonstrieren:
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 ) ;
//
Es ist wichtig zu erwähnen, dass alle Mitglieder von MetaClass eingefroren (unveränderlich) sind und nach der Erstellung der Klasse (nachdem alle postInit-Handler aufgerufen wurden) nicht mehr geändert werden können. MetaInfo kann während der Klassenerstellung nur durch Init-Handler und Klassenerweiterungen geändert werden. Verwenden Sie die Memberfunktion getMutable()
um eine Eigenschaft von MetaInfo vorübergehend veränderbar zu machen.
Das folgende Beispiel zeigt, wie benutzerdefinierte Reflexionsinformationen zur MetaInfo hinzugefügt werden:
// 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