Extensible Meta Objects (xmo.js) adalah perpustakaan kelas ringan untuk membuat kelas JS dan mixin.
xmo.js
adalah pustaka javascript ringan untuk membuat kelas dan mix JS. Tidak seperti banyak perpustakaan lain yang mencoba memecahkan masalah yang sama, xmo.js
dirancang agar dapat diperluas dan hanya menyediakan mekanisme untuk membuat kelas atau mixin baru dengan kemungkinan untuk mendaftarkan ekstensi yang ditentukan pengguna dan penangan init.
Ada banyak toolkit UI dan kerangka kerja JS yang menyediakan semacam subsistem kelas yang menambahkan properti, peristiwa, dan fitur lain ke kelas javascript tradisional. Pendekatan ini membuat subsistem kelas bergantung pada kerangka kerja dan membuatnya tidak dapat digunakan di luar kerangka tersebut.
Pendekatan yang digunakan oleh xmo.js
berbeda. Hal ini memungkinkan untuk mendaftarkan penangan init dan ekstensi ke kelas itu sendiri sehingga setiap kelas yang mewarisinya akan menyediakan fitur yang sama dengan kelas dasar. Ini berarti bahwa banyak kerangka kelas yang ada secara teoritis dapat diimplementasikan di atas xmo.js
itu sendiri.
Jadi bagaimana cara membuat Kelas JavaScript? Cukup impor perpustakaan xmo
(atau sertakan jika Anda bekerja di sisi klien) dan gunakan xmo
sebagai fungsi untuk membuat kelas Anda sendiri. Fungsi ini hanya menerima satu argumen def
, yang digunakan untuk mendefinisikan anggota, properti statis, penangan init, dan ekstensi; itu akan mengembalikan objek Class
baru yang dapat dipakai dengan kata kunci new
. Contoh sederhana ditunjukkan di bawah ini:
// 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 ;
}
} ) ;
Properti constructor
mendefinisikan konstruktor kelas dan properti lainnya menentukan anggota kelas. Class
dapat dibuat instance-nya hanya dengan menggunakan operator new
seperti yang ditunjukkan dalam contoh berikut:
// 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`.
Kelas dasar dapat ditentukan oleh properti $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`.
Anggota statis (Kelas) dapat ditentukan oleh properti $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`).
Banyak kerangka kerja JS, terutama yang dirancang untuk penggunaan UI, menyediakan serangkaian ekstensi tetap untuk model objek yang mereka gunakan. Misalnya Qooxdoo mendukung mixin, antarmuka, dan memungkinkan untuk menentukan properti dan acara. Tujuan xmo.js
bukan untuk menyediakan semua fitur yang bisa dibayangkan, tetapi untuk memberikan landasan untuk menambahkan ekstensi ke kelas tertentu yang kemudian akan disertakan oleh semua kelas yang mewarisinya. Hal ini memungkinkan untuk memperluas sistem kelas pada saat runtime dan untuk mendukung hampir semua kebutuhan pengguna.
Saat ini ada 2 konsep yang didukung oleh xmo.js
:
$preInit
dan $postInit
.$extensions
.Kelas baru selalu dibuat dengan langkah-langkah berikut:
$preInit
.$extensions
yang ditentukan juga)$postInit
. Contoh berikut menggunakan properti $extensions
untuk mendefinisikan ekstensi $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`.
Ekstensi mendefinisikan fungsi yang dipanggil jika key
tertentu ada dalam definisi kelas. Karena konsep ini baik-baik saja dan berguna secara umum, kadang-kadang berguna untuk dapat memanggil handler sebelum dan/atau setelah pengaturan kelas terlepas dari properti mana yang ada dalam objek def
definisi kelas. Penangan $preInit
dan $postInit
keduanya didukung dan dapat digunakan untuk menambahkan satu atau beberapa penangan sekaligus.
Contoh berikut melakukan hal yang sama seperti ekstensi $properties
, namun menggunakan handler $postInit
sebagai gantinya:
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`.
Penangan init sangat mirip dengan ekstensi, namun tidak memerlukan properti apa pun untuk didefinisikan dan selalu dipanggil satu kali per kelas. Penangan init secara umum lebih kuat, karena mereka dapat menggunakan properti apa pun atau beberapa properti untuk menambahkan sesuatu ke kelas itu sendiri.
Mixin adalah sekumpulan fungsi yang dapat dimasukkan ke dalam kelas atau mixin lain. Mixin didefinisikan dengan menggunakan xmo.mixin(def)
, dengan def
definisi serupa yang kompatibel dengan xmo.js
itu sendiri, tetapi tanpa dukungan constructor
(mixin tidak dapat dipakai). Mixin juga memahami $extensions
dan $preinit/$postInit
handler, sehingga memungkinkan untuk mendefinisikannya dalam mixin yang kemudian disertakan dalam kelas lain.
// 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]`.
Menggabungkan lebih banyak mixin ke dalam satu 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 ]
} ) ;
Setiap kelas yang dibuat oleh xmo.js
berisi properti non-enumerable yang disebut MetaInfo. Ini berisi informasi penting tentang kelas itu sendiri (dan warisan) dan dapat digunakan untuk menyimpan informasi tambahan yang diperlukan oleh ekstensi.
Mari kita tunjukkan dasar-dasar 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 ) ;
//
Penting untuk disebutkan bahwa semua anggota MetaClass dibekukan (tidak dapat diubah) dan tidak dapat dimodifikasi setelah kelas dibuat (setelah semua penangan postInit dipanggil). MetaInfo hanya dapat diubah selama pembuatan kelas oleh penangan init dan ekstensi kelas. Gunakan fungsi anggota getMutable()
untuk membuat properti MetaInfo dapat diubah untuk sementara.
Contoh berikut menunjukkan cara menambahkan informasi refleksi kustom ke 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