Skip to content

Instantly share code, notes, and snippets.

@nzakas
Created December 14, 2012 22:27
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nzakas/4289220 to your computer and use it in GitHub Desktop.
Save nzakas/4289220 to your computer and use it in GitHub Desktop.
Experiment: Simple way to define types, including prototypal inheritance, in JavaScript
/*
* 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
@getify
Copy link

getify commented Dec 15, 2012

I made a variation on this approach: https://gist.github.com/4289270#comment-628331

@getify
Copy link

getify commented Dec 16, 2012

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.

@pvdz
Copy link

pvdz commented Dec 18, 2012

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

@nzakas
Copy link
Author

nzakas commented Dec 18, 2012

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment