Extensible Meta Objects (xmo.js) est une bibliothèque de classes légère pour créer des classes et des mixins JS.
xmo.js
est une bibliothèque javascript légère pour créer des classes et des mixins JS. Contrairement à de nombreuses autres bibliothèques qui tentent de résoudre le même problème, xmo.js
a été conçu pour être extensible et fournit uniquement le mécanisme permettant de créer une nouvelle classe ou un nouveau mixin avec la possibilité d'enregistrer des extensions définies par l'utilisateur et des gestionnaires d'initialisation.
Il existe de nombreuses boîtes à outils d'interface utilisateur et frameworks JS qui fournissent une sorte de sous-système de classe qui ajoute des propriétés, des événements et d'autres fonctionnalités à la classe javascript traditionnelle. Cette approche rend le sous-système de classe dépendant du framework et le rend inutilisable en dehors de celui-ci.
L'approche utilisée par xmo.js
est différente. Il permet d'enregistrer des gestionnaires d'initialisation et des extensions dans la classe elle-même afin que toute classe qui en hérite fournisse les mêmes fonctionnalités que la classe de base. Cela signifie que de nombreux frameworks de classes existants pourraient être théoriquement implémentés au-dessus de xmo.js
lui-même.
Alors comment créer une classe JavaScript ? Importez simplement la bibliothèque xmo
(ou incluez-la si vous travaillez côté client) et utilisez xmo
comme fonction pour créer votre propre classe. La fonction n'accepte qu'un seul argument def
, qui est utilisé pour définir les membres, les propriétés statiques, les gestionnaires d'initialisation et les extensions ; il renverra un nouvel objet Class
qui pourra être instancié par un new
mot-clé. Un exemple simple est présenté ci-dessous :
// 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 ;
}
} ) ;
La propriété constructor
définit le constructeur de classe et d'autres propriétés définissent les membres de la classe. La Class
peut être instanciée simplement en utilisant l'opérateur new
, comme le montre l'exemple suivant :
// 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`.
La classe de base peut être spécifiée par la propriété $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`.
Les membres statiques (classe) peuvent être définis par la propriété $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`).
De nombreux frameworks JS, en particulier ceux conçus pour les versions d'interface utilisateur, fournissent un ensemble fixe d'extensions au modèle objet qu'ils utilisent. Par exemple, Qooxdoo prend en charge les mixins, les interfaces et permet de définir des propriétés et des événements. Le but de xmo.js
n'est pas de fournir toutes les fonctionnalités imaginables, mais de fournir une base pour ajouter des extensions à une classe particulière qui seront ensuite incluses par toutes les classes qui en héritent. Cela permet d'étendre le système de classes au moment de l'exécution et de prendre en charge pratiquement tous les besoins des utilisateurs.
Il y a pour le moment 2 concepts supportés par xmo.js
:
$preInit
et $postInit
.$extensions
.Une nouvelle classe est toujours créée en procédant comme suit :
$preInit
.$extensions
définies)$postInit
. L'exemple suivant utilise la propriété $extensions
pour définir une extension $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`.
Les extensions définissent une fonction qui est appelée si une key
donnée est présente dans la définition de classe. Puisque ce concept est bon et généralement utile, il est parfois pratique de pouvoir appeler un gestionnaire avant et/ou après la configuration de la classe, quelles que soient les propriétés présentes dans l'objet def
des définitions de classe. Les gestionnaires $preInit
et $postInit
sont pris en charge et peuvent être utilisés pour ajouter un ou plusieurs gestionnaires à la fois.
L'exemple suivant fait la même chose que l'extension $properties
, mais utilise plutôt le gestionnaire $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`.
Les gestionnaires d'initialisation sont très similaires aux extensions, cependant, ils n'ont besoin d'aucune propriété pour être définie et sont toujours appelés une fois par classe. Les gestionnaires d'initialisation sont en général plus puissants, car ils peuvent utiliser n'importe quelle propriété ou plusieurs propriétés pour ajouter des éléments à la classe elle-même.
Un mixin est un ensemble de fonctions qui peuvent être incluses dans une autre classe ou mixin. Les mixins sont définis à l'aide de xmo.mixin(def)
, où def
est une définition similaire compatible avec xmo.js
lui-même, mais sans prise en charge constructor
(les mixins ne peuvent pas être instanciés). Les mixins comprennent également les gestionnaires $extensions
et $preinit/$postInit
, il est donc possible de les définir dans le mixin qui est ensuite inclus dans d'autres classes.
// 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]`.
Combiner plusieurs mixins en un seul mixin :
// 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 ]
} ) ;
Chaque classe créée par xmo.js
contient une propriété non énumérable appelée MetaInfo. Il contient des informations essentielles sur la classe elle-même (et l'héritage) et peut être utilisé pour stocker des informations supplémentaires requises par les extensions.
Montrons les bases de 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 ) ;
//
Il est important de mentionner que tous les membres de MetaClass sont gelés (immuables) et ne peuvent pas être modifiés après la création de la classe (après l'appel de tous les gestionnaires postInit). MetaInfo ne peut être modifié que lors de la création de la classe par les gestionnaires d'initialisation et les extensions de classe. Utilisez la fonction membre getMutable()
pour rendre une propriété de MetaInfo temporairement mutable.
L'exemple suivant montre comment ajouter des informations de réflexion personnalisées à 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