Thursday, August 5, 2010

Constructors without using "new"

When creating a constructor in Javascript, one has to worry about a problem: what if someone calls your constructor without using new?

function Point(x,y) {
  this.x = x;
  this.y = y
}

// This is good
var pt = new Point(20,30);

// This is not
var pt2 = Point(20,30);

You'll notice when you don't use new, pt2 is undefined after the call. Even worse, we've added two variables to the global scope, x, y. That is because if you call a function without specifying the context, the browser passes in the global object, which is "window";

The first attempt to fix this is the following

function Point(x,y) {
  if (this instanceof Point) {
    this.x = x;
    this.y = y
  } else {
    return new Point(x,y);
  }
}

// This is good
var pt = new Point(20,30);

// This is OK also
var pt2 = Point(20,30);

However, that seems like a lot of boiler plate code to add to every constructor. How can we abstract that? Here's what I came up with.

/**
 * Wraps the passed in constructor so it works with
 * or without the new keyword
 * @param {Function} realCtor The constructor function.
 *    Note that this is going to be wrapped
 *    and should not be used directly 
 */
function ctor(realCtor){
  // This is going to be the actual constructor
  return function wrapperCtor(){
    var obj; // object that will be created
    if (this instanceof wrapperCtor) {
      // Called with new
      obj = this;
    } else {
      // Called without new. Create an empty object of the
      // correct type without running that constructor
      surrogateCtor.prototype = wrapperCtor.prototype;
      obj = new surrogateCtor();
    }
    // Call the real constructor function
    realCtor.apply(obj, arguments);
    return obj;
  }
}

/** 
 * A function that does nothing, so we can create objects 
 * of a prototype without running unnecessary code. 
 * See its usage above
 */
function surrogateCtor() {}

// Create our point contructor
Point = ctor(function(x,y){
  this.x = x;
  this.y = y;
});

// This is good
var pt = new Point(20,30);
// This is OK also
var pt2 = Point(20,30);

// It's even ok with inheritance, though a 3D point shouldn't
// really inherit from a 2D point, it's just to make a point...
Point3D = ctor(function(x,y,z){
  Point.call(this, x, y);  
  this.z = z;
});

// Note that this is not the best way to setup inheritance.
// My next post will explain a better way
Point3D.prototype = new Point();

var pt3D = new Point3D(5,3,8);

// Outputs true
console.log(pt3D instanceof Point3D && pt3D instanceof Point);

6 comments:

  1. Hey man, good blog btw.
    I kinda have mix feelings about this no "new" keyword for class instantiation.
    I think the new keyword explicitly say that this is your brand new instance of this object and nothing else.
    Now with no new keyword it seems like it is not a new instance of this class, and worse yet, it could confuse with the Singletons that you might want to create shorthand, ie: dh = Ext.Domhelper;
    I understand the problems that occur when someone instantiate your class with no new keyword, but then again, they shouldn't be doing that anyway.
    I never seen in any tutorial someone saying that you could use either way (new/no new); google just released their new google map v3 and if you dont say "new google.maps.Map(map-id, mapoptions)" you wont get anything back.
    That is just what I think, you dont have to agree with me though :)

    ReplyDelete
  2. @bostonian: To be honest, I don't really use this in my code, so I do agree with you. This post is more about explaining how to do it if you wanted to. Say, someone writing a library trying to make it more fool proof.

    ReplyDelete
  3. So you think that is more intuitive to someone reading your code than adding

    if (!(this instanceof Point))
    return new Point(x, y);

    to the top of your constructors? :)

    ReplyDelete
  4. @Jaka: Great point. Had I thought of reversing my if/else when writing the post, I probably would have stopped at that since that doesn't feel like too much boiler plate code. Still, abstracting the idea allows it to be used transparently from a library that helps with JS inheritance like http://edspencer.net/2011/01/classes-in-ext-js-4-under-the-hood.html or http://mootools.net/docs/core/Class/Class

    ReplyDelete



  5. Hi there, awesome site. I thought the topics you posted on were very interesting. I tried to add your RSS to my feed reader and it a few. take a look at it, hopefully

    I can add you and follow.












    Function Point

    ReplyDelete
  6. Cool tips. Too bad you haven't had time to keep posting...

    ReplyDelete