Oops - object-oriented programming simplified
A small library that makes object-oriented programming in javascript a little simpler.
Installation
npm install node-oops
In a web browser...
Rationale
The ECMAScript 5 syntax for defining object properties is a bit cumbersome. I'm NOT saying its unusable, not at all... but it does require some concentration, especially when revisiting code written more than a week ago. oops
was born out of a few helper methods I put together to make object-oriented javascript a bit more understandable and clear. It allows you to define what an object is, has and does.
Warning #1
Before proceeding you should understand that as I code primarily for the server-side, I purposely chose NOT to support the extremely loose typing that infects the browser-side. Mind you, I enjoy some of those patterns -- they are powerful techniques when used judiciously -- but in order to keep this library small and focused I stuck to bare-bones, classic, type definition stuff. If you find yourself defining types using the ECMAScript 5 Object.defineProperty(ies)
method, and customizing a property's CEWability (configurable, enumerable, writable), then you're already in the paradigm I'm talking about. If you tend to use object-merge style inheritance rather than classic prototypal inheritance then this library won't be very useful to you.
Compatability
Warning #2
oops
purposely extends Object.prototype
and Function.prototype
, and as with most libraries that do so, it is purely for convenience and the semantic sugar. That said, oops
is well-behaved. It detects whether its two additions (Object.prototype.defines
and Function.prototype.inherits
) are objstructed and if so does not replace them. It also supplies a noConflict
method that will restore your environment and run oops in pristine
mode.
Warnings aside, oops
is nothing more than a small wrapper over ECMAScript 5's object definition methods. Ultimately it turns right around and calls Object.defineProperty
after composing the appropriate descriptor
. It is entirely compatible with well-understood, object-oriented practice in javascript and can be mixed and matched successfully with types defined using prototypal inheritance.
Warning #3
As already divulged, I code primarily on the server-side, nodejs
mostly, so oops
is entirely node compatible. This means that you can interchangeably use util.inherits
and oops.inherits
, and (function My(){}).inherits
. All three of these use identical code to establish the prototype chain and each pokes super_
onto your type/function. I put this warning here so that if you're not already familiar with inheritance the way the node community does it you can go acquire that knowledge before continuing.
Example
Ponder this object definition, it will take just a minute to comprehend in its entirety.
var oops = dbc = oopsdbc;/*** A classic parallel-programming future variable (oops).** Create one on an existing value and it is available immediately* via the `get` method.** Create one without a value and either poll it using the `has`* method or call `get` with a callback to get notified when the* value is available.** The value may be written only once via the `set` method.*/ { var _val = val _callbacks; { _val = val; if _callbacks var i len = _callbackslength; fori = 0; i < len; i++ var cb = _callbacksi; ; delete _callbacks; } thisdefinesenumerable method { return typeof _val !== 'undefined'; } method { var v = _val; if cb if typeof v === 'undefined' _callbacks; else ; return v; }; if typeof _val === 'undefined' _callbacks = ; thisdefinesenumerable method { ; if typeof val !== 'undefined' ; }; }
This is the very same object, defined using standard ECMAScript 5 Object.defineProperty(ies)
methods:
/*** A classic parallel-programming future variable (ECMA).** Create one on an existing value and it is available immediately* via the `get` method.** Create one without a value and either poll it using the `has`* method or call `get` with a callback to get notified when the* value is available.** The value may be written only once via the `set` method.*/ { var _val = val _callbacks; { _val = val; if _callbacks var i len = _callbackslength; fori = 0; i < len; i++ var cb = _callbacksi; ; delete _callbacks; } Object; if typeof _val === 'undefined' _callbacks = ; Object; }
Scroll back and forth between the two -- not that either is too complicated, and oops
is certainly not novel, but the semantic allows you to quickly determine the object's characteristics.
Use
Importing
var oops = ;
Specifying Descriptor Properties
The default descriptor properties used by oops
are identical to ECMAScript 5. If you don't specify otherwise then configurable
, enumerable
, and writable
are false.
Therefore, if you don't specify otherwise:
- The property can't be reconfigured,
- The property won't be discovered by methods that enumerate the object's properties,
- The property's value can't be modified.
Configurable - allowing later redefinition
var oops = assert = ; var initial = 'its an immutable string value' reconfigured = 'now a function' finale = 'now a property' it = {}; // give it a valueitdefinesconfigurablevalue'prop' initial; // verify it...assert; // benign assignment because prop is not writableitprop = reconfigured;assert; // and the prop is not enumerable...assert; // ok, can't write it but we can reconfigure it...itdefinesconfigurablemethod'prop' { return reconfigured; }; // note we're now treating it as a method...assert; // how about reconfiguring it as a property...itdefinesconfigurable; // now, treat it as a value again...assert;
If you don't define a property as configurable then redefining throws TypeError
as proved in the next example. In this way you can close a property definition.
Enumerable - discoverable
var oops = assert = ; var value = 'your name here' it = {} other = {}; // give it an enumerable propitdefinesenumerablevalue'prop' value; // verify...assert;// and we can see it by enumerating...; try // while we're here, prove it is non-configurable itdefinesvalue'prop' 'some other value'; assert; catche ; // Cannot redefine property: prop // give other a prop that is NOT enumerableotherdefinesvalue'prop' value; // verify...assert;// and we CAN NOT see it by enumerating...;
If you're practiced in other OO languages you may think that we're talking about visibility here but we're not. In javascript things are visible if you know how to refer to them. To make something truly invisible you'll have to use a closure scope [which is out of scope for this readme].
One notable place where enumerability comes into play is when copy-constructing, or extending objects [re. extend
]. Most well-behaved extend methods will respect the enumerability of properties.
JSON serialization is another good example, properties that are not enumerable won't get serialized by standard libraries.
Writable
Well, this one is self explanitory, but here's a proof.
var oops = assert = ; var value = 'your name here' updated = 'Gilbert Snodgraph' it = {} not = {}; // give it a writable propitdefineswritablevalue'prop' value; // verify...assert; // write it...itprop = updated; // verify...assert; // now for one that is not writable...notdefinesvalue'prop' value; // verify...assert; // write it...notprop = updated; // verify the value did not change...assert;
Configurable, Enumerable, Writable - all together now
When defining object properties, you can stack the descriptors and it does what you expect.
mydefinesenumerablewritablevalue'prop' {};// my.prop is enumerable and writable
Further, once you've established CEW you can chain the definitions:
var oops = dbc = oopsdbc assert = ; { // first CEW descriptor thisdefinesenumerablewritable value'firstName' first || '' value'lastName' last || ''; // second CEW descriptor - note that accessing the // defines property inializes a new descriptor. thisdefinesenumerable value'middleNames' middles || ;} // We can define directly against the function/ctor.// Make the fullName configurable.Persondefinesconfigurableenumerable ; var person = "Bilbo" "Baggins" "the Thief"; // verify...assert;
Defining Values
A value is any property that is not backed by a user-supplied getter or setter. All values must be explicitly named because there isn't a reasonable way to infer the name. Since there are so many examples above I'll keep this short:
mydefinesvalue'name' value;
Defining Properties
There are options when creating properties. You can make them read-only by providing just the getter, or read-write by providing a getter and a setter. Further, the property's name can be inferred from the name of the getter.
Read-only
var my = {} _name = "My name"; // let oops infer the property name from the getter's name...mydefinesenumerable;
// ...or specify the name of the property...mydefinesenumerable;
Read-write
var my = {} _name = "My name"; mydefinesenumerable;
// ...or...mydefinesenumerable;
Personally I nearly always let oops
infer the name. It saves some typing and allows
me to define the functions in one scope, then reuse them as methods when defining types.
NOTE: currently there is no support for defining write-only properties. To accomplish this rarity you'll have to revert to Object.defineProperty
like this:
var my = {} _name = "My name"; Object
Defining Methods
Defining methods is similar to defining properties. oops
will infer the name of the method from the function provided, alternately you can specify it.
The following is a more involved example. I've mentioned that I prefer letting oops
infer the names of things I define. In this example I've established a scope in order to illustrate one form of encapsulation, and within the scope I define independent functions later assigned as methods. Personally, I appreciate the separation when I come back to code after some time away -- but of course you're free to use a style that works for you.
the illustrated method
definition is about half way down the example
var oops = dbc = oopsdbc util = ; var piehole = { var it = {} belly = {}; { var typ = typeof what; if typeof bellytyp === 'undefined' bellytyp = ; bellytyp; return what; } { if typeof wherelog == 'function' var keys = Object; keys; } // define the methods, we'll go grade-school and rename the second method... itdefinesenumerable methodstuff method'barf' puke ; return it;}; { console;} // now stuff our pie hole...;;;;;;; // that ought to do it... now wait just a second...;
No Conflict
oops
supports a no-conflict mode. In this mode you must explicitly create your own instances of the Define
type... my personal practice is to reassign the factory method create
to a local variable that mimics the basic semantics of oops
:
var oops = defines = oopscreate; { value'greeting' greeting;} { Greetersuper_;}oops; enumerable method { console; }; var bob = "Good day";bob;
API
Methods
dbc
- a light-weight desing-by-contract method (enforces one or more required conditions).create
- Factory method foroops.Define
. Useful inno-conflict
mode.inherits
- same as node'sutil.inherits
, redefined to support contexts other than node.noConflict
- resets the javascript environment topristine
mode.obstructed
- reports whether a critical feature is obstructed (defines
orinherits
)
Types
Define
- used to define an object's characteristics.- Properties
configurable
- establishes the subsequent definition(s) as configurable.enumerable
- establishes the subsequent definition(s) as enumerable.writable
- establishes the subsequent definition(s) as writable.
- Methods
method
- defines a method.property
- defines a property.value
- defines a value.
- Properties
ContractError
- thrown bydbc
when one or more conditions is not truthy.
Environmental
Object.prototype.defines
- A read-only property; constructs an instance ofDefine
over the object on which it is invoked.Function.prototype.inherits
- A single-use method for establishing a type's inheritance hierarchy. Seeinherits
.