ig-object

object.js is a set of tools and abstractions to create and manage constructors, objects and prototype chains in idiomatic JavaScript.

Usage no npm install needed!

<script type="module">
  import igObject from 'https://cdn.skypack.dev/ig-object';
</script>

README

object.js

object.js is a set of tools and abstractions to create and manage constructors, objects and prototype chains in idiomatic JavaScript.

This is an alternative to the ES6 class syntax in JavaScript and provides several advantages:

  • Uniform and minimalistic definition "syntax" based on basic JavaScript object literals. No special cases, special syntax or "the same but slightly different" ways to do things, trying to adhere to POLS as much as possible,
  • Transparently based on JavaScript's prototypical inheritance model,
  • Produces fully introspectable constructors/instances,
  • Does not try to emulate constructs foreign to JavaScript (i.e. classes),
  • Granular 2-stage instance construction and initialization (a-la Python's .__new__(..) and .__init__(..) methods),
  • Simple way to define callable instances (including a-la Python's .__call__(..)),
  • Less restrictive:
    • new is optional,
    • all input components are reusable JavaScript objects,
    • no artificial restrictions.

Disadvantages compared to the class syntax:

  • No syntactic sugar,
  • Slightly more complicated calling of parent (super) methods.

Note that the produced constructors and objects are functionally identical (almost) to the ones produced via ES6 classes and are interchangeable with them.

Here is a basic comparison:

object.js

var A = object.Constructor('A', {
    // prototype attribute (inherited)...
    attr: 'prototype',

    method: function(){
        // ...
    },
})

var B = object.Constructor('B', A, {
    constructor_attr: 'constructor',

    constructor_method: function(){
        return 'constructor'
    },
}, {
    get prop(){
        return 42 },

    __init__: function(){
        this.instance_attr = 7
    },
})
  • No direct way to do "private" definitions,
  • Clear separation of constructor and .prototype
    For example, in B:
    • First block (optional) is merged with B,
    • Second block is the B.prototype,
  • No special syntax, stands out less.

ES6

class A {
    // instance attribute (copied)...
    attr = 'instance'

    method(){
        // ...
    }
}

class B extends A {
    static constructor_attr = 'class'

    static constructor_method(){
        return 'class'
    }

    get prop(){
        return 42 }

    constructor(){
        super(...arguments)	

        this.instance_attr = 7
    }
}
  • Syntax pretty but misleading;
    calling a constructor a class is not correct,
  • static and instance definitions are not separated,
  • lots of details done non-transparently under the hood.

Contents

Installation

$ npm install ig-object

Or just download and drop object.js into your code.

Basic usage

Include the code, this is compatible with both node's and RequireJS' require(..)

var object = require('ig-object')

Create a basic constructor...

// NOTE: new is optional here...
var A = new object.Constructor('A', {})

var B = object.Constructor('B', A, {})

var C = object.Constructor('C', B, {})

Now we can test this...

var c = C() // or new C()

c instanceof C // -> true
c instanceof B // -> true
c instanceof A // -> true

Note:

  • in object.Constructor('X', A) the second argument is used as the prototype, to use A as a parent constructor add an empty object as a third argument, i.e. 'object.Constructor('X', A, {})'
    (see: Constructor(..) / C(..) for more info)

Inheritance

//
//	  Base <--- Item <--- SubItem
//
var Base = object.Constructor('Base', {
    proto_attr: 'prototype attr value',

    get prop(){
        return 'propery value' },

    method: function(){
        console.log('Base.method()') },

    // initializer...
    __init__: function(){
        this.instance_attr = 'instance'
    },
})

var Item = object.Constructor('Item', Base, {
    method: function(){
        // ... 

        // call the "super" method...
        return object.parentCall(Item.prototype, 'method', this, ...arguments)
    },

    __init__: function(...args){
        // call the "super" method...
        object.parentCall(this.__init__, this, ...args)

        this.item_attr = 'instance attribute value'
    },
})

var SubItem = object.Constructor('SubItem', Item, {
    // ...
})

Callable instances

var Action = object.Constructor('Action',
    // constructor as a function...
    function(context, ...args){
        // return the instance...
        return this
    })

// a more flexible approach...
//
// This is the same as the above but a bit more convenient as we do 
// not need to use Object.assign(..) or object.mixinFlat(..) to define
// attributes and props.

var Action2 = object.Constructor('Action2', {
    __call__: function(context, ...args){
        // call the callable parent...
        return object.parentCall(Action2.prototype, '__call__', this, ...arguments)
    },
})


var action = Action()
var action2 = new Action2()

// the instances are now functions...
action()
action2()

In the above cases both the function constructor and the .__call__(..) method receive a context argument in addition to this context, those represent the two contexts relevant to the callable instance:

  • Internal context (this)
    This always references the instance being called
  • External context (context) This is the object the instance is called from, i.e. the call context (window or global by default)

If the prototype is explicitly defined as a function then it is the user's responsibility to call .__call__(..) method.

When calling the parent passing '__call__' will get the parent in both the function and .__call__(..) implementations, but extra care must be taken in passing the reference prototype to .parentCall(..), the instance is implemented as a proxy function that will pass the arguments to the implementation (i.e. this.constructor.prototype(..)) so this proxy function as well as the .constructor.prototype(..) are valid implementations and both will be retrieved by sources(this, '__call__'), values(this, '__call__') and by extension parent(this, '__call__') and friends, so this is another reason not to use this in the general case.

Notes:

  • The two approaches (function vs. .__call__(..)) will produce functionally identical but structurally different constructors/objects, the difference is in .prototype -- what is defined as the prototype is the prototype (POLS), so we get:

    • prototype function -> .prototype is that exact function object,
    • .__call__(..) -> .prototype is the object with the .__call__(..) method.

    The instance in both cases is a function wrapper that will proxy the call to the corresponding implementation. (this may change in the future)

  • Making an object callable does not guarantee that <obj> instanceof Function will be true, though typeof(<obj>) == 'function'will always work. To satisfy the instanceof Function test the prototype tree must be rooted in Function.

Mix-ins

Prototype-based mixin...

var utilityMixin = {
    utility: function(){
        // ...
    },
}

var Base = object.Constructor('Base') 

// normal instance prototype chain:
//	b -> Base.prototype -> .. 
//
var b = Base()

// mixin directly into the instance...
//
// now the prototype chain looks like this:
//	b -> mixinFlat({}, utilityMixin) -> Base.prototype -> ..
//
object.mixin(b, utilityMixin)

.mixin(..) will copy the contents of utilityMixin into the prototype chain between b and b.__proto__.

We can also remove the mixin...

o.mixout(b, utilityMixin)

The mixed-in data is removed iff a matching object is found in the chain with the same attributes as utilityMixin and with each attribute matching identity with the corresponding attribute in the mixin.

Constructor-based mixin...

var UtilityMixin = function(parent){
    return object.Constructor(parent.name + '+utils', parent, utilityMixin) }

var Mixed = object.Constructor('Mixed', UtilityMixin(Base), {
    // ...
})

var m = Mixed()

Notes:

  • It is not recommended to .mixin(..) into constructors directly, use .mixinFlat(..) instead.

Advanced usage

Low level constructor

var LowLevel = object.Constructor('LowLevel', {
    __new__: function(context, ...args){
        return {}
    },
})

The value .__new__(..) returns is used as the instance and gets linked to the prototype chain by the calling constructor's .__rawinstance__(..), the constructor then will call .__init__(..) if defined.

Note that .__init__(..) is called by the constructor and not by RawInstance(..) or .__rawinstance__(..).

Like function constructor and .__call__(..) this also has two contexts, but the internal context is different -- as it is the job of .__new__(..) to create an instance, at time of call the instance does not exist and this references the .prototype object.

The external context is the same as above.

Contexts:

  • Internal context (this)
    References the .prototype of the constructor.
  • External context (context) This is the object the instance is called from, i.e. the call context (window or global by default), the same as for function constructor and .__call__(..).

This has priority over the callable protocols above, thus the user must take care of both the function constructor and prototype.__call__(..) handling.

Extending the constructor

var C = object.Constructor('C', {
    // this will get mixed into the constructor C...

    constructor_attr: 123,

    constructorMethod: function(){
        // ...
    },

    // ...
}, {
    instanceMethod: function(){
        // get constructor data...
        var x = this.constructor.constructor_attr

        // ...
    },
    // ...
})

And the same thing while extending...

var D = object.Constructor('D', C, {
    // ...
}, {
    // ...
})

Inheriting from native constructor objects

var myArray = object.Constructor('myArray', Array, {
    // ...
})

All special methods and protocols defined by object.js except for .__new__(..) will work here without change.

For details on .__new__(..) and native .constructor(..) interaction see: Extending native .constructor(..)

Extending native .constructor(..)

Extending .constructor(..) is not necessary in most cases as .__init__(..) will do everything generally needed, except for instance replacement.

var myArray = object.Constructor('myArray', Array, {
    __new__: function(context, ...args){
        var obj = Reflect.construct(myArray.__proto__, args, myArray)

        // ...

        return obj
    },
})

Special methods

<object>.__new__(..)

Create new instance object.

<object>.__new__(<context>, ..)
    -> <instance>

This is called in the context of <constructor> as at time of call no instance exists yet.

<context> is the outer context of the call, i.e. the object from which <constructor> was referenced before it was called.

For more info see:

<object>.__init__(..)

Initialize the instance.

<object>.__init__(..)

Return value is ignored.

<object>.__call__(..)

Call the object.

<object>.__call__(<context>, ..)
    -> <result>

This is called in the context of <object>.

<context> is the outer context of the call, i.e. the object from which <object> was referenced before it was called.

For more info see: Callable instances

Components

Note that all of the following are generic and will work on any relevant JavaScript object.

For example, this will happily create a normal native array object ['a', 'b', 'c']:

var l = object.RawInstance(null, Array, 'a', 'b', 'c')

STOP / STOP(..)

Used in sources(..), values(..) and mixins(..) to stop the search before it reaches the top of the prototype chain.

Constructor(..) / C(..)

Define an object constructor

Constructor(<name>)
Constructor(<name>, <prototype>)
Constructor(<name>, <parent-constructor>, <prototype>)
Constructor(<name>, <parent-constructor>, <constructor-mixin>, <prototype>)
Constructor(<name>, <constructor-mixin>, <prototype>)
    -> <constructor>

Constructor(..) essentially does the following:

  • Creates a constructor function,
  • Sets constructor .name and .toString(..) for introspection,
  • Creates .__rawinstance__(..) wrapper to RawInstance(..)
  • Sets constructor .__proto__, .prototype and .prototype.constructor,
  • Mixes in constructor-mixin if given.

The resulting constructor function when called will:

  • call constructor's .__rawinstance__(..) if defined or RawInstance(..) to create an instance,
  • call instance's .__init__(..) if present.

Note that Constructor(<name>, <prototype>) is intentionally set as default instead of having the parent-constructor as the last argument, this is done for two reasons:

  • The main cause to inherit from a constructor is to extend it,
  • In real code the Constructor(<name>, <prototype>) is more common than empty inheritance.

Shorthand to Constructor(..)

C(<name>, ..)
    -> <constructor>

Constructor(..) / C(..) and their products can be called with and without new.

create(..) / Constructor.create(..)

Create a new object from the given

create(<base>)
    -> <obj>

For functions we can set .name

create(<name>, <base-func>)
    -> <func>

This is similar to Object.create(..) but handles callables correctly, i.e. if <base> is a callable then <obj> will also be callable.

<obj> respects the call protocol, and will call <obj>.__call__(..) if defined.

sources(..) / Constructor.sources(..)

Get sources for attribute

sources(<object>, <name>)
sources(<object>, <name>, <callback>)
    -> <list>
callback(<source>, <index>)
    -> STOP 
    -> STOP(<value>)
    -> undefined
    -> <value>

The callback(..) controls the output of sources(..) by returning one of the following:

  • object.STOP
    This will make sources(..) stop and return the <list> up to and including the object that triggered the stop.
  • object.STOP(<value>)
    Same as returning object.STOP but will put the <value> at the end of the returned list instead of the input object.
  • undefined
    Add the object triggering callback(..) in <list> as-is and continue.
  • array
    The containing values will be merged into the result list and continue. This is a way to either skip an object by returning [] or multiple values instead of one.
  • <value>
    Add to the resulting <list> as-is instead of the object triggering callback(..) and continue.

Special case: get callable implementations

sources(<object>, '__call__', ..)
    -> <list>

This will get the callable implementations regardless of the actual implementation details, i.e. both function prototype or .__call__(..) methods will be matched.

values(..) / Constructor.values(..)

Get values for attribute in prototype chain

values(<object>, <name>)
values(<object>, <name>, <callback>)
    -> <list>
callback(<value>, <source>, <index>)
    -> STOP 
    -> undefined
    -> <value>

Get property descriptors for attribute in prototype chain

values(<object>, <name>, true)
values(<object>, <name>, <callback>, true)
    -> <list>
callback(<descriptor>, <source>)
    -> STOP 
    -> STOP(value)
    -> undefined
    -> <value>

Special case: get callable implementations

values(<object>, '__call__', ..)
    -> <list>

This will return the callable objects themselves or the value of .__call__.

See sources(..) for docs on callback(..) and special cases.

parent(..) / Constructor.parent(..)

Get parent attribute value or method

parent(<prototype>, <name>)
    -> <parent-value>
    -> undefined

It is recommended to use the relative<constructor>.prototype as <prototype> and in turn not recommended to use this or this.__proto__ as they will not provide the appropriate reference point in the prototype chain for the current method and may result in infinite recursion.

For access to parent methods the following special case is better.

parent(<method>, <this>)
    -> <parent-method>
    -> undefined

Edge case: The parent(<method>, ..) has one potential pitfall -- in the rare case where a prototype chain contains two or more references to the same method under the same name, parent(..) can't distinguish between these references and will always return the second one.

Special case: get the parent callable implementation

parent(<prototype>, '__call__')
    -> <parent-value>
    -> undefined

See sources(..) for more info on the special case.

parentProperty(..) / Constructor.parentProperty(..)

Get parent property descriptor

parentProperty(<prototype>, <name>)
    -> <prop-descriptor>
    -> undefined

parentCall(..) / Constructor.parentCall(..)

Get parent method and call it

parentCall(<prototype>, <name>, <this>)
    -> <result>
    -> undefined

parentCall(<method>, <this>)
    -> <result>
    -> undefined

Special case: call the parent callable implementation

parentCall(<prototype>, '__call__', <this>)
    -> <result>
    -> undefined

See parent(..) and sources(..) for more details.

parentOf(..) / childOf(..) / related(..) and Constructor.*(..) variants

Test if a is parent of b and/or vice-versa.

parentOf(<parent>, <child>)
    -> <bool>

childOf(<child>, <parent>)
    -> <bool>

related(<a>, <b>)
    -> <bool>

These are similar to instanceof but will test if the two objects are in the same prototype chain and in case of parentOf(..)/childOf(..) in what order.

RawInstance(..)

Make a raw (un-initialized) instance

RawInstance(<context>, <constructor>, ..)
    -> <object>

RawInstance(..) will do the following:

  • Create an instance object
    • get result of .__new__(..) if defined, or
    • if prototype is a function or .__call__(..) is defined, create a wrapper function, or
    • if constructor's .__proto__ has a .__rawinstance__(..) use it to create an instance, or
    • if constructor's .__proto__ is a function (constructor) use it to create an instance, or
    • use {}.
  • Link the object into the prototype chain

Un-initialized means this will not call .__init__(..)

RawInstance(..) can be called with and without new.

Mixin(..)

Create a mixin wrapper.

Mixin(<name>, <obj>, ..)
    -> <mixin>

This will create a more convenient <mixin> object.

The following two are the same

var mixin = {
    // ...
}

var obj = mixinFlat({
    // ...
}, mixin)

and

var mixin = Mixin('mixin', {
    // ...
})

var obj = mixin('flat', {
    // ...
})

The former approach is better suited for inline mixing in, where one could use Object.assign(..) while the later is more convenient for working with library and reusable mixin object as it is more readable and more centralized.

This also makes combining mixins simpler

var A = Mixin('A', {
    // ...
})

var B = Mixin('B', {
    // ...
})

// this is a combination of A and B...
var C = Mixin('C', A, B, {
    // NOTE: this "block" is optional...
    // ...
})

Note that for multiple mixins used in Mixin(..) as well as in mixin(..)/mixinFlat(..), mixins from right to left, e.g. in the above example B will overwrite intersecting data in A, ... etc.

<mixin>(..)

Mixin into <target> as a prototype

<mixin>(<target>)
<mixin>('proto', <target>)
    -> <target>

Mixin into <target> directly (flatly)

<mixin>('flat', <target>)
    -> <target>

These are similar to using mixin(..) or mixinFlat(..) respectively.

<mixin>.mode

Sets the default mode for <mixin>(..).

Can be:

<mixin>.mixout(..)

Remove <mixin> from <target>

<mixin>.mixout(<target>)
    -> <target>

This is the same as mixout(..)

<mixin>.isMixed(..)

Check if <mixin> is mixed into <target>

<mixin>.isMixed(<target>)
    -> <bool>

This is the same as hasMixin(..)

mixin(..) / Mixin.mixin(..)

Mixin objects into a prototype chain

mixin(<base>, <object>, ..)
    -> <base>

This will link the base .__proto__ to the last mixin in chain, keeping the prototype visibility the same.

This will copy the content of each input object without touching the objects themselves, making them fully reusable.

It is not recommended to .mixin(..) into constructors directly, use .mixinFlat(..) instead.

mixinFlat(..) / Mixin.mixinFlat(..)

Mixin contents of objects into one base object

mixinFlat(<base>, <object>, ..)
    -> <base>

This is like Object.assign(..) but copies property descriptors rather than property values.

Also like Object.assign(..) this will overwrite attribute values in <base>.

mixout(..) / Mixin.mixout(..)

Remove the first match matching input mixin from base of base

mixout(<base>, <object>, ..)
mixout(<base>, 'first', <object>, ..)
    -> <base>

Remove all occurrences of each matching input mixin from base

mixout(<base>, 'all', <object>, ..)
    -> <base>

This is the opposite of mixin(..)

mixins(..) / Mixin.mixins(..)

Get matching mixins

mixins(<base>, <object>)
mixins(<base>, [<object>, ..])
mixins(<base>, <object>, <callback>)
mixins(<base>, [<object>, ..], <callback>)
    -> list
callback(<match>, <object>, <parent>)
    -> STOP 
    -> undefined
    -> <value>

See sources(..) for docs on callback(..)

hasMixin(..) / Mixin.hasMixin(..)

Check if base object has mixin

hasMixin(<base>, <mixin>)
    -> <bool>

Utilities

normalizeIndent(..) / normalizeTextIndent(..) / doc / text

Align code to shortest leading white-space

normalizeIndent(<text>)
normalizeIndent(<text>, <tab-size>)
normalizeIndent(<text>, <tab-size>, <leading-tabs>)
    -> <text>

This is used to format .toString(..) return values for nested functions to make source printing in console more pleasant to read.

tab_size defaults to object.TAB_SIZE

leading_tabs defaults to object.LEADING_TABS

A shorthand to normalizeIndent(..) optimized for text rather than code

normalizeTextIndent(..)
    -> <text>

This ignores object.LEADING_TABS and leading_tabs is 0 by default.

doc and text are template string versions of normalizeIndent(..) and normalizeTextIndent(..) respectively.

deepKeys(..) / Constructor.deepKeys(..)

deepKeys(<obj>)
    -> <keys>
deepKeys(<obj>, <stop>)
    -> <keys>

This is like Object.keys(..) but will get the keys from the whole prototype chain or until <stop> if given.

match(..) / Constructor.match(..)

Test if the two objects match in attributes and attribute values

match(<base>, <obj>)
    -> <bool>

This relies on first level object structure to match the input object, for a successful match one of the following must apply:

  • object are identical

or:

  • typeof matches and,
  • attribute count matches and,
  • attribute names match and,
  • attribute values are identical.

Non-strict match

match(<base., <obj>, true)
    -> <bool>

Like the default case but uses equality instead of identity to match values.

matchPartial(..) / Constructor.matchPartial(..)

matchPartial(<base>, <obj>)
    -> <bool>

// non-strict version...
matchPartial(<base>, <obj>, true)
    -> <bool>

Like .match(..) but will check for a partial match, i.e. when obj is a non-strict subset of base.

Limitations

Can not mix unrelated native types

At this point we can't mix native types, for example it is not possible to make a callable Array object...

This is not possible in current JavaScript implementations directly as most builtin objects rely on "hidden" mechanics and there is no way to combine or inherit them.

To illustrate:

// produces an Array that looks like a function but does not act like one...
var a = Reflect.construct(Array, [], Function)

// creates a function that looks like an array... 
var b = Reflect.construct(Function, [], Array)

So these will produce partially broken instances:

var A = object.Constructor('A', Array, function(){ .. })

var B = object.Constructor('B', Array, {
    __call__: function(){ .. },
})

Essentially this issue and the inability to implement it without emulation, shows the side-effects of two "features" in JavaScript:

  • lack of multiple inheritance
  • hidden protocols/functionality (namely: calls, attribute access)

Still, this is worth some thought.

More

For more info see the source...

License

BSD 3-Clause License

Copyright (c) 2016-2021, Alex A. Naanou,
All rights reserved.