magic-class
Activate PHP-like magic methods in Javascript classes and instances.
- Installation
- Usage
- Features
Installation
npm install magic-class --save
Usage
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static magicProps = {}
static __set(prop, value) {
this.magicProps[`static:${prop}`] = value
}
static __get(prop) {
if (`static:${prop}` in this.magicProps) {
return this.magicProps[`static:${prop}`]
}
if (prop.startsWith('call')) {
return undefined
}
return `static:${prop}`
}
static __call(method, ...parameters) {
return {method: `static:${method}`, parameters}
}
static __has(prop) {
return `static:${prop}` in this.magicProps
}
static __delete(prop) {
return delete this.magicProps[`static:${prop}`]
}
magicProps = {}
constructor(normal) {
this.normal = normal
}
__set(prop, value) {
this.magicProps[prop] = value
}
__get(prop) {
if (prop in this.magicProps) {
return this.magicProps[prop]
}
if (prop.startsWith('call')) {
return undefined
}
return prop
}
__call(method, ...parameters) {
return {method, parameters}
}
__has(prop) {
return prop in this.magicProps
}
__delete(prop) {
return delete this.magicProps[prop]
}
__invoke(...parameters) {
return {parameters}
}
}
// Create magic class
const MagicClass = magic(NormalClass)
// magic static __set
MagicClass.magic = true
console.log(MagicClass.magicProps) // (object) {'static:magic': true}
// magic static __get
console.log(MagicClass.magic) // (boolean) true
console.log(MagicClass.any) // (string) 'static:any'
// magic static __call
console.log(MagicClass.callAny(true)) // (object) {method: 'static:callAny', parameters: [true]}
// magic static __has
console.log('magic' in MagicClass) // (boolean) true
// magic static __delete
console.log(delete MagicClass.magic) // (boolean) true
console.log('magic' in MagicClass) // (boolean) false
// Create magic instance
const magicInstance = new MagicClass('normal')
/* or */
// const magicInstance = magic(new NormalClass())
// magic __set
magicInstance.magic = true
console.log(magicInstance.magicProps) // (object) {magic: true}
// magic __get
console.log(magicInstance.magic) // (boolean) true
console.log(magicInstance.any) // (string) 'any'
// magic __call
console.log(magicInstance.callAny(true)) // (object) {method: 'callAny', parameters: [true]}
// magic __has
console.log('magic' in magicInstance) // (boolean) true
// magic __delete
console.log(delete magicInstance.magic) // (boolean) true
console.log('magic' in magicInstance) // (boolean) false
// magic __invoke
console.log(magicInstance(true)) // (object) {parameters: [true]}
Features
Magic methods
__set
__set
is run when writing data to non-existing properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
magicProps = {}
constructor(normal) {
this.normal = normal
}
__set(prop, value) {
this.magicProps[prop] = value
}
}
const MagicClass = magic(NormalClass)
const magicInstance = new MagicClass('normal')
// existing prop
console.log(magicInstance.normal) // (string) 'normal'
magicInstance.normal = 'new value'
console.log(magicInstance.normal) // (string) 'new value'
// non-existing prop
try {
console.log(magicInstance.magic)
}
catch (e) {
console.log(e.message) // (string) 'Property [magic] does not exist.'
}
magicInstance.magic = true
try {
console.log(magicInstance.magic)
}
catch (e) {
console.log(e.message) // (string) 'Property [magic] does not exist.'
}
console.log(magicInstance.magicProps) // (object) {magic: true}
**Note:
- While magic is activated in default strict mode
and without magic
__get
/__call
methods, accessing non-existing properties will throwReferenceError
exception instead of gettingundefined
. - While magic is activated in default strict mode,
writing data to non-existing properties will throw
ReferenceError
exception.
__get
__get
is run when reading data from non-existing properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
constructor(normal) {
this.normal = normal
}
__get(prop) {
if (prop.startsWith('call')) {
return undefined
}
return `magic:${prop}`
}
}
const MagicClass = magic(NormalClass)
const magicInstance = new MagicClass('normal')
// existing prop
console.log(magicInstance.normal) // (string) 'normal'
// non-existing prop
console.log(magicInstance.value) // (string) 'magic:value'
console.log(magicInstance.any) // (string) 'magic:any'
try {
console.log(magicInstance.callAny)
}
catch (e) {
console.log(e.message) // (string) 'Property [callAny] does not exist.'
}
**Note: While magic is activated in default strict mode and
without magic __call
method, accessing non-existing properties
will throw ReferenceError
exception when magic __get
returns undefined
.
__call
__call
is run when calling non-existing properties as function
while magic __get
is not declared or magic __get
returns undefined
.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
/* magic `__get` is not declared */
class NormalClass1
{
constructor(normal) {
this.normal = normal
}
__call(method, ...parameters) {
return {method, parameters}
}
}
const MagicClass1 = magic(NormalClass1)
const magicInstance1 = new MagicClass1('normal')
// existing prop
console.log(magicInstance1.normal) // (string) 'normal'
// non-existing prop
console.log(magicInstance1.any) // (function)
console.log(magicInstance1.value(1)) // (object) {method: 'value', parameters: [1]}
/* magic `__get` returns `undefined` in some cases */
class NormalClass2
{
constructor(normal) {
this.normal = normal
}
__get(prop) {
if (prop.startsWith('call')) {
return undefined
}
return `magic:${prop}`
}
__call(method, ...parameters) {
return {method, parameters}
}
}
const MagicClass2 = magic(NormalClass2)
const magicInstance2 = new MagicClass2('normal')
// existing prop
console.log(magicInstance2.normal) // (string) 'normal'
// non-existing prop
console.log(magicInstance2.any) // (string) 'magic:any'
try {
console.log(magicInstance2.value(1))
}
catch (e) {
console.log(e.message) // (string) 'magicInstance2.value is not a function'
}
console.log(magicInstance2.callValue(1)) // (object) {method: 'callValue', parameters: [1]}
**Note: If magic __get
never returns undefined
, magic __call
is also never run.
__invoke
__invoke
is run when calling instance as a function.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
constructor(normal) {
this.normal = normal
}
__invoke(parameters) {
return {parameters}
}
}
const MagicClass = magic(NormalClass)
const magicInstance = new MagicClass('normal')
console.log(magicInstance(1)) // (object) {parameters: [1]}
__has
__has
is run when checking existence of non-existing properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
constructor(normal) {
this.normal = normal
}
__has(prop) {
if (prop === 'magic') {
return true
}
return false
/* or */
// return // returning nothing means returning `false`
}
}
const MagicClass = magic(NormalClass)
const magicInstance = new MagicClass('normal')
// existing prop
console.log('normal' in magicInstance) // (boolean) true
// non-existing prop
console.log('magic' in magicInstance) // (boolean) true
console.log('other' in magicInstance) // (boolean) false
**Note: Magic __has
has a fallback of returning false
in case it returns nothing.
__delete
__delete
is run when deleting non-existing properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
magicProps = {
magic: true,
}
constructor(normal) {
this.normal = normal
}
__delete(prop) {
return delete this.magicProps[prop]
}
}
const MagicClass = magic(NormalClass)
const magicInstance = new MagicClass('normal')
// existing prop
try {
console.log(delete magicInstance.normal)
}
catch (e) {
console.log(e.message) // (string) 'Cannot delete property [normal].'
}
// non-existing props
console.log('magic' in magicInstance.magicProps) // (boolean) true
console.log(delete magicInstance.magic) // (boolean) true
console.log('magic' in magicInstance.magicProps) // (boolean) false
console.log('other' in magicInstance.magicProps) // (boolean) false
console.log(delete magicInstance.other) // (boolean) true
console.log('other' in magicInstance.magicProps) // (boolean) false
**Note:
- While magic is activated in default strict mode, deleting existing properties
throws
TypeError
exception. - Magic
__delete
has a fallback of returningtrue
in case it returns nothing.
Method chaining
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
chain = []
constructor(...parameters) {
this.push(...parameters.map(p => `construct:${p}`))
}
push(...parameters) {
this.chain.push(...parameters)
return this
}
__get(prop) {
if (prop.startsWith('call')) {
return undefined
}
if (prop === 'self') {
return this
}
return this.push(`get:${prop}`)
}
__call(method, ...parameters) {
if (['callInsert', 'callAdd'].includes(method)) {
return this.push(...parameters.map(p => `${method}:${p}`))
}
return this.push(`call:${method}`)
}
__invoke(...parameters) {
return this.push(...parameters.map(p => `invoke:${p}`))
}
}
const MagicClass = magic(NormalClass)
// Chain: (constructor)->(magic __invoke)->(existing method)->(magic __call)->(magic __get)->(existing prop)
const magicChain = (new MagicClass(0))(1).push(2).callInsert(3).callAdd(4).callAny(5).any.self.chain
console.log(magicChain) // (array) ['construct:0', 'invoke:1', 2, 'callInsert:3', 'callAdd:4', 'call:callAny', 'get:any']
Magic static methods
Static __set
Static __set
is run when writing data to non-existing static properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static normal = 'normal'
static magicProps = {}
static __set(prop, value) {
this.magicProps[prop] = value
}
}
const MagicClass = magic(NormalClass)
// existing static prop
console.log(MagicClass.normal) // (string) 'normal'
MagicClass.normal = 'new value'
console.log(MagicClass.normal) // (string) 'new value'
// non-existing static prop
try {
console.log(MagicClass.magic)
}
catch (e) {
console.log(e.message) // (string) 'Static property [magic] does not exist.'
}
MagicClass.magic = true
try {
console.log(MagicClass.magic)
}
catch (e) {
console.log(e.message) // (string) 'Static property [magic] does not exist.'
}
console.log(MagicClass.magicProps) // (object) {magic: true}
**Note:
- While magic is activated in default strict mode and
without magic static
__get
/__call
methods, accessing non-existing static properties will throwReferenceError
exception instead of gettingundefined
. - While magic is activated in default strict mode,
writing data to non-existing static properties will throw
ReferenceError
exception.
Static __get
Static __get
is run when reading data from non-existing static properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static normal = 'normal'
static __get(prop) {
if (prop.startsWith('call')) {
return undefined
}
return `magic:${prop}`
}
}
const MagicClass = magic(NormalClass)
// existing static prop
console.log(MagicClass.normal) // (string) 'normal'
// non-existing static props
console.log(MagicClass.value) // (string) 'magic:value'
console.log(MagicClass.any) // (string) 'magic:any'
try {
console.log(MagicClass.callAny)
}
catch (e) {
console.log(e.message) // (string) 'Static property [callAny] does not exist.'
}
**Note: While magic is activated in default strict mode and
without magic static __call
method, accessing non-existing static properties
will throw ReferenceError
exception when magic static __get
returns undefined
.
Static __call
Static __call
is run when calling non-existing static properties as function
while magic static __get
is not declared or magic static __get
returns undefined
.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
/* magic `__get` is not declared */
class NormalClass1
{
static normal = 'normal'
static __call(method, ...parameters) {
return {method, parameters}
}
}
const MagicClass1 = magic(NormalClass1)
// existing prop
console.log(MagicClass1.normal) // (string) 'normal'
// non-existing prop
console.log(MagicClass1.any) // (function)
console.log(MagicClass1.value(1)) // (object) {method: 'value', parameters: [1]}
/* magic `__get` returns `undefined` in some cases */
class NormalClass2
{
static normal = 'normal'
static __get(prop) {
if (prop.startsWith('call')) {
return undefined
}
return `magic:${prop}`
}
static __call(method, ...parameters) {
return {method, parameters}
}
}
const MagicClass2 = magic(NormalClass2)
// existing static prop
console.log(MagicClass2.normal) // (string) 'normal'
// non-existing static props
console.log(MagicClass2.any) // (string) 'magic:any'
try {
console.log(MagicClass2.value(1))
}
catch (e) {
console.log(e.message) // (string) 'MagicClass2.value is not a function'
}
console.log(MagicClass2.callValue(1)) // (object) {method: 'callValue', parameters: [1]}
**Note: If magic static __get
never returns undefined
, magic static __call
is also never run.
Static __has
Static __has
is run when checking existence of non-existing static properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static normal = 'normal'
static __has(prop) {
if (prop === 'magic') {
return true
}
return false
/* or */
// return // returning nothing means returning `false`
}
}
const MagicClass = magic(NormalClass)
// existing static prop
console.log('normal' in MagicClass) // (boolean) true
// non-existing static props
console.log('magic' in MagicClass) // (boolean) true
console.log('other' in MagicClass) // (boolean) false
**Note: Magic static __has
has a fallback of returning false
in case it returns nothing.
Static __delete
Static __delete
is run when deleting non-existing static properties.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static normal = 'normal'
static magicProps = {
magic: true,
}
static __delete(prop) {
return delete this.magicProps[prop]
}
}
const MagicClass = magic(NormalClass)
// existing static prop
try {
console.log(delete MagicClass.normal)
}
catch (e) {
console.log(e.message) // (string) 'Cannot delete property [normal].'
}
// non-existing static props
console.log('magic' in MagicClass.magicProps) // (boolean) true
console.log(delete MagicClass.magic) // (boolean) true
console.log('magic' in MagicClass.magicProps) // (boolean) false
console.log('other' in MagicClass.magicProps) // (boolean) false
console.log(delete MagicClass.other) // (boolean) true
console.log('other' in MagicClass.magicProps) // (boolean) false
**Note:
- While magic is activated in default strict mode,
deleting existing static properties throws
TypeError
exception. - Magic static
__delete
has a fallback of returningtrue
in case it returns nothing.
Static method chaining
It is possible to call magic static methods in a chain.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static chain = [0]
static push(...parameters) {
this.chain.push(...parameters)
return this
}
static __get(prop) {
if (prop.startsWith('call')) {
return undefined
}
if (prop === 'self') {
return this
}
return this.push(`get:${prop}`)
}
static __call(method, ...parameters) {
if (['callInsert', 'callAdd'].includes(method)) {
return this.push(...parameters.map(p => `${method}:${p}`))
}
return this.push(`call:${method}`)
}
}
const MagicClass = magic(NormalClass)
// Chain: (class)->(existing static method)->(magic static __call)->(magic static __get)->(existing static prop)
const magicChain = MagicClass.push(1).callInsert(2).callAdd(3).callAny(4).any.self.chain
console.log(magicChain) // (array) [0, 1, 'callInsert:2', 'callAdd:3', 'call:callAny', 'get:any']
Use Symbol
as magic method name
Besides strings, there are defined symbols you can use to naming the magic methods:
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
// Defined symbols
console.log(magic.__set) // (symbol) Symbol(Symbol.__set)
console.log(magic.__get) // (symbol) Symbol(Symbol.__get)
console.log(magic.__call) // (symbol) Symbol(Symbol.__call)
console.log(magic.__has) // (symbol) Symbol(Symbol.__has)
console.log(magic.__delete) // (symbol) Symbol(Symbol.__delete)
console.log(magic.__invoke) // (symbol) Symbol(Symbol.__invoke)
class NormalClass
{
static magicProps = {}
// equivalent to `static __set`
static [magic.__set](prop, value) {
this.magicProps[`static:${prop}`] = value
}
// equivalent to `static __get`
static [magic.__get](prop) {
if (`static:${prop}` in this.magicProps) {
return this.magicProps[`static:${prop}`]
}
if (prop.startsWith('call')) {
return undefined
}
return `static:${prop}`
}
// equivalent to `static __call`
static [magic.__call](method, ...parameters) {
return {method: `static:${method}`, parameters}
}
// equivalent to `static __has`
static [magic.__has](prop) {
return `static:${prop}` in this.magicProps
}
// equivalent to `static __delete`
static [magic.__delete](prop) {
return delete this.magicProps[`static:${prop}`]
}
magicProps = {}
constructor(normal) {
this.normal = normal
}
// equivalent to `__set`
[magic.__set](prop, value) {
this.magicProps[prop] = value
}
// equivalent to `__get`
[magic.__get](prop) {
if (prop in this.magicProps) {
return this.magicProps[prop]
}
if (prop.startsWith('call')) {
return undefined
}
return prop
}
// equivalent to `__call`
[magic.__call](method, ...parameters) {
return {method, parameters}
}
// equivalent to `__has`
[magic.__has](prop) {
return prop in this.magicProps
}
// equivalent to `__delete`
[magic.__delete](prop) {
return delete this.magicProps[prop]
}
// equivalent to `__invoke`
[magic.__invoke](...parameters) {
return {parameters}
}
}
// Create magic class
const MagicClass = magic(NormalClass)
// magic static __set
MagicClass.magic = true
console.log(MagicClass.magicProps) // (object) {'static:magic': true}
// magic static __get
console.log(MagicClass.magic) // (boolean) true
console.log(MagicClass.any) // (string) 'static:any'
// magic static __call
console.log(MagicClass.callAny(true)) // (object) {method: 'static:callAny', parameters: [true]}
// magic static __has
console.log('magic' in MagicClass) // (boolean) true
// magic static __delete
console.log(delete MagicClass.magic) // (boolean) true
console.log('magic' in MagicClass) // (boolean) false
// Create magic instance
const magicInstance = new MagicClass('normal')
/* or */
// const magicInstance = magic(new NormalClass())
// magic __set
magicInstance.magic = true
console.log(magicInstance.magicProps) // (object) {magic: true}
// magic __get
console.log(magicInstance.magic) // (boolean) true
console.log(magicInstance.any) // (string) 'any'
// magic __call
console.log(magicInstance.callAny(true)) // (object) {method: 'callAny', parameters: [true]}
// magic __has
console.log('magic' in magicInstance) // (boolean) true
// magic __delete
console.log(delete magicInstance.magic) // (boolean) true
console.log('magic' in magicInstance) // (boolean) false
// magic __invoke
console.log(magicInstance(true)) // (object) {parameters: [true]}
**Note: The Symbol
-naming magic method has a higher priority in calling
than the string
-naming one.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
// equivalent to `__get` but has a higher priority
[magic.__get](prop) {
return `symbol:${prop}`
}
__get(prop) {
return prop
}
}
const magicInstance = magic(new NormalClass())
console.log(magicInstance.magic) // (string) 'symbol:magic'
Prototype operations
Technically, the class after the magic is activated (which is a proxy object) is different from
the original class, but their prototypes are the same. So, operations by getPrototypeOf
method,
isPrototypeOf
method and the instanceof
operator should work normally as usual.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class GrandParentClass
{
}
class ParentClass extends GrandParentClass
{
}
class NormalClass extends ParentClass
{
__get(prop) {
return prop
}
}
const MagicClass = magic(NormalClass)
/* getPrototypeOf */
console.log(Object.getPrototypeOf(NormalClass) === ParentClass) // (boolean) true
console.log(Object.getPrototypeOf(MagicClass) === ParentClass) // (boolean) true
/* isPrototypeOf */
console.log(ParentClass.isPrototypeOf(NormalClass)) // (boolean) true
console.log(ParentClass.isPrototypeOf(MagicClass)) // (boolean) true
console.log(GrandParentClass.isPrototypeOf(NormalClass)) // (boolean) true
console.log(GrandParentClass.isPrototypeOf(MagicClass)) // (boolean) true
/* instanceof */
const normalInstance = new NormalClass()
const magicInstance = new MagicClass()
/* or */
// const magicInstance = magic(normalInstance)
console.log(normalInstance instanceof MagicClass) // (boolean) true
console.log(normalInstance instanceof NormalClass) // (boolean) true
console.log(normalInstance instanceof ParentClass) // (boolean) true
console.log(normalInstance instanceof GrandParentClass) // (boolean) true
// `instanceof MagicClass = true` but no magic
console.log(normalInstance.value) // (undefined) undefined
console.log(magicInstance instanceof MagicClass) // (boolean) true
console.log(magicInstance instanceof NormalClass) // (boolean) true
console.log(magicInstance instanceof ParentClass) // (boolean) true
console.log(magicInstance instanceof GrandParentClass) // (boolean) true
// Magic!
console.log(magicInstance.value) // (string) 'value'
**Note: Operation by setPrototypeOf
method is not allowed. Trying to apply it
to magic classes or instances will throw TypeError
exception.
Strict mode
Strict mode is on by default while activating the magic. It will raise exceptions in following cases:
- Writing data to non-existing properties while magic
__set
is not declared. - Reading data from non-existing properties while magic
__call
is not declared, and magic__get
is not declared or returnsundefined
. - Deleting existing properties.
- Writing data to non-existing static properties while magic static
__set
is not declared. - Reading data from non-existing properties while magic static
__call
is not declared, and magic static__get
is not declared or returnsundefined
. - Deleting existing static properties.
To turn off strict mode, pass the false
value as the second parameter while calling magic
function.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static normal = 'normal'
normal = 'normal'
}
/* Strict mode is ON */
const StrictMagicClass = magic(NormalClass)
const strictMagicInstance = new StrictMagicClass()
// get non-existing prop
try {
console.log(strictMagicInstance.magic)
}
catch (e) {
console.log(e.message) // (string) 'Property [magic] does not exist.'
}
// set non-existing prop
try {
strictMagicInstance.magic = 'magic'
}
catch (e) {
console.log(e.message) // (string) 'Property [magic] does not exist.'
}
// delete existing prop
try {
delete strictMagicInstance.normal
}
catch (e) {
console.log(e.message) // (string) 'Cannot delete property [normal].'
}
// get non-existing static prop
try {
console.log(StrictMagicClass.magic)
}
catch (e) {
console.log(e.message) // (string) 'Static property [magic] does not exist.'
}
// set non-existing static prop
try {
StrictMagicClass.magic = 'magic'
}
catch (e) {
console.log(e.message) // (string) 'Static property [magic] does not exist.'
}
// delete existing static prop
try {
delete StrictMagicClass.normal
}
catch (e) {
console.log(e.message) // (string) 'Cannot delete static property [normal].'
}
/* Strict mode is OFF */
const NotStrictMagicClass = magic(NormalClass, false)
const notStrictMagicInstance = new NotStrictMagicClass()
// get non-existing prop
console.log(notStrictMagicInstance.magic) // (undefined) undefined
console.log('magic' in notStrictMagicInstance) // (boolean) false
// set non-existing prop
notStrictMagicInstance.magic = 'magic'
console.log(notStrictMagicInstance.magic) // (string) 'magic'
console.log('magic' in notStrictMagicInstance) // (boolean) true
// delete existing prop
delete notStrictMagicInstance.normal
console.log(notStrictMagicInstance.normal) // (undefined) 'undefined'
console.log('normal' in notStrictMagicInstance) // (boolean) false
// get non-existing static prop
console.log(NotStrictMagicClass.magic) // (undefined) undefined
console.log('magic' in NotStrictMagicClass) // (boolean) false
// set non-existing static prop
NotStrictMagicClass.magic = 'magic'
console.log(NotStrictMagicClass.magic) // (string) 'magic'
console.log('magic' in NotStrictMagicClass) // (boolean) true
// delete existing static prop
delete NotStrictMagicClass.normal
console.log(NotStrictMagicClass.normal) // (undefined) 'undefined'
console.log('normal' in NotStrictMagicClass) // (boolean) false
Special magic static methods
Static __instance
This magic static method is to create instance of the class without using new
operator.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
constructor(...parameters) {
this.parameters = parameters
}
}
const MagicClass = magic(NormalClass)
/* `new` operator */
const magicInstance1 = new MagicClass(1, 2, 3)
console.log(magicInstance2.parameters) // (array) [1, 2, 3]
/* magic `__instance` */
const magicInstance2 = MagicClass.__instance(1, 2, 3)
console.log(magicInstance1.parameters) // (array) [1, 2, 3]
Static __singleton
This magic static method is to create only one instance of the class.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
constructor(...parameters) {
this.parameters = parameters
}
}
const MagicClass = magic(NormalClass)
// create a singleton instance
const magicInstance1 = MagicClass.__singleton(1, 2, 3)
console.log(magicInstance1.parameters) // (array) [1, 2, 3]
// create new instance via `new` operator?
const magicInstance2 = new MagicClass(4, 5, 6)
const magicInstance3 = new MagicClass()
// no, it's the same with the first instance
console.log(magicInstance2 === magicInstance1) // (boolean) true
console.log(magicInstance2.parameters) // (array) [1, 2, 3]
console.log(magicInstance3 === magicInstance1) // (boolean) true
console.log(magicInstance3.parameters) // (array) [1, 2, 3]
// create new instance via magic `__instance`?
const magicInstance4 = MagicClass.__instance(7, 8, 9)
const magicInstance5 = MagicClass.__instance()
// no, it's the same with the first instance
console.log(magicInstance4 === magicInstance1) // (boolean) true
console.log(magicInstance4.parameters) // (array) [1, 2, 3]
console.log(magicInstance5 === magicInstance1) // (boolean) true
console.log(magicInstance5.parameters) // (array) [1, 2, 3]
// create new instance via magic `__singleton` again?
const magicInstance6 = MagicClass.__singleton(9, 10, 11)
const magicInstance7 = MagicClass.__singleton()
// no, it's the same with the first instance
console.log(magicInstance6 === magicInstance1) // (boolean) true
console.log(magicInstance6.parameters) // (array) [1, 2, 3]
console.log(magicInstance7 === magicInstance1) // (boolean) true
console.log(magicInstance7.parameters) // (array) [1, 2, 3]
**Note: The instances created before the first call to magic static __singleton
are different from
the instance created by magic static __singleton
.
Inheritance
Declaring a magic subclass inherits directly from the magic class is possible, but not recommended.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
magicProps = {}
__set(prop, value) {
this.magicProps[prop] = value
}
__get(prop) {
if (prop.startsWith('call')) {
return undefined
}
return prop
}
__call(method, ...parameters) {
return {method, parameters}
}
__invoke(...parameters) {
return {parameters}
}
__has(prop) {
return prop in this.magicProps
}
__delete(prop) {
return delete this.magicProps[prop]
}
}
const MagicClass = magic(NormalClass)
/* magic subclass inherits directly from magic class */
const SubMagicClass = class extends MagicClass
{
subMagicProps = {
subMagic: true,
}
/* overrides magic `__set` */
__set(prop, value) {
super.__set(prop, {sub: value})
}
/* overrides magic `__get` */
__get(prop) {
const value = super.__get(prop)
return value === undefined ? undefined : {sub: value}
}
/* overrides magic `__call` */
__call(method, ...parameters) {
return {sub: super.__call(method, ...parameters)}
}
/* overrides magic `__invoke` */
__invoke(...parameters) {
return {sub: super.__invoke(...parameters)}
}
/* overrides magic `__has` */
__has(prop) {
return super.__has(prop) || prop in this.subMagicProps
}
/* overrides magic `__delete` */
__delete(prop) {
super.__delete(prop)
return delete this.subMagicProps[prop]
}
}
// create sub magic instance
const subMagicInstance = new SubMagicClass
// magic `__set` still works
subMagicInstance.magic = true
console.log(subMagicInstance.magicProps) // (object) {magic: {sub: true}}
// magic `__get` still works
console.log(subMagicInstance.magic) // (object) {sub: 'magic'})
// magic `__call` still works
console.log(subMagicInstance.callMagic('magic', true)) // (object) {sub: {method: 'callMagic', parameters: ['magic', true]}}
// magic `__invoke` still works
console.log(subMagicInstance('magic', true)) // (object) {sub: {parameters: ['magic', true]}}
// magic `__has` still works
console.log('subMagic' in subMagicInstance) // (boolean) true
// magic `__delete` still works
console.log(delete subMagicInstance.subMagic) // (boolean) true
console.log('subMagic' in subMagicInstance) // (boolean) false
console.log(subMagicInstance.subMagicProps) // (object) {}
The reason is static properties/methods (including magic static methods and special ones) cannot be overridden with direct inheritance.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
static magicProps = {}
static __set(prop, value) {
this.magicProps[prop] = value
}
static __get(prop) {
if (prop.startsWith('call')) {
return undefined
}
return prop
}
static __call(method, ...parameters) {
return {method, parameters}
}
static __has(prop) {
return prop in this.magicProps
}
static __delete(prop) {
return delete this.magicProps[prop]
}
}
const MagicClass = magic(NormalClass)
/* magic subclass inherits directly from magic class */
const SubMagicClass = class extends MagicClass
{
static subMagicProps = {
subMagic: true,
}
/* overrides magic static `__set` */
static __set(prop, value) {
super.__set(prop, {sub: value})
}
/* overrides magic static `__get` */
static __get(prop) {
const value = super.__get(prop)
return value === undefined ? undefined : {sub: value}
}
/* overrides magic static `__call` */
static __call(method, ...parameters) {
return {sub: super.__call(method, ...parameters)}
}
/* overrides magic static `__has` */
static __has(prop) {
return super.__has(prop) || prop in this.subMagicProps
}
/* overrides magic static `__delete` */
static __delete(prop) {
super.__delete(prop)
return delete this.subMagicProps[prop]
}
}
// magic static `__set` not work as expected
SubMagicClass.magic = true
console.log(SubMagicClass.magicProps) // (object) {magic: true} // expected: (object) {magic: {sub: true}}
// magic static `__get` not work as expected
console.log(SubMagicClass.magic) // (string) 'magic' // expected: (object) {sub: 'magic'})
// magic static `__call` not work as expected
console.log(SubMagicClass.callMagic('magic', true)) // (object) {method: 'callMagic', parameters: ['magic', true]} // expected: (object) {sub: {method: 'callMagic', parameters: ['magic', true]}}
// magic static `__has` still works
console.log('subMagic' in SubMagicClass) // (boolean) false // expected: (boolean) true
// magic static `__delete` still works
console.log(delete SubMagicClass.subMagic) // (boolean) true
console.log('subMagic' in SubMagicClass) // (boolean) false
console.log(SubMagicClass.subMagicProps) // (object) {subMagic: true} // expected: (object) {}
The recommended way:
- Firstly, declaring the subclass inherits from the original class.
- Then, apply the magic to the subclass to get the magic subclass.
const magic = require('magic-class')
/* or ES6 */
// import magic from 'magic-class'
class NormalClass
{
// ...
}
const MagicClass = magic(NormalClass)
// 1. Declaring the subclass inherits from the original class
class SubNormalClass extends NormalClass
{
// ...
}
// 2. Apply the magic to the subclass to get the magic subclass
const SubMagicClass = magic(SubNormalClass)
// ...