trope
v1.1.2
Published
Object-oriented JS for everyone - Take full advantage of JavaScript prototypes plus extras (private state, multiple inheritance, and super functions).
Downloads
34
Maintainers
Readme
trope.js
Overview
Trope is a simple interface for JavaScript inheritance that provides some extra, useful capabilities. At the core of this tool are prototypes which are used to maintain private state, allow access to super functions, and create objects with multiple inheritance. It's easy to start integrating with most projects since native JS constructors, object prototypes and ES6 classes are already valid Trope definitions.
API Documentation
Features
- Simplified object inheritance
- Private object state
- Super functions
- Multiple inheritance
- Compatible with native JS
Examples
- Object factories
- Private properties
- Inheritance
- Multiple Inheritance
- Native JS Compatibility
- Selfish Definitions
Object factories
A simple object factory.
var Greeter = Trope({
setName: function (name) {
this.name = name;
},
sayHello: function () {
return 'Hello, ' + this.name + '!';
}
});
var greeter = Greeter.create();
greeter.setName('Bertrand');
greeter.sayHello(); // 'Hello, Bertrand!'
Pass in an initializer function, to set state upon creation.
var Greeter = Trope(function (name) {
this.setName(name);
},{
setName: function (name) {
this.name = name;
},
sayHello: function () {
return 'Hello, ' + this.name + '!';
}
});
var greeter = Greeter.create('Bertrand');
greeter.name; // 'Bertrand'
greeter.sayHello(); // 'Hello, Bertrand!'
New objects can also be created by calling the Trope as a function with or without the new
operator.
greeter = Greeter.create('Bertrand');
greeter = Greeter('Bertrand');
greeter = new Greeter('Bertrand');
Private properties
privacy
mode can be enabled to prevent object properties from being accessed by an outside context.
var Greeter = Trope.privacy(function (name) {
this.setName(name);
},{
setName: function (name) {
this.name = name;
},
sayHello: function () {
return 'Hello, ' + this.name + '!';
}
});
var greeter = Greeter.create('Bertrand');
greeter.name; // undefined
greeter.sayHello(); // 'Hello, Bertrand!'
Public properties can still be set in privacy
mode by setting them on this.exports
.
var Greeter = Trope.privacy(function (name) {
this.setName(name);
},{
setName: function (name) {
this.name = name;
this.exports.exposedName = name;
},
sayHello: function () {
return 'Hello, ' + this.name + '!';
}
});
var greeter = Greeter.create('Bertrand');
greeter.name; // undefined
greeter.exposedName; // 'Bertrand'
greeter.sayHello(); // 'Hello, Bertrand!'
Trope.privacy
is the easiest way to define a trope in privacy mode, but you also be explicit in your definition with Trope.define
:
var Greeter = Trope.define({
privacy: true,
init: function (name) {
this.setName(name);
},
prototype: {
setName: function (name) {
this.name = name;
this.exports.exposedName = name;
},
sayHello: function () {
return 'Hello, ' + this.name + '!';
}
}
});
See how private properties are implemented in Trope.
Inheritance
Start with an existing definition
var Animal = Trope({
getLongName: function () {
return 'Animalia';
}
});
Inherit from base definitions by chaining off of them with turn
, or proto
, or extend
and passing in a new definition. Functions can be overloaded and the original can be called with this.super()
(or this._super()
).
var Vertebrate = Animal.turn({
getLongName: function () {
return this.super() + ' Chordata';
}
});
var Mammal = Vertebrate.turn({
getLongName: function () {
return this.super() + ' Mammalia';
}
});
var mammal = Mammal.create();
mammal.getLongName(); // 'Animalia Chordata Mammalia'
The native instanceof
operator works as expected since Trope inheritance is based on JavaScript's native inheritance mechanism.
mammal instanceof Mammal;
mammal instanceof Vertebrate;
mammal instanceof Animal;
See how this.super is implemented in Trope.
Multiple Inheritance
First we define two unrelated Tropes Logger
and EventEmitter
.
var Logger = Trope({
log: function (msg) {
console.log(msg);
}
});
var EventEmitter = Trope({
on: function (name, handler) {
if (!this.eventMap) {
this.eventMap = {};
}
if (!this.eventMap[name]) {
this.eventMap[name] = [];
}
this.eventMap[name].push(handler);
},
fire: function (name, data) {
if (this.eventMap && this.eventMap[name]) {
this.eventMap[name].forEach(function (handler) {
handler.call(this, data);
}.bind(this));
}
}
});
They can be combined to create a new Trope Eventedlogger
.
var EventedLogger = Trope([Logger, EventEmitter]);
var eventedLogger = EventedLogger.create();
eventedLogger.on('logme', function (msg) {
eventedLogger.log('LOGME: ' + msg);
});
eventedLogger.fire('logme', 'hello'); // logs 'LOGME: hello'
eventedLogger.fire('logme', 'world'); // logs 'LOGME: world'
Multiple inheritance can often result in some interesting combinations.
For example, define a simple Cat
.
// Define a `Cat`
var Cat = Trope(function (name) { // init function
this.name = name;
}, {
vocalize: function () {
return 'Meow!';
}
});
var cat = Cat.create('Raja');
cat.vocalize(); // 'Meow!'
Now inherit from Logger
, EventEmitter
, and Cat
while passing in a new definition to create something completely different.
var LoggingEventedCat = Trope([Logger, EventEmitter, Cat], function (name) { // init function
this.super.as(Cat)(name);
this.on('meow', function (sound) {
console.log(this.name + ': ' + sound);
});
},{ // overload the `vocalize` method inherited from `Cat`
vocalize: function (sound) {
sound = sound || this.super.as(Cat)();
this.fire('meow', sound);
}
});
var loggingEventedCat = LoggingEventedCat.create('Raja');
loggingEventedCat.vocalize(); // logs 'Raja: Meow!'
loggingEventedCat.vocalize('Purr...'); // logs 'Raja: Purr...'
Yes, defining a LoggingEventedCat
is entirely possible in Trope, however the usefulness of these composites is at the discretion of the developer.
See how multiple inheritance and this.super are implemented in Trope.
Native JS Compatibility
Trope is completely compatibile with native JS because JavaScript constructors are valid Trope definitions! This makes it easy to start using it without having to modify an existing code base.
// Native JS constructor Shape
function Shape (sides) {
this.sides = sides;
}
Shape.prototype.getSides = function () {
return this.sides;
};
var triangle = new Shape(3);
triangle instanceof Shape; // true
triangle.getSides(); // 3
Shape
can be wrapped with Trope and immediately treated as any other Trope to create a Shape
object. instanceof
still works since Shape.prototype
is part of triangle
's prototype chain.
var triangle = Trope(Shape).create(3);
triangle instanceof Shape; // true
Wrapping Shape
in a Trope gives it access to the same features available to any other Trope. Chain off of it to add new functionality or make it more specific.
var Quadrilateral = Trope(Shape).turn(function (opts) {
this.sides = 4;
});
var quadrilateral = Quadrilateral.create();
quadrilateral instanceof Quadrilateral;
quadrilateral instanceof Shape;
quadrilateral.getSides(); // 4
Selfish Definitions
Trope allows for different styles in defining prototypes, classes, or factories. selfish
is a style where each method can be explicitly passed the object reference as the first parameter. This is useful if you find yourself typing var self = this
or var that = this
at the beginning of every method. It's also a style that Python developers will be very familiar with (Python classes).
var Greeter = Trope.selfish({
__init__: function (self, name) {
self.name = name;
},
setName: function (self, name) {
self.name = name;
},
sayHello: function (self) {
return 'Hello, ' + self.name + '!';
}
});
var greeter = Greeter('Bertrand');
greeter.sayHello(); // "Hello, Bertrand!"
greeter.setName('Russell');
greeter.sayHello(); // "Hello, Russell!"
Note that Trope.selfish
will just set the selfish
flag on the Trope definition and accept the same arguments as the Trope
or Trope.interpret
functions. An equivalent definition with Trope.define
could look like this:
var Greeter = Trope.define({
selfish: true,
constructor: function Greeter (self, name) {
self.name = name;
},
prototype: {
setName: function (self, name) {
self.name = name;
},
sayHello: function (self) {
return 'Hello, ' + self.name + '!';
}
}
});
var greeter = Greeter('Bertrand');
greeter.sayHello(); // "Hello, Bertrand!"
greeter.setName('Russell');
greeter.sayHello(); // "Hello, Russell!"
Description
Another OO Lib, seriously?
I know that OO libs are somewhat of a...well trope. The reason is because different people have different, sometimes strongly-held beliefs about how object-oriented programming works and what it should look like. As the author, I am not very opinionated on this subject and feel you should be able to use whatever paradigm you happen to subscribe to. The purpose of Trope is not to change your way of thinking, but to provide a tool that can make OO JavaScript a lot easier to code, review, and test.
But traits, classes, mixins, monads, gonads...
Yes those are all great abstractions (except maybe gonads), and you should use each of them where appropriate. Trope doesn't claim to follow the one true paradigm, but it does make it simple to quickly code up object factories that play nice with native JS, use inheritance, respect private members, call overloaded functions, and do multiple inheritance.
Feature Details
Private object state
Trope takes a slightly unique approach to implementing privacy in objects. Existing libraries and patterns mostly use logic wrapped in a closure to prevent external access. Trope combines this with access to an exclusive part of the prototype chain to maintain private state with hidden properties on the created object.
Normally, objects will have a prototype chain similar to below where the only reference to the object is the the HEAD of the chain, in this case {public}
where the object properties can be referenced.
|
V
{public} -> {proto} -> {object} -> null
Trope creates another acting head to this chain ({private}
) which can only be accessed by methods inside the definition and not from any outside context.
|
V
{private} -> {public} -> {proto} -> {object} -> null
The result is an object with real private members rather than just some private state on an object defined in a closure. It may also give inheriting definitions access to this private context, elevating these members to a protected status. see example.
Access to Super Methods
Calling super functions in native JS usually involves code like SuperName.prototype.methodName.call(this, arg1, ...)
. With Trope, this.super()
is smart enough to know which is the correct super function of the currently executing context. This makes your code a lot easier to read and write. It also allows the developer to create new links to the middle of inheritance chains without having to modify references when calling super methods. see example.
When inheriting from many different parents, you can reference any of them with this.super.as
allowing you to call masked functions that are not members of the immediate parent. see example.
Multiple Inheritance
Multiple Inheritance is not a feature of the JavaScript language, which is a good thing! The single inheritance restriction eliminates a lot of the complexity and ambiguity involved with allowing objects to inherit from various unrelated parents. Also multiple inheritance is not necessary, and a talented programmer could avoid these issues by not using multiple inheritance altogether.
Still, there is sometimes a desire to inherit behavior from various unrelated objects in certain cases. The solution to this problem in JavaScript often involves using traits, mixins, the jquery or underscore extend
functions, or even the crude for..in
loop to produce an aggregate object with a union of properties. Problems that often occur with these approaches include information loss about the kind of object being inherited, lost references/errors when collisions occur, and prototype chain pollution. Some libraries have very sophisticated techniques for overcoming these issues, but I feel it's easier to embrace prototypes rather than fight them.
Trope handles multiple inheritance by ensuring the prototype chain remains clean. Overloaded methods can be accessed with this.super.as
, privacy is maintained, and the object's instanceof relationship can be determined with Trope.instanceOf
.
The solution is to have dynamically generated Tropes created upon definition so that JavaScript's single inheritance restriction can be used to emulate a multiple inheritance relationship.
Refer to the LoggingEventedCat example. The object loggingEventedCat
does not inherit from EventEmitter
. Rather it inherits from the dynamically created [EventEmitter which inherits Logger]
. This allows for multiple unrelated parent chains to be normalized into a single, direct inheritance chain. Using these dynamically generated definitions means that the native instanceof
operator cannot be depended on for these kind of objects. However, the Trope.instanceOf
function is available and will still determine the correct relationship.
How to get it
Node
install with npm
npm install --save trope
Browser
install with bower
bower install --save trope
or download and use directly as a script
<script src="trope.js"></script>
git
clone git reposity
git clone https://github.com/ogupte/trope.git
then use npm to install or link it
npm install ./trope
or
cd trope; sudo npm link
Test
Unit
Tests are in mocha
using chaijs
grunt test
or you can run test in the browser by opening test/browser/test.html
Coverage
Run the code coverage report using blanket
.
grunt coverage