Extensible Meta Objects(xmo.js)는 JS 클래스 및 믹스인을 생성하기 위한 경량 클래스 라이브러리입니다.
xmo.js
는 JS 클래스와 믹스인을 생성하기 위한 경량 자바스크립트 라이브러리입니다. 동일한 문제를 해결하려고 시도하는 다른 많은 라이브러리와 달리 xmo.js
확장 가능하도록 설계되었으며 사용자 정의 확장 및 초기화 핸들러를 등록할 수 있는 가능성이 있는 새 클래스 또는 믹스인을 생성하는 메커니즘만 제공합니다.
기존 자바스크립트 클래스에 속성, 이벤트 및 기타 기능을 추가하는 일종의 클래스 하위 시스템을 제공하는 많은 UI 툴킷과 JS 프레임워크가 있습니다. 이 접근 방식은 클래스 하위 시스템을 프레임워크에 종속되게 만들고 프레임워크 외부에서는 사용할 수 없도록 만듭니다.
xmo.js
에서 사용하는 접근 방식은 다릅니다. 초기화 핸들러와 확장을 클래스 자체에 등록할 수 있으므로 이를 상속하는 모든 클래스는 기본 클래스와 동일한 기능을 제공합니다. 이는 기존의 많은 클래스 프레임워크가 xmo.js
자체 위에 이론적으로 구현될 수 있음을 의미합니다.
그렇다면 JavaScript 클래스를 만드는 방법은 무엇입니까? xmo
라이브러리를 가져오고(또는 클라이언트 측에서 작업하는 경우 포함) xmo
함수로 사용하여 자신만의 클래스를 만듭니다. 이 함수는 멤버, 정적 속성, 초기화 처리기 및 확장을 정의하는 데 사용되는 def
인수 하나만 허용합니다. new
키워드로 인스턴스화할 수 있는 새 Class
객체를 반환합니다. 간단한 예가 아래에 나와 있습니다.
// 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
의 목적은 상상할 수 있는 모든 기능을 제공하는 것이 아니라 특정 클래스에 확장을 추가한 다음 이를 상속하는 모든 클래스에 포함될 수 있는 기반을 제공하는 것입니다. 이를 통해 런타임 시 클래스 시스템을 확장하고 사용자가 필요로 하는 거의 모든 것을 지원할 수 있습니다.
현재 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 핸들러는 확장과 매우 유사하지만 정의할 속성이 필요하지 않으며 항상 클래스당 한 번씩 호출됩니다. Init 핸들러는 일반적으로 어떤 속성이나 여러 속성을 사용하여 클래스 자체에 항목을 추가할 수 있기 때문에 더 강력합니다.
믹스인은 다른 클래스나 믹스인에 포함될 수 있는 함수 집합입니다. 믹스인은 xmo.mixin(def)
사용하여 정의됩니다. 여기서 def
는 xmo.js
자체와 호환되는 유사한 정의이지만 constructor
지원하지 않습니다(믹스인은 인스턴스화할 수 없음). 믹스인은 $extensions
및 $preinit/$postInit
핸들러도 이해하므로 이를 믹스인에서 정의한 후 다른 클래스에 포함시키는 것이 가능합니다.
// 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는 초기화 처리기 및 클래스 확장을 통해 클래스를 생성하는 동안에만 변경할 수 있습니다. MetaInfo의 속성을 일시적으로 변경 가능하게 만들려면 getMutable()
멤버 함수를 사용하세요.
다음 예에서는 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