Extensible Meta Objects (xmo.js) 是一个用于创建 JS 类和 mixin 的轻量级类库。
xmo.js
是一个轻量级的 javascript 库,用于创建 JS 类和 mixin。与许多其他试图解决同一问题的库不同, xmo.js
被设计为可扩展的,并且仅提供创建新类或 mixin 的机制,并可以注册用户定义的扩展和 init 处理程序。
有许多 UI 工具包和 JS 框架提供某种类子系统,为传统的 javascript 类添加属性、事件和其他功能。这种方法使类子系统依赖于框架,并使其在框架之外无法使用。
xmo.js
使用的方法有所不同。它允许注册初始化处理程序和类本身的扩展,因此继承它的任何类都将提供与基类相同的功能。这意味着许多现有的类框架理论上可以在xmo.js
本身之上实现。
那么如何创建一个 JavaScript 类呢?只需导入xmo
库(或者如果您在客户端工作,则包含它)并使用xmo
作为创建您自己的类的函数。该函数仅接受一个参数def
,该参数用于定义成员、静态属性、初始化处理程序和扩展;它将返回一个新的Class
对象,可以通过new
关键字实例化。一个简单的例子如下所示:
// 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 takeits 设计的框架,为它们使用的对象模型提供了一组固定的扩展。例如,Qooxdoo 支持 mixins、接口,并允许定义属性和事件。 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 处理程序通常更强大,因为它们可以使用任何属性或多个属性来向类本身添加内容。
mixin 是一组可以包含在另一个类或 mixin 中的函数。 Mixins 是通过使用xmo.mixin(def)
来定义的,其中def
是与xmo.js
本身兼容的类似定义,但没有constructor
支持(mixins 无法实例化)。 Mixins 还理解$extensions
和$preinit/$postInit
处理程序,因此可以在 mixin 中定义它们,然后将其包含在其他类中。
// 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]`.
将多个 mixin 组合成一个 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 ]
} ) ;
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 只能在类创建期间通过 init 处理程序和类扩展进行更改。使用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