Spritzr is an inheritance/traits/talents library for node.js and the browser.
It aims to provide three things:
- Extension: to do classical single inheritance
- Traits: mixins at a class level and with state (a bit like multiple inheritance)
- Talents: like traits, but applied to a single instance (and they can also be removed at will)
Spritzr was inspired by a great traits/talents library CocktailJS https://github.com/CocktailJS/cocktail.
Spritzr is automatically unit tested against the following browsers:
- Internet Explorer 6, 7, 8, 9, 10 and 11
- Firefox latest and 3.6 versions
- Chrome latest version
- Safari desktop latest version
- Opera desktop latest version
- Android 2.3 and 4.2
- iOS 7
It is also tested on node.js version 0.10 and PhantomJS (headless webkit).
Special thanks to BrowserStack for providing a free account to run our automated tests on all these different browsers and operating systems!
Spritzr has an extend
function which implements a basic single inheritance model.
The prototype chain is maintained so that instanceof
works as expected.
function Animal(type) {
this.type = type;
};
Animal.prototype.type = null;
Animal.prototype.greet = function() {
return "Hi, I'm a " + this.type + " called " + this.name;
};
// We need to explicitly call the super constructor using a handy $super property.
function Person(name) {
this.$super("person");
this.name = name;
};
Person.prototype.name = null;
Spritzr.extend(Person, Animal);
var steve = new Person("Steve");
expect(steve.greet()).toBe("Hi, I'm a person called Steve");
expect(steve instanceof Person).toBe(true);
expect(steve instanceof Animal).toBe(true);
// Spritzr also provides an isa() function which acts like instanceof
expect(Spritzr.isa(steve, Animal)).toBe(true);
// We can also override methods and properties of the super class
// but still access them using $super
var Tony = function Tony() {
this.$super("Tony");
};
Spritzr.extend(Tony, Person);
Tony.prototype.greet = function() {
var normalGreeting = this.$super.greet();
return normalGreeting + ", y'all!";
};
var tony = new Tony();
expect(tony.greet()).toBe("Hi, I'm a person called Tony, y'all!");
When extending a class, the super class constructor will not be implicitly called when the sub class is instantiated. Therefor you should call the $super()
constructor from within the subclass constructor; for example:
var Animal = function() {
// Set up animaly stuff
};
var Mammal = function() {
this.$super(); // Call the Animal() constructor
// Set up mammalian stuff like live babies and stuff.
};
Spritzr.extend(Mammal, Animal);
When you call a super class method using this.$super.method()
then the scope gets lost somewhere along the way, which means that this
within the super method is not pointing to the correct object. There are two ways around this:
- Ensure you always call
this.$super()
from within the constructor. This has access to the real object and will cache it for later within the $super object. You can then just callthis.$super.method()
to call the super class method. - Pass in othe correct scope when you call the method by using
this.$super.method.call(this, arg1, arg2 ... )
.
Classes can be extended with multiple traits, which are a bit like interfaces in Java but can also contain method implementations and properties.
var Animal = function() { };
var Mammal = function() { };
Spritzr.extend(Mammal, Animal); // Mammal is an extension of Animal
var Amphibian = function() { };
Spritzr.extend(Amphibian, Animal); // Amphibian is an extension of Animal
var Bird = function() { };
Spritzr.extend(Bird, Animal); // Bird is an extension of Animal
// We create a LaysEggs trait, which can be a class or a plain old object
var LaysEggs = function() { };
LaysEggs.prototype.layEgg = function() {
return new Egg();
};
// And we can apply the trait to specific classes
Spritzr.spritz(Amphibian, LaysEggs); // All Amphibians can now lay eggs
Spritzr.spritz(Bird, LaysEggs); // All Birds can now lay eggs
// Then we can use isa() to work out if an object has a specific trait
var cat = new Mammal();
expect(Spritzr.isa(cat, LaysEggs)).toBe(false);
var frog = new Amphibian();
expect(Spritzr.isa(frog, LaysEggs)).toBe(true);
var parrot = new Bird();
expect(Spritzr.isa(parrot, LaysEggs)).toBe(true);
// Traits are also inherited through class extension
var Emu = function() { };
Spritzr.extend(Emu, Bird); // Emu extends Bird
var emu = new Emu();
expect(Spritzr.isa(emu, LaysEggs)).toBe(true);
// or from other traits
var Monotreme = function() { };
Spritzr.spritz(Monotreme, LaysEggs);
var Platypus = function() { };
Spritzr.extend(Platypus, Mammal); // A platypus is a mammal
Spritzr.spritz(Platypus, Monotreme); // But also a monotreme
var ducky = new Platypus();
expect(Spritzr.isa(ducky, LaysEggs)).toBe(true);
Talents are a bit like traits, but they are applied to instances rather than classes. They can also be removed at any point.
// We have a class describing people
var Person = function(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
};
Person.prototype.firstName = null;
Person.prototype.lastName = null;
Person.prototype.getDisplayName = function() {
return this.firstName + ' ' + this.lastName;
};
var sharon = new Person("Sharon", "Ackerman");
var tony = new Person("Tony", "Jones");
// We can define a talent to describe friendship
var Friend = function(nickname) {
this.nickname = nickname;
};
Friend.prototype.nickname = null;
Friend.prototype.getDisplayName = function() {
return this.firstName + ' "' + this.nickname + '" ' + this.lastName;
};
// And add a new talent to our friends
Spritzr.spritz(sharon, new Friend('The Shazza'));
// We can then check for existence of the talent to find out if a person is our friend
expect(Spritzr.isa(sharon, Friend)).toBe(true);
expect(Spritzr.isa(tony, Friend)).toBe(false);
// And the methods should be overridden appropriately
expect(sharon.getDisplayName()).toBe('Sharon "The Shazza" Ackerman');
expect(tony.getDisplayName()).toBe('Tony Jones');
// We can also remove talents from an instance using the amazing titled unspritz function
Spritzr.unspritz(sharon, Friend);
expect(sharon.getDisplayName()).toBe('Sharon Ackerman');
// We don't need to instantiate the talent first either - the constructor will automatically be called
var HasAccount = function() {
this.username = (this.firstName.substr(0,1) + this.lastName).toLowerCase();
};
HasAccount.prototype.username = null;
Spritzr.spritz(tony, HasAccount); // This adds the methods/properties and calls the constructor
expect(Spritzr.isa(tony, HasAccount)).toBe(true);
expect(tony.username).toBe("tjones");
The library is reasonably well tested, but there are some flows which haven't been thought about much so far:
- The effect of spritzing the same trait or talent twice into a class of object is untested and undefined.
- New properties added to a trait after it has been spritzed into a class won't be reflected in the class (so if you add trait A into class B, then add a method to A it won't be reflected in B). You should set up your class hierarchy at the start of the application and avoid mutating it later.
- Equally, methods added to a talent after it's been spritzed into an instance also won't be reflected in the instance.
- There is no cycle checking in the isa() method, so if you have circular class dependencies then it's going to go into infinite recursion.
- Extending a class twice is currently not prevented and will probably cause some wierd behaviour.
- Probably a load of other stuff I haven't thought of!
The intention is to overcome these caveats in the future - it should be possible.