Extensible Meta Objects (xmo.js) é uma biblioteca de classes leve para criar classes JS e mixins.
xmo.js
é uma biblioteca javascript leve para criar classes e mixins JS. Ao contrário de muitas outras bibliotecas que tentam resolver o mesmo problema, xmo.js
foi projetado para ser extensível e apenas fornece o mecanismo para criar uma nova classe ou mixin com a possibilidade de registrar extensões definidas pelo usuário e manipuladores de inicialização.
Existem muitos kits de ferramentas de UI e estruturas JS que fornecem algum tipo de subsistema de classe que adiciona propriedades, eventos e outros recursos à classe javascript tradicional. Essa abordagem torna o subsistema de classe dependente da estrutura e o torna inutilizável fora dela.
A abordagem usada por xmo.js
é diferente. Ele permite registrar manipuladores de inicialização e extensões para a própria classe, para que qualquer classe que a herde forneça os mesmos recursos da classe base. Isso significa que muitas estruturas de classes existentes poderiam ser teoricamente implementadas sobre o próprio xmo.js
Então, como criar uma classe JavaScript? Basta importar a biblioteca xmo
(ou incluí-la se você trabalha no lado do cliente) e usar xmo
como uma função para criar sua própria classe. A função aceita apenas um argumento def
, que é usado para definir membros, propriedades estáticas, manipuladores de inicialização e extensões; ele retornará um novo objeto Class
que pode ser instanciado por uma new
palavra-chave. Um exemplo simples é mostrado abaixo:
// 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 ;
}
} ) ;
A propriedade constructor
define o construtor da classe e outras propriedades definem os membros da classe. A Class
pode ser instanciada simplesmente usando o operador new
, conforme demonstrado no exemplo a seguir:
// 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`.
A classe base pode ser especificada pela propriedade $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`.
Membros estáticos (classe) podem ser definidos pela propriedade $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`).
Muitas estruturas JS, especialmente aquelas projetadas para UI takeits, fornecem um conjunto fixo de extensões para o modelo de objeto que usam. Por exemplo, Qooxdoo suporta mixins, interfaces e permite definir propriedades e eventos. O objetivo do xmo.js
não é fornecer todos os recursos imagináveis, mas fornecer uma base para adicionar extensões a uma classe específica que será então incluída por todas as classes que a herdarem. Isso permite estender o sistema de classes em tempo de execução e suportar praticamente qualquer coisa que o usuário precise.
Existem no momento 2 conceitos suportados por xmo.js
:
$preInit
e $postInit
.$extensions
.Uma nova classe é sempre criada pelas seguintes etapas:
$preInit
.$extensions
definidas)$postInit
. O exemplo a seguir usa a propriedade $extensions
para definir uma extensão $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`.
As extensões definem uma função que é chamada se uma determinada key
estiver presente na definição da classe. Como esse conceito é bom e geralmente útil, às vezes é útil poder chamar um manipulador antes e/ou depois da configuração da classe, independentemente de quais propriedades estão presentes nas definições de classe do objeto def
. Os manipuladores $preInit
e $postInit
são suportados e podem ser usados para adicionar um ou vários manipuladores de uma vez.
O exemplo a seguir faz o mesmo que a extensão $properties
, mas usa o manipulador $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`.
Os manipuladores init são muito semelhantes às extensões, porém, não precisam de nenhuma propriedade para serem definidos e são sempre chamados uma vez por classe. Os manipuladores init são em geral mais poderosos, porque podem usar qualquer propriedade ou múltiplas propriedades para adicionar coisas à própria classe.
Um mixin é um conjunto de funções que podem ser incluídas em outra classe ou mixin. Os mixins são definidos usando xmo.mixin(def)
, onde def
é uma definição semelhante compatível com o próprio xmo.js
, mas sem suporte constructor
(os mixins não podem ser instanciados). Os mixins também entendem os manipuladores $extensions
e $preinit/$postInit
, portanto é possível defini-los no mixin que é então incluído em outras 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]`.
Combinando mais mixins em um único 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 ]
} ) ;
Cada classe criada por xmo.js
contém uma propriedade não enumerável chamada MetaInfo. Ele contém informações essenciais sobre a própria classe (e herança) e pode ser usado para armazenar informações adicionais exigidas por extensões.
Vamos demonstrar o básico do 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 ) ;
//
É importante mencionar que todos os membros da MetaClass são congelados (imutáveis) e não podem ser modificados após a criação da classe (após todos os manipuladores postInit terem sido chamados). MetaInfo só pode ser alterado durante a criação da classe por manipuladores init e extensões de classe. Use a função membro getMutable()
para tornar uma propriedade de MetaInfo temporariamente mutável.
O exemplo a seguir mostra como adicionar informações de reflexão personalizadas ao 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