object.js/README.md

747 lines
18 KiB
Markdown
Raw Normal View History

# 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](https://en.wikipedia.org/wiki/Principle_of_least_astonishment)
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:
<table border="0" width="100%">
<tr valign="top">
<td width="50%">
_object.js_
```javascript
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.
</td>
<td>
_ES6_
```javascript
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.
</td>
</tr>
</table>
## Contents
- [object.js](#objectjs)
- [Contents](#contents)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Inheritance](#inheritance)
- [Callable instances](#callable-instances)
- [Mix-ins](#mix-ins)
- [Advanced usage](#advanced-usage)
- [Low level constructor](#low-level-constructor)
- [Extending the constructor](#extending-the-constructor)
- [Inheriting from native constructor objects](#inheriting-from-native-constructor-objects)
- [Extending native `.constructor(..)`](#extending-native-constructor)
- [Components](#components)
- [`sources(..)`](#sources)
- [`parent(..)`](#parent)
- [`parentProperty(..)`](#parentproperty)
- [`parentCall(..)`](#parentcall)
- [`mixin(..)`](#mixin)
- [`mixins(..)`](#mixins)
- [`hasMixin(..)`](#hasmixin)
- [`mixout(..)`](#mixout)
- [`mixinFlat(..)`](#mixinflat)
- [`RawInstance(..)`](#rawinstance)
- [`Constructor(..)` / `C(..)`](#constructor--c)
- [Utilities](#utilities)
- [`normalizeIndent(..)` / `normalizeTextIndent(..)`](#normalizeindent--normalizetextindent)
- [`match(..)`](#match)
- [Limitations](#limitations)
- [Can not mix unrelated native types](#can-not-mix-unrelated-native-types)
- [License](#license)
## Installation
```shell
$ npm install ig-object
```
Or just download and drop [object.js](object.js) into your code.
## Basic usage
Include the code, this is compatible with both [node's](https://nodejs.org/) and
[RequireJS'](https://requirejs.org/) `require(..)`
```javascript
var object = require('ig-object')
```
Create a basic constructor...
```javascript
// 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...
```javascript
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(..)`](#constructor--c) for more info)
### Inheritance
```javascript
//
// Base <--- Item <--- SubItem
//
2019-07-17 00:26:39 +03:00
var Base = object.Constructor('Base', {
proto_attr: 'prototype attr value',
get prop(){
return 'propery value' },
method: function(){
console.log('Base.method()') },
2019-07-17 00:26:39 +03:00
// initializer...
__init__: function(){
this.instance_attr = 'instance'
},
2019-07-17 00:26:39 +03:00
})
var Item = object.Constructor('Item', Base, {
__init__: function(){
// call the "super" method...
object.parentCall(this.prototype.__init__, this)
this.item_attr = 'instance attribute value'
},
2019-07-17 00:26:39 +03:00
})
var SubItem = object.Constructor('SubItem', Item, {
// ...
})
```
### Callable instances
```javascript
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` or `global` 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...
```javascript
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...
```javascript
o.mixout(b, utilityMixin)
```
The mixed-in data is removed iff a [matching](#match) 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...
```javascript
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
```javascript
var LowLevel = object.Constructor('LowLevel', {
__new__: function(context, ...args){
return {}
},
})
```
Like [_function constructor_ and `.__call__(..)`](#callable-instances)
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__(..)`.
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
```javascript
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...
```javascript
var D = object.Constructor('D', C, {
// ...
}, {
// ...
})
```
### Inheriting from native constructor objects
```javascript
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 native `.constructor(..)`
Extending `.constructor(..)` is not necessary in most cases as
`.__init__(..)` will do everything generally needed, except for instance
replacement.
```javascript
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']`:
```javascript
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 `{}`.
- Link the object into the prototype chain
_Un-initialized_ means this will not call `.__init__(..)`
### `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>
```
## 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:
```javascript
// 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:
```javascript
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
[BSD 3-Clause License](./LICENSE)
2019-07-17 00:26:39 +03:00
Copyright (c) 2019, Alex A. Naanou,
All rights reserved.
<!-- vim:set ts=4 sw=4 spell : -->