Package Exports
- ig-object
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ig-object) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
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
},
})
|
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
}
}
|
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 useA
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, {
__init__: function(){
// call the "super" method...
object.parentCall(this.prototype.__init__, this)
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){
return this
},
})
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
orglobal
by default)
If the prototype is explicitly defined as a function then it is the
user's responsibility to call .__call__(..)
method.
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)
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()
Advanced usage
Low level constructor
var LowLevel = object.Constructor('LowLevel', {
__new__: function(context, ...args){
return {}
},
})
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
orglobal
by default), the same as for function constructor and.__call__(..)
.
The value .__new__(..)
returns is used as the instance and gets linked
in the prototype chain.
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
},
})
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')
sources(..)
Get sources for attribute
sources(<object>, <name>)
sources(<object>, <name>, <callback>)
-> <list>
callback(<source>)
-> 'stop' | false
-> undefined
parent(..)
Get parent attribute value or method
parent(<prototype>, <name>)
-> <parent-value>
-> undefined
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.
parentProperty(..)
Get parent property descriptor
parentProperty(<prototype>, <name>)
-> <prop-descriptor>
-> undefined
parentCall(..)
Get parent method and call it
parentCall(<prototype>, <name>, <this>)
-> <result>
-> undefined
parentCall(<method>, <this>)
-> <result>
-> undefined
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.
mixins(..)
Get matching mixins
mixins(<base>, <object>)
mixins(<base>, [<object>, ..])
mixins(<base>, <object>, <callback>)
mixins(<base>, [<object>, ..], <callback>)
-> list
callback(<match>, <object>, <parent>)
-> 'stop' | false
-> undefined
hasMixin(..)
Check if base object has mixin
hasMixin(<base>, <mixin>)
-> <bool>
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(..)
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.
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__
is a function (constructor) use it to create an instance, or - use
{}
.
- get result of
- Link the object into the prototype chain
Un-initialized means this will not call .__init__(..)
RawInstance(..)
can be called with and without new
.
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 toRawInstance(..)
- 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 orRawInstance(..)
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(..)
can be called with and without new
.
Utilities
normalizeIndent(..)
/ normalizeTextIndent(..)
Align code to shortest leading white-space
normalizeIndent(<text>)
normalizeIndent(<text>, <tab-size>)
normalizeIndent(<text>, <tab-size>, <keep-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
keep_tabs
defaults to object.KEEP_TABS
A shorthand to normalizeIndent(..)
optimized for text rather than code
normalizeTextIndent(..)
-> <text>
This ignores object.KEEP_TABS
and keep_tabs
is 0 by default.
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.
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.
License
Copyright (c) 2019, Alex A. Naanou,
All rights reserved.