-
-
Save nzakas/4289220 to your computer and use it in GitHub Desktop.
/* | |
* This is just an experiment. Don't read too much into the fact that these are global variables. | |
* The basic idea is to combine the two steps of defining a constructor and modifying a prototype | |
* into just one function call that looks more like traditional classes and other OO languages. | |
*/ | |
// Utility function | |
function mixin(receiver, supplier) { | |
if (Object.getOwnPropertyDescriptor) { | |
Object.keys(supplier).forEach(function(property) { | |
var descriptor = Object.getOwnPropertyDescriptor(supplier, property); | |
Object.defineProperty(receiver, property, descriptor); | |
}); | |
} else { | |
for (var property in supplier) { | |
if (supplier.hasOwnProperty(property)) { | |
receiver[property] = supplier[property] | |
} | |
} | |
} | |
return receiver; | |
} | |
/** | |
* Creates a new constructor with appropriate prototype members. If there's only one | |
* argument, it's considered the declaration. When you want to inherit from another | |
* object or constructor, then that is the first argument and the declaration is second. | |
* I considered always having the declaration as the first argument, but found it was | |
* easy to forget to do include the prototype as the second argument. Also, this | |
* allows you to look at the top of the type declaration to see if there is any | |
* inheritance, whereas having it as a second argument could lead to it being overlooked. | |
* | |
* @param {Function|Object} prototype (optional) The prototype for the new type. If | |
* this is a function, then the function's prototype is used. If this | |
* is an object, then that object is used. If omitted, Object.prototype | |
* is used as is the case for all generic objects. | |
* @param {Object} declaration The object literal containing at least a constructor | |
* function. All other methods are added to the resulting constructor's | |
* prototype. If there's only one argument to the function, then it is considered | |
* to be the declaration. | |
*/ | |
function type(prototype, declaration) { | |
// if there's only one argument, then the first argument is the declaration | |
if (!declaration) { | |
declaration = prototype; | |
declaration.constructor.prototype = declaration; | |
} else { | |
// make sure the prototype is an object | |
prototype = (typeof prototype == "function") ? prototype.prototype : prototype; | |
// create a new prototype for the constructor function | |
declaration.constructor.prototype = Object.create(prototype, { | |
constructor: { | |
configurable: true, | |
enumerable: true, | |
value: declaration.constructor, | |
writable: true | |
} | |
}); | |
// add everything from the declaration onto the new prototype | |
mixin(declaration.constructor.prototype, declaration); | |
} | |
// return the now-complete constructor function | |
return declaration.constructor; | |
} | |
//--------------------------------------------------------------------------- | |
// Usage | |
//--------------------------------------------------------------------------- | |
var Rectangle = type({ | |
constructor: function(length, width) { | |
this.length = length; | |
this.width = width; | |
}, | |
getArea: function() { | |
return this.length * this.width; | |
} | |
}); | |
// inherit from rectangle | |
var Square = type(Rectangle, { | |
constructor: function(size) { | |
Rectangle.call(this, size, size); | |
} | |
}); | |
var rect = new Rectangle(3, 10); | |
console.log(rect instanceof Rectangle); // true | |
console.log(rect.constructor === Rectangle); // true | |
console.log(rect.getArea()); // 30 | |
var square = new Square(10); | |
console.log(square instanceof Square); // true | |
console.log(square instanceof Rectangle); // true | |
console.log(square.constructor === Square); // true | |
console.log(square.constructor === Rectangle); // false | |
console.log(square.getArea()); // 100 |
I made some performance tests on jsperf to compare your implementation here (which I call "type1" in the tests) to my two forks (which I call "type2" and "make3", respectively).
My two forks:
"type2" https://gist.github.com/4289270
"make3" https://gist.github.com/4302554
The performance tests:
"definition" http://jsperf.com/js-classes-objects-definition
"instantiation" http://jsperf.com/js-classes-objects-instantiation
"usage" http://jsperf.com/js-classes-objects-usage
Definitely some surprising (to me, anyway) results looking through those performance metrics.
Fair 'nuf.
What's the upshot of this opposed to the classic approach using a temporary function with the target constructor prototype. Could even use __proto__
, although that's not backwards compat, which I assume you still want.
Classic approach:
function Rectangle(w,h){ this.width = w; this.height = h; }
Rectangle.prototype = {
constructor: Rectangle,
getArea: function() {
return this.width * this.height;
}
};
// setup inheritance (prevents premature invocation of Rectangle for inheritance)
function tmp(){}
tmp.prototype = Rectangle.prototype; // this makes instanceof work
// Square will inherit from Rectangle
function Square(side){
Rectangle.call(this, side, side);
}
Square.prototype = new tmp(); // instanceof magic
Square.prototype.constructor = Square;
// but now you need to mixin your properties :(
var rect = new Rectangle(3, 10);
console.log(rect instanceof Rectangle); // true
console.log(rect.constructor === Rectangle); // true
console.log(rect.getArea()); // 30
var square = new Square(10);
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square.constructor === Square); // true
console.log(square.constructor === Rectangle); // false
console.log(square.getArea()); // 100
And with dunderproto:
function Rectangle(w,h){ this.width = w; this.height = h; }
Rectangle.prototype = {
constructor: Rectangle,
getArea: function() {
return this.width * this.height;
}
};
// Square will inherit from Rectangle
function Square(side){
Rectangle.call(this, side, side);
}
Square.prototype = {
__proto__: Rectangle.prototype,
constructor: Square,
};
var rect = new Rectangle(3, 10);
console.log(rect instanceof Rectangle); // true
console.log(rect.constructor === Rectangle); // true
console.log(rect.getArea()); // 30
var square = new Square(10);
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square.constructor === Square); // true
console.log(square.constructor === Rectangle); // false
console.log(square.getArea()); // 100
There are two things here: the syntax for creating a custom type and then how the prototype is assigned.
My main goal with this was to create a more succinct syntax for creating a custom type. I've always hated needing to create a constructor and then needing to manually modify the prototype. I wanted to do that in just one step.
Object.create()
effectively does the same thing as your first example using tmp
, which is the same as Crockford's object()
function. Your second example is, once again, the same as using Object.create()
just with __proto__
to do the assignment instead. So your two examples and my approach all essentially work the same way - these are all just different ways of assigning a prototype without needing to call the supertype constructor again.
I made a variation on this approach: https://gist.github.com/4289270#comment-628331