From closures to prototypes, part 2

In part 1 of this post, I exposed the differences between the closure and the prototype patterns to define JavaScript classes. In this post, I'll show the shortest path to convert an existing class built using Atlas and closures to a prototype-based class. I'll also show a few caveats that you need to be aware of.

Here's an example of a class built using the July CTP of Atlas:

Custom.Color = function(r, g, b) {
  Custom.Color.initializeBase(this);

  var _r = r || 0;
  var _g = g || 0;
  var _b = b || 0;

  this.get_red = function() {
    return _r;
  }
  this.set_red = function(value) {
    _r = value;
  }

  this.get_green = function() {
    return _g;
  }
  this.set_green = function(value) {
    _g = value;
  }
  this.get_blue = function() {
    return _b;
  }
  this.set_blue = function(value) {
    _b = value;
  }

  this.toString = function() {
    return Custom.Color.callBaseMethod(this, "toString") +
        "(" + _r + "," + _g + "," + _b + ")";
  }
  Custom.Color.registerBaseMethod(this, "toString");
}
Custom.Color.registerClass("Custom.Color", Sys.Component);

The first thing you need to do is insert the following right after the private variable initialization, as this is all that's needed now in the constructor:

}
Custom.Color.prototype = {

Then you need to replace the variable initializations in the constructor with instance variable initialization:

  this._r = r || 0;
  this._g = g || 0;
  this._b = b || 0;

The next step is of course to replace all occurrences of _r, _g and _b with this._r, this._g and this._b throughout the rest of the code.

Note the convention that private members (fields and methods) are prefixed with an underscore. This convention will be adopted by Visual Studio Orcas in IntelliSense.

For each method and property accessor, you need to remove the "this." before the name and replace the " = " with ": ". Also add a comma after the closing brace of each method but the last one. Finally, remove any registerBaseMethod you may have (in prototype-based classes, everything is virtual).

That's it, your class is now built using prototypes:

Custom.Color = function(r, g, b) {
  Custom.Color.initializeBase(this);

  this._r = r || 0;
  this._g = g || 0;
  this._b = b || 0;
}
Custom.Color.prototype = {
  get_red: function() {
    return this._r;
  },
  set_red: function(value) {
    this._r = value;
  },

  get_green: function() {
    return this._g;
  },
  set_green: function(value) {
    this._g = value;
  },

  get_blue: function() {
    return this._b;
  },
  set_blue: function(value) {
    this._b = value;
  },

  toString: function() {
    return Custom.Color.callBaseMethod(this, "toString") +
        "(" + this._r + "," + this._g + "," + this._b + ")";
  }
}
Custom.Color.registerClass("Custom.Color", Sys.component);

I've highlighted where the code differs in both samples for easy reference.

So that was easy. Now let's enumerate a few do's and don't's...

  • Do transform private constructor variables into underscore-prefixed instance fields.
    Foo.Bar = function() {
      var _baz = "";
    }
    becomes
    Foo.Bar = function() {
      this._baz: ""
    }
  • Do move all methods and property accessors from the constructor to the prototype.
  • Do update all references to fields and methods to be dereferenced through the this pointer.
    _member
    becomes
    this._member
  • Do separate prototype members with commas, but don't leave a comma after the last one (JSON syntax doesn't allow it).
  • Don't use the instanceof operator with Atlas classes. Do use 
    MyNamespace.MyType.isInstanceOfType(myInstance)
    instead of 
    myInstance instanceof MyNamespace.MyType
  • Do continue to set initial values of fields from the constructor.
  • Don't move field initializations that depend on the this pointer to prototype (but do make them fields)
    Foo.Bar = function() {
      var _delegate = Function.createDelegate(this, this._handler);
    }
    becomes
    Foo.Bar = function() {
      this._delegate = Function.createDelegate(this, this._handler);
    }
  • Don't register base methods: with the prototype pattern, all methods are virtual. The registerBaseMethod will not be available in Atlas starting with Beta 1, which makes it impossible for closure-based classes to be base Atlas classes.
  • Do move private functions to the prototype (prefixing the name with underscore) unless they have a compelling use of the closure pattern (for security for example), which is quite rare.
    Foo.Bar = function() {
      function privateMethod() {
        //...
      }
    }
    becomes
  • Foo.Bar = function() {}
    Foo.Bar.prototype = {
      _privateMethod: function() {
        //...
      }
    }

