-
-
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()); // 100And 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()); // 100There 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