Not too long ago in the era of Javascript ES5, I played around a bit creating a very simple Javascript library called Species. While developing this library I encountered some weirdness and, in the process, nearly broke my mind.
Let me tell you about it.
The aim of Species
In this phase of my life I obsessed with anthropomorphizing javascript objects, and somehow doing away with the this
keyword.
I also wanted to mimic classification and inheritance as it was understood in classical biology. Different kinds of objects are analalous to different kinds of creature, I thought, and just as one creature can inherit it’s characteristics and behaviour from it’s parent, javascript objects would, in my new library, be able to inherit from a parent.
This library would follow a once popular Javascript Design Pattern: adapting prototypical inheritance to implement class inheritance.
The small library I developed was called Species.
Exploring Species
Species has only one function: the Species contructor Function, which accepts a blueprint object and returns a new constructor function that creates objects with the characteristics defined in the blueprint.
var pigBlueprint = {
tail: "curly"
}
var Pig = new Species(pigBlueprint)
var pepper = new Pig()
console.log(pepper.tail) // => "curly"
Not very interesting. Let’s see what else we can do.
Inheritance
By specifying an ancestorSpecies
in the bluesprint that you pass into the Species constructor function, you can create a Species that inherits characteristics from the specified ancestorSpecies.
Inheritance from a plain old javascript object.
var Wolf = { wild : true };
Wolf.prototype = {};
Wolf.prototype.communicate = function() { return "woof" };
var Dog = Species({ ancestorSpecies : Wolf });
var fido = new Dog()
console.log(fido instanceof Dog) // => true
console.log(fido.ancestorSpecies) // => the Wolf object
console.log(fido.communicate()) // => "woof"
console.log(fido.wild) // => undefined
Inheritance from another Species
Specifying a Bio Function in the bluesprint object passed to the constructor allows you to define attributes that are not shared among instances of that species. You can also return an anonymous inner function in the bio function that will be executed upon construction.
var Vulcan = new Species({
bio: function() {
this.haircut = "bad";
return function() {
this.ears = "pointy";
this.emotional = false;
};
},
homeworld: "Vulcan"
});
var Romulan = Species({
ancestorSpecies : Vulcan,
bio: function() {
return function() {
this.emotional = true;
};
},
homeworld: "Romulus"
});
var spock = new Vulcan();
var tuvok = new Vulcan();
var remus = new Romulan();
console.log(remus instanceof Romulan) // => true
console.log(remus instanceof Vulcan) // => true
Let’s see what attributes and behaviour are shared with other instances of the same species.
console.log(remus.ears) // => "pointy"
console.log(spock.ears) // => "pointy"
console.log(tuvok.ears) // => "pointy"
remus.ears = "burned"
tuvok.ears = "chopped off"
console.log(remus.ears) // => "burned"
console.log(spock.ears) // => still "pointy"
console.log(tuvok.ears) // => "chopped off"
console.log(spock.haircut) // => "bad"
console.log(tuvok.haircut) // => "bad"
console.log(remus.haircut) // => "bad"
spock.haircut = "hipster trendy"
console.log(spock.haircut) // => now "hipster trendy"
console.log(tuvok.haircut) // => still "bad"
console.log(remus.haircut) // => still "bad"
console.log(remus.emotional) // => true
console.log(spock.emotional) // => false
console.log(tuvok.emotional) // => false
Attributes defined outside the bio
function are added to the prototypes of the object instances and are shared by all instances of the same Species.
spock.__proto__.homeworld = "Earth"
console.log(spock.homeworld) // => now "Earth"
console.log(tuvok.homeworld) // => "Earth" - all vulcans have the same homeworld
console.log(remus.homeworld) // => still "Romulus"
Using personal pronoun syntax in place of ‘this’
I mentioned earlier that I was trying to do away with the this
keyword in Javascript. I wasn’t kidding.
In the bio
function, list personal pronouns you would like to use (or any words for that matter) in place of the this
keyword, and they will be aliased for you and available to use inside the bio function.
var Bird = new Species({
// In the bio function parameters, specify the list of...
// ...personal pronouns you wish to use. Can be anything.
bio : function(my, I) {
// Then, inside bio function, use personal pronouns instead of `this`.
I.sayName = function() {
return "My name is " + my.name;
}
return function(name) {
my.name = name;
}
}
});
var woody = new Bird("woody the woodpecker");
console.log(woody.sayName()) // => "My name is woody the woodpecker"
var Duck = new Species({
ancestorSpecies : Bird,
// In the bio function parameters, specify the list of...
// ...personal pronouns you wish to use. Can be anything.
bio : function(my, I) {
// Then, inside bio function, use personal pronouns instead of `this`.
I.quack = function(words) {
return "quack! " + words + " quack!"
}
return function(name, hat) {
// Assignment of name attribute happens in parent so no need to do it here
my.hat = hat;
}
}
});
var donald = new Duck("donald duck", "sailor");
console.log(donald.sayName()) // => "My name is donald duck"
console.log(donald.quack("I")) // => "quack! I quack!"
console.log(donald.hat) // => "sailor"
Reality unwinding
So far so good. However, at about this point, things start to become weird.
var Human = new Species()
var bob = new Human()
console.log(Human.prototype.constructor.name) // => "God" ??????
console.log(bob.constructor.name) // => "God"
God eh? Yep.
It seemed appropriate for the object creating new species objects to be named God. What could be the harm? 1.
1. Little did I know…
Let’s examine the nature of this “God” object.
var God = bob.constructor
console.log(God) // => function God() { }
console.log(God.prototype.name) // => "God"
console.log(God.prototype === God) // => God
Interesting: God is a function and is his own prototype. So God is created in his own image. Neat.
Does God have the ability to create life?
var Flower = God.createSpecies({
bio : function(Iam, my) {
Iam.pretty = true
return function(color) {
my.color = color;
}
}
})
var tulip = God.bringToLife(null, Flower, "red")
console.log(tulip.color) // => "red"
console.log(tulip.pretty) // => true
console.log(tulip instanceof Flower) // => true
Yes, God can create life.
So what’s the meaning of life, God?
console.log(God.meaningOfLife) // => 42
Oh very witty. 2
2. You knew that was coming, right?
I wonder whether each Species has its own separate god.
var fidoGod = fido.constructor
var spockGod = spock.constructor
var tuvokGod = tuvok.constructor
var remusGod = tuvok.constructor
console.log(God === fido.constructor) // => true
console.log(God === spock.constructor) // => true
console.log(God === tuvok.constructor) // => true
console.log(God === remus.constructor) // => true
Woh!. There is only one God. Deep.
So God created all the creatures (objects) of each Species.
But, wait a moment…
I’m the developer, aren’t I?
Didn’t I create these objects?
Hmm…
…Oh of course!
Ha ha [nervous laughs] How silly!
God created all the Species instances, and I created God.
So man created God afterall. Ha ha…
…ha…
But, then…
console.log(God.constructor.name) // => "God"
…why does it say that God is the constructor of God?
Wait…
What?!!
[feels like falling]
Wwwwwwwwwhhhhhhhhhhhhhhhhhaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!!!!!
[takes a breath]
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaagggggggghhhhhhhhhhhhhhhhhhhhhh
[sometime later]
I’m ok. I’m ok.
Wwwwwwaaaaaaaaaaaaaaaaaaaaggggggggghhhhhhh!!!!!!!
Programming is fun
Don’t worry. I’m alright now.
Programming is so powerful, you can literally create your own worlds. Mucking about with code is really fun and can teach you deep lessons about the language you’re using. Why not play around with some ideas yourself? You may be surprised.
The code for the Species library
If anyone is interested in using this odd and totally pointless little library (please don’t) or just wants to see the code (why would you?) then here it is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
var Species = (function() {
var grantKnowledgeOfCreatorTo = function(species) {
species.prototype.constructor = God;
};
var copyAttributes = function(attrSource, attrDestination){
for (var attribute in attrSource) {
if (attrSource.hasOwnProperty(attribute)) {
attrDestination.prototype[attribute] = attrSource[attribute];
}
}
};
var applyDNA = function(ancestorSpecies, childSpecies) {
var DNA = function DNA() {};
DNA.prototype = ancestorSpecies.prototype;
childSpecies.prototype = new DNA();
childSpecies.ancestorSpecies = ancestorSpecies.prototype;
};
var generateIdentitiesFor = function(self, identityCount) {
return Array.apply(null, new Array(identityCount)).map(Object.prototype.valueOf, self);
};
var consciousness = function(speciesBio, attributes) {
var numberOfIdentities = speciesBio.length;
var selfAwareness = generateIdentitiesFor(this, numberOfIdentities);
var selfAwareBeing = [this].concat(selfAwareness);
var powerOfLife = speciesBio.apply(this, selfAwareBeing);
if (typeof powerOfLife === 'function'){
powerOfLife.apply(this, attributes);
}
};
var God = function God() { };
God.meaningOfLife = 42;
God.prototype = God;
God.prototype.constructor = God;
God.bringToLife = function(newBeing, speciesType) {
var attributes = Array.prototype.slice.call(arguments, 2);
var being = newBeing || new speciesType();
if (speciesType.ancestorSpecies && speciesType.ancestorSpecies.hasOwnProperty("bio")) {
var ancestorBio = speciesType.ancestorSpecies.bio || function(){};
consciousness.call(being, ancestorBio, attributes);
}
if (speciesType.prototype.hasOwnProperty("bio")) {
var speciesBio = speciesType.prototype.bio || function(){};
consciousness.call(being, speciesBio, attributes);
}
return being;
};
God.createSpecies = function(blueprint, constructorFunction) {
var species = constructorFunction || function Species() {
God.bringToLife.apply(this, [this, Species].concat(Array.prototype.slice.call(arguments)));
};
var ancestorSpecies = (blueprint && blueprint.ancestorSpecies) ? blueprint.ancestorSpecies : Object;
applyDNA(ancestorSpecies, species);
copyAttributes(blueprint, species);
grantKnowledgeOfCreatorTo(species);
return species;
};
return function(blueprint) {
return God.createSpecies(blueprint);
};
})();