Skip to content

Activate PHP-like magic methods in Javascript classes and instances.

License

Notifications You must be signed in to change notification settings

linhntaim/magic-class

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

magic-class

NPM version Github Actions Coveralls License

Activate PHP-like magic methods in Javascript classes and instances.



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 throw ReferenceError exception instead of getting undefined.
  • 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 returning true 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 throw ReferenceError exception instead of getting undefined.
  • 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 returning true 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 returns undefined.
  • 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 returns undefined.
  • 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)

// ...