Extensible Meta Objects (xmo.js) เป็นไลบรารีคลาสน้ำหนักเบาสำหรับการสร้างคลาส JS และมิกซ์อิน
xmo.js
เป็นไลบรารี javascript น้ำหนักเบาสำหรับการสร้างคลาส JS และมิกซ์อิน แตกต่างจากไลบรารีอื่น ๆ มากมายที่พยายามแก้ไขปัญหาเดียวกัน xmo.js
ได้รับการออกแบบมาให้ขยายได้และมีกลไกในการสร้างคลาสใหม่หรือมิกซ์อินที่มีความเป็นไปได้ในการลงทะเบียนส่วนขยายที่ผู้ใช้กำหนดและตัวจัดการเริ่มต้น
มีชุดเครื่องมือ UI และเฟรมเวิร์ก JS มากมายที่ให้ระบบย่อยคลาสบางประเภทที่เพิ่มคุณสมบัติ เหตุการณ์ และคุณสมบัติอื่น ๆ ให้กับคลาส Javascript แบบดั้งเดิม แนวทางนี้ทำให้ระบบย่อยคลาสขึ้นอยู่กับเฟรมเวิร์ก และทำให้ไม่สามารถใช้งานได้ภายนอก
แนวทางที่ใช้โดย xmo.js
นั้นแตกต่างออกไป อนุญาตให้ลงทะเบียนตัวจัดการ init และส่วนขยายให้กับคลาสเอง ดังนั้นคลาสใดๆ ที่สืบทอดมาก็จะมีคุณสมบัติเหมือนกับคลาสพื้นฐาน ซึ่งหมายความว่าเฟรมเวิร์กคลาสที่มีอยู่จำนวนมากสามารถนำไปใช้ในทางทฤษฎีบน xmo.js
ได้
แล้วจะสร้างคลาส JavaScript ได้อย่างไร? เพียงนำเข้าไลบรารี xmo
(หรือรวมไว้หากคุณทำงานบนฝั่งไคลเอ็นต์) และใช้ xmo
เป็นฟังก์ชันเพื่อสร้างคลาสของคุณเอง ฟังก์ชันยอมรับอาร์กิวเมนต์เดียวเท่านั้น def
ซึ่งใช้ในการกำหนดสมาชิก คุณสมบัติคงที่ ตัวจัดการ init และส่วนขยาย มันจะส่งคืนออบเจ็กต์ 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
กำหนดตัวสร้างคลาสและคุณสมบัติอื่น ๆ กำหนดสมาชิกของคลาส Class
สามารถสร้างอินสแตนซ์ได้ง่ายๆ โดยใช้ตัวดำเนินการ new
ดังแสดงในตัวอย่างต่อไปนี้:
// 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 มีชุดส่วนขยายคงที่สำหรับโมเดลออบเจ็กต์ที่พวกเขาใช้ ตัวอย่างเช่น Qooxdoo รองรับมิกซ์อิน อินเทอร์เฟซ และอนุญาตให้กำหนดคุณสมบัติและเหตุการณ์ วัตถุประสงค์ของ xmo.js
ไม่ใช่เพื่อให้มีคุณลักษณะทั้งหมดเท่าที่จะจินตนาการได้ แต่เพื่อให้เป็นรากฐานในการเพิ่มส่วนขยายให้กับคลาสเฉพาะที่คลาสทั้งหมดที่สืบทอดมาจะรวมไว้ด้วย สิ่งนี้ทำให้สามารถขยายระบบคลาส ณ รันไทม์ และรองรับทุกสิ่งที่ผู้ใช้ต้องการ
ในขณะนี้มี 2 แนวคิดที่รองรับโดย xmo.js
:
$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 จะมีประสิทธิภาพมากกว่า เนื่องจากสามารถใช้คุณสมบัติใดๆ หรือหลายคุณสมบัติเพื่อเพิ่มสิ่งต่าง ๆ ให้กับคลาสได้
มิกซ์อินคือชุดของฟังก์ชันที่สามารถรวมไว้ในคลาสหรือมิกซ์อินอื่นได้ Mixins ถูกกำหนดโดยใช้ xmo.mixin(def)
โดยที่ def
มีคำจำกัดความที่คล้ายกันซึ่งเข้ากันได้กับ xmo.js
เอง แต่ไม่มีการสนับสนุน constructor
(ไม่สามารถสร้างอินสแตนซ์ของ mixins ได้) Mixins ยังเข้าใจ $extensions
และ $preinit/$postInit
handlers ดังนั้นจึงเป็นไปได้ที่จะกำหนดสิ่งเหล่านี้ใน 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]`.
การรวมมิกซ์อินเพิ่มเติมเข้ากับมิกซ์อินเดียว:
// 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 สามารถเปลี่ยนแปลงได้ในระหว่างการสร้างคลาสโดยตัวจัดการเริ่มต้นและส่วนขยายคลาสเท่านั้น ใช้ฟังก์ชันสมาชิก 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