Created
December 14, 2012 22:27
-
-
Save nzakas/4289220 to your computer and use it in GitHub Desktop.
Experiment: Simple way to define types, including prototypal inheritance, in JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 usingtmp
, which is the same as Crockford'sobject()
function. Your second example is, once again, the same as usingObject.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.