In the next posts, I'll get into other specific changes that we're making in Beta 1.

Read part 1 here: http://weblogs.asp.net/bleroy/archive/2006/10/11/F...

14 Comments

  • With the ambiguity about reference versus value types for fields, why not ALWAYS put the fields in the constructor and never in the prototype? It would be simpler and more consistent, not to mention faster since the lookup against the object instance always occurs before the lookup against the prototype. You example translation even stops there, but the guidelines then get ambiguous.

    Please, before this gets too far, can we rethink these guidelines and aim for something that is far less likely to result in accidental errors?

  • hello.

    in my opinion, _baz should be declared inside the constructor, not inside the object that it's assigned to the prototype property. in other words, it should be:

    Foo.Bar = function() {
    this._baz = "";
    }
    Foo.Bar.prototype = {

    }

  • not sure if my previous comment was submitted, so i'll put it here again.

    in my opinion, private constructor variables should be assigned inside the constructor and not in the object assigned to the prototype property. ex.:

    Foo.Bar = function() {
    this._baz = "";
    }
    Foo.Bar.prototype = {

    }

    i think that prototype may not give you the semantics you want in all cases...

  • You're both right, I fixed the post. Luis: I explained that in this post:
    http://weblogs.asp.net/bleroy/archive/2006/10/07/Careful-with-that-prototype_2C00_-Eugene.aspx
    In principle, null and value types are fine to initialize from the prototype, but it does make clearer guidelines to do them all from the constructor.

  • One other thought... I much prefer doing the seperator comma as a leading instead of trailing comma. This prevents accidentally leaving that dangling comma. I do the same thing in SQL queries.

  • hi.
    yep, you're right...i've missed that post...

  • [quote]
    Note the convention that private members (fields and methods) are prefixed with an underscore. This convention will be adopted by Visual Studio Orcas in IntelliSense.
    [/quote]

    We need to prefix the variables with this. to access them, so why the extra _ prefix?

    // Ryan

  • Bertrand - do you have any benchmarks on these changes when compared to closure based Atlas? What kind of improvements can we expect? I only ask this because in experimenting with different optimization in js I have found very far and few improvements.

  • Ryan: from the *outside*, you need a way to tell public and private members apart. Users of your classes (and Visual Studio) must be able to determine easily if Foo.bar() or Foo._bar() is public or private.

    Alex: from our benchmarks, creating an instance is between 3.5 and 16 times faster depending on the browser. Firefox is getting the biggest benefits. Calling members is a little slower on all browsers (between 8 and 58 percent) because of the need to lookup on the instance, then the prototype. But the biggest benefit is in terms of working set with the prototype model taking about 30 times less memory than closures. Finally, parsing times are about twice as fast on IE with closures, and four times as fast with prototypes on Firefox.
    So all in all, closures are generally a little faster on IE, but prototypes really shine on Firefox. All this was with IE6 but I don't think the JavaScript engine was changed in IE7.
    Of course, all those are micro-benchmarks so your mileage may vary, depending on your type of application.

  • Bertrand, does this mean the syntax conventions will make it possible to forgo the type descriptor interface that's required in the current builds?

  • Rick: this in principle doesn't change anything to type descriptors, which still need additional type information. The design for type descriptors *did* change, but for different reasons. More on that later.

  • I had heard that the next release would be sometime this week. Do you know if there's any truth to that prediction? Thanks for these Atlas posts...they're very helpful.

  • Bertrand - any ideas when a public CTP using prototypes will be available?

  • Michael, Jon: it's imminent.

Comments have been disabled for this content.