Creating objects via constructor functions is fairly straightforward in JavaScript. But as soon as you want to do inheritance, things become complicated. This post examines how inheritance works in traditional JavaScript. It then presents four APIs that make things easier, without adding too much bulk to the language: ECMAScript 5, YUI, Prototype.js, and John Resig’s Simple Inheritance.
What does an API look like that helps us with inheritance, but otherwise changes JavaScript as little as possible? We use the name “object factory” for a construct that allows us to create objects. Here are the required pieces of an API:
- A single construct for object creation. Having a single construct means less clutter. Conversely, defining a constructor function in traditional JavaScript is always a two-step process: Function first, prototype second.
- Invoke the object factory via the new operator. As a result, code that uses the API looks familiar and is thus easier to understand.
- Define what is shared between objects and what isn’t. Methods usually fall into the former category, data properties usually fall into the latter. These definitions are what object creation is all about.
- Inheritance. Tricky in traditional JavaScript, so we want an API to help us.
- Adding methods to existing object factories. Adding methods to the prototype property of a constructor function is common practice in JavaScript and should be supported by an API.
- Easy invocation of overridden methods. Verbose in traditional JavaScript.
- Support instance tests. There needs to be a way for testing whether an object is an instance of a given object factory.
In the following sections, we first review how inheritance is done in traditional JavaScript and then move on to four APIs that adhere to the above mentioned requirements: ECMAScript 5, YUI, Prototype.js, and John Resig’s Simple Inheritance. There are APIs that are more powerful, but these deviate too much from JavaScript proper for the purposes of this post, where we want to keep things light.
Traditional JavaScript
When it comes to creating instances in JavaScript, one has to distinguish between instance-specific things (data properties) and things that are shared between all instances (methods). Given a
constructor function C that produces objects,
C.prototype is where the shared things go, while the function
C itself adds instance-specific properties. This is a clear separation of responsibility.
function Person(name) {
this.name = name;
}
Person.prototype.describe = function() {
return "Person called "+this.name;
}
function Employee(name, title) {
Person.call(this, name);
this.title = title;
}
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.describe = function() {
return Person.prototype.describe.call(this)+" ("+this.title+")";
}
Interaction:
> var john = new Person("John");
> john.describe()
Person called John
> var jane = new Employee("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> jane instanceof Employee
true
> jane instanceof Person
true
Employee is a sub-constructor of
Person. To create it, we have to solve the following problems:
- Employee.prototype must be an object whose prototype is Person.prototype. We solve this problem by using ECMAScript 5’s Object.create() (which is explained in the next section). An often-used more traditional solution is to assign Employee.prototype = new Person(), but then Employee.prototype has the instance variables of Person which are redundant.
- An instance of Employee must have the instance variables of Person, in addition to its own. We solve this be letting the Employee constructor invoke the Person constructor, but without the new operator. Thus, no new instance is created, but Person still adds its instance variables as required.
- Invoking super-methods. We have to directly refer to the super-method (which is part of the super-prototype) to do so.
- Enabling instanceof: object instanceof ConstructorFunction works by checking whether ConstructorFunction.prototype is in the prototype chain of object. Thus, instead of jane instanceof Person, we could have written Person.prototype.isPrototypeOf(jane). isPrototypeOf() is an ECMAScript 5 method.
Note that we have no generic way of referring to the super-constructor, we always mention Person directly.
ECMAScript 5
To understand ECMAScript’s approach, we first need to look at how ECMAScript handles properties.
Property characteristics. A property in ECMAScript can have the following characteristics:
- There are three kinds of properties:
- A named data property is a normal property in an object where it associates a name with a value.
- A named accessor property is a property where access is handled via a getter and a setter method. The property value might be computed and thus not stored anywhere.
- An internal property is managed by the JavaScript engine and might not be accessible at all via JavaScript, or only indirectly. For example, the prototype of an object is not writable in many JavaScript engines and only accessible via Object.getPrototypeOf().
- Property attributes: There are three boolean flags that determine how a property works.
- writable: if true, the value of a property can be changed
- enumerable: if false, the property is hidden in some contexts
- configurable: if true, the property can be deleted, its attributes can be changed, and it can be changed from a data property to an accessor property (or vice versa)
- Own versus inherited: Own properties are directly contained in this, inherited properties are accessible via the prototype chain.
Property descriptors. ECMAScript uses
property descriptors to specify properties to be added. Such a descriptor can have the following properties:
- value: assign the value of a data property
- get, set: Assign the getter and setter of an accessor property.
- writable, enumerable, configurable: Set the corresponding property attributes. The default is for all attributes to be true.
As an example, the following code adds a read-only property to an object.
Object.defineProperty(obj, "prop", { writable: false, value: "abc" });
Setting the prototype of an object. The only way to assign the prototype of an object in ECMAScript is to create a new one via
Object.create(). This function has the following signature.
Object.create(prototype, [propertyDescriptors]);
The second argument is optional. If omitted, an empty object is created. The prototype of an object is also called the
parent of the object. The object is also called the
child of the prototype.
Creating instances with factory functions. We don’t use constructors to create instances, but normal functions (no new or this). These functions can be called factory functions and use Object.create() to create objects that have a common prototype.
Inheritance. Inheritance needs to take care of two things: the prototype and the instance. A super-prototype is extended by prepending the sub-prototype. That is, the super-prototype becomes the sub-prototype’s parent. Thus, the super-prototype must be accessible from the sub-factory. Ensuring the presence of both super-properties and sub-properties in an instance is more complicated. A naive thought might be to make the super-property-descriptors available to the sub-factory, but then we cannot easily initialize values via parameters. Thus, we simulate the traditional approach by giving a factory function an optional parameter self to which it should add properties. self corresponds to this in traditional JavaScript. If self is undefined, a new instance of the super-prototype is created. The sub-function uses self to hand in an instance of the sub-prototype.
var PersonProto = {
describe: function() {
return "Person called "+this.name;
}
};
function createPerson(name, self) {
self = self || Object.create(PersonProto);
self.name = name;
return self;
}
// The sub-prototype extends the super-prototype
var EmployeeProto = Object.create(PersonProto, {
describe: {
value: function() {
return PersonProto.describe.call(this)+" ("+this.title+")";
}
}
});
function createEmployee(name, title) {
var self = createPerson(name, Object.create(EmployeeProto));
self.title = title;
return self;
}
Interaction:
> var john = createPerson("John");
> john.describe()
Person called John
> var jane = createEmployee("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> EmployeeProto.isPrototypeOf(jane)
true
> PersonProto.isPrototypeOf(jane)
true
We cannot use
instanceof. If we try the following.
jane instanceof EmployeeProto
We get the following exception (in most browsers).
TypeError: Result of expression 'EmployeeProto' [[object Object]] is not a valid argument for 'instanceof'.
Thus, we use
isPrototypeOf().
YUI
YUI().use("oop", function(Y) {
function Person(name) {
this.name = name;
}
Person.prototype.describe = function() {
return "Person called "+this.name;
}
function Employee(name, title) {
Employee.superclass.constructor.call(this, name);
this.title = title;
}
Y.extend(Employee, Person); // before adding methods
Employee.prototype.describe = function() {
return Employee.superclass.describe.call(this)+" ("+this.title+")";
}
var john = new Person("John");
console.log(john.describe());
var jane = new Employee("Jane", "CTO");
console.log(jane.describe());
console.log(jane instanceof Employee);
console.log(jane instanceof Person);
});
Console output:
Person called John
Person called Jane (CTO)
true
true
Comments:
- This API is the absolute minimum needed to introduce inheritance to traditional JavaScript.
- It handles the wiring of the prototypes and sets the superclass property of a constructor function so that one can refer to the super-constructor generically, without a hard-coded name.
- Node.js has the function sys.extends which is similar.
Further reading:
Prototype.js
var Person = Class.create({
initialize: function(name) {
this.name = name;
},
describe: function() {
return "Person called "+this.name;
}
});
var Employee = Class.create(Person, {
initialize: function($super, name, title) {
$super(name);
this.title = title;
},
describe: function($super) {
return $super()+" ("+this.title+")";
}
});
var john = new Person("John");
console.log(john.describe());
var jane = new Employee("Jane", "CTO");
console.log(jane.describe());
console.log(jane instanceof Employee);
console.log(jane instanceof Person);
Console output:
Person called John
Person called Jane (CTO)
true
true
Explanations:
- If a method has a first argument named $super, Prototype will hand in the super method. I like the explicitness and simplicity of this approach (similar to Java marking overriding methods with the @Override annotation). One has to be careful about minification, though, because it often renames function arguments to save space.
- Initialization happens in the initialize() method. This separates object construction and object initialization. Constructor chaining becomes method chaining, using the standard $super mechanism.
- Not that the above code is not much different from traditional JavaScript (which was one of the requirements).
Further reading:
John Resig’s Simple Inheritance
The motivation behind Resig’s solution is as follows. The prototype property of a constructor function can be assigned as an object literal. That object literal is the closest thing to a class definition that JavaScript currently has. All that is missing is the initialization code from the constructor function. Thus, the API works like this: Define a class by handing in the prototype as an object literal. Make the initialization code part of that literal by putting it in a method called
init().
var Person = Class.extend({
init: function(name) {
this.name = name;
},
describe: function() {
return "Person called "+this.name;
}
});
var Employee = Person.extend({
init: function(name, title) {
this._super(name);
this.title = title;
},
describe: function() {
return this._super()+" ("+this.title+")";
}
});
Interaction:
> var john = new Person("John");
> john.describe()
Person called John
> var jane = new Employee("Jane", "CTO");
> jane.describe()
Person called Jane (CTO)
> jane instanceof Employee
true
> jane instanceof Person
true
This API is the one that I like most (on the outside). While invoking a method, it makes its super-method available as
this._super. While I think that Prototype’s
$super parameter is prettier, Resig’s solution does not have minification issues and might also be more efficient (no binding of
this, no prepending a parameter). It is thus the better choice.
- You cannot have an object both be invokable via new (only functions can do that) and be the prototype (with methods) for instances. This is a shame, because it prevents the classes from inheriting class methods from their super-classes. The main example is the method extend which must be added to each new class.
- Accessing overridden methods: The API temporarily adds the super-method to this, under the name of _super. This automatically takes care of giving the super-method the proper value for this. If recursive method invocations are to work, _super has to be saved before invoking a method and restored afterwards. The API takes care of this transparently.
Resources:
-
John Resig - Simple JavaScript Inheritance. Explains how the API is used and how it has been implemented.
- Class.js, my own lightweight inheritance API which is very similar to the Simple Inheritance API, but may be easier to understand than Resig’s code.
- Warning: The code uses the ECMAScript 5 API which means you will need a shim on older browsers.
Understanding Resig’s code:
- The prototype property of a constructor function must contain an object whose prototype is the prototype property of the super-constructor. This is achieved by the code by letting the super-constructor produce as in instance, but in a special mode where it doesn’t add instance variables or otherwise initializes the instance. My implementation uses Object.create() for this purpose, obviating the need for a special mode.
- The global Class is created inside a non-method function by assigning to this.Class. It thus uses a JavaScript quirk where this is the global object in non-methods. This is not permitted in strict mode.
- The xyz regular expression tests whether a function’s source code can be searched for the property name _super. Potential obstacles are minification and JavaScript engines that don’t return a function’s source code when it is coerced to string.
- Also incompatible with strict mode: accessing arguments.callee.
Further reading