https://github.com/linhntaim/magic-class
Activate PHP-like magic methods in Javascript classes and instances.
https://github.com/linhntaim/magic-class
magic magic-method magic-methods method methods node node-js node-package nodejs npm php php-like
Last synced: 6 months ago
JSON representation
Activate PHP-like magic methods in Javascript classes and instances.
- Host: GitHub
- URL: https://github.com/linhntaim/magic-class
- Owner: linhntaim
- License: mit
- Created: 2023-10-28T06:55:25.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2023-11-07T03:14:36.000Z (about 2 years ago)
- Last Synced: 2025-06-22T15:06:04.875Z (6 months ago)
- Topics: magic, magic-method, magic-methods, method, methods, node, node-js, node-package, nodejs, npm, php, php-like
- Language: JavaScript
- Homepage:
- Size: 289 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# magic-class
[](https://www.npmjs.com/package/magic-class)
[](https://github.com/linhntaim/magic-class/actions/workflows/build.yml)
[](https://coveralls.io/github/linhntaim/magic-class)
[](https://github.com/linhntaim/magic-class/blob/master/LICENSE)
Activate PHP-like magic methods in Javascript classes and instances.
---
- [Installation](#installation)
- [Usage](#usage)
- [Features](#features)
- [Magic methods](#magic-methods)
- [`__set`](#__set)
- [`__get`](#__get)
- [`__call`](#__call)
- [`__invoke`](#__invoke)
- [`__has`](#__has)
- [`__delete`](#__delete)
- [Method chaining](#method-chaining)
- [Magic static methods](#magic-static-methods)
- [Static `__set`](#static-__set)
- [Static `__get`](#static-__get)
- [Static `__call`](#static-__call)
- [Static `__has`](#static-__has)
- [Static `__delete`](#static-__delete)
- [Static method chaining](#static-method-chaining)
- [Use `Symbol` as magic method name](#use-symbol-as-magic-method-name)
- [Prototype operations](#prototype-operations)
- [Strict mode](#strict-mode)
- [Special magic static methods](#special-magic-static-methods)
- [Static `__instance`](#static-__instance)
- [Static `__singleton`](#static-__singleton)
- [Inheritance](#inheritance)
---
## Installation
```bash
npm install magic-class --save
```
## Usage
```javascript
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.
```javascript
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](#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](#strict-mode),
writing data to non-existing properties will throw `ReferenceError` exception.
#### `__get`
`__get` is run when reading data from non-existing properties.
```javascript
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](#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`.
```javascript
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.
```javascript
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.
```javascript
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.
```javascript
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](#strict-mode), deleting existing properties
throws `TypeError` exception.
- Magic `__delete` has a fallback of returning `true` in case it returns nothing.
#### Method chaining
```javascript
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.
```javascript
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](#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](#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.
```javascript
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](#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`.
```javascript
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.
```javascript
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.
```javascript
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](#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.
```javascript
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:
```javascript
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.
```javascript
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.
```javascript
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.
```javascript
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.
```javascript
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.
```javascript
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**.
```javascript
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](#magic-static-methods)
and [special ones](#special-magic-static-methods)) cannot be overridden with direct inheritance.
```javascript
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.
```javascript
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)
// ...
```