JavaScript objects are prototype-based, whereas the other most widely used languages today all use class-based objects. In a class-based system, an object is defined by describing what it’ll look like with a class. In prototype-based systems, we create an object that looks like what we want all objects of that type to look like, and then tell the JavaScript engine that we want more objects that look like that.
Not to stretch a metaphor too far, but if architecture were a class-based system, an architect would draw up the blueprints of a house and then have houses built based on that blueprint. If architecture were prototype-based, the architect would build a house and then have houses built to look like that one.
Let’s build on our earlier prisoner example and compare what it takes in each sys- tem to create a single prisoner with properties for the name, prisoner ID, length of prison sentence in years, and number of years probation.
The prototype-based object is simpler and quicker to write when there’s only one instance of an object. In class-based systems you have to define a class, define a con- structor, and then instantiate an object that is a member of that class. A prototype- based object is simply defined in place.
The prototype-based system shines for the simple one object use case, but it can also support the more complex use case of having multiple objects that share similar characteristics. Let’s take the previous example of prisoners and let the code change the name and id of the prisoners, but keep the same preset years in sentence and years until probation.
As you can see in table 2.3, the two kinds of programming follow a similar sequence, and if you’re used to classes, adjusting to prototypes shouldn’t be much of a stretch. But the devil is in the details, and if you’re coming from a class-based system and jump into JavaScript without learning the prototype-based approach, it’s easy to
Table 2.2 Simple object creation: class versus prototype
Class-based Prototype-based
public class Prisoner { public int sentence = 4;
public int probation = 2;
public string name = "Joe";
public int id = 1234;
}
Prisoner prisoner = new Prisoner();
var prisoner = { sentence : 4, probation : 2, name : 'Joe',
id : 1234
};
get tripped up on something that seems like it should be simple. Let’s step through the sequence and see what we can learn.
In each method, we first create the template for our objects. The template is called the class in class-based programming and the prototype object in prototype-based programming, but they serve the same purpose: acting as a framework from which objects will be created.
Second, we create a constructor. In class-based languages, the constructor is defined inside of the class so it’s clear when instantiating the object which constructor goes with which class. In JavaScript, the object constructor is set outside of the proto- type, so an additional step is needed to link them together.
Finally, the objects are instantiated.
JavaScript’s use of the new operator is a departure from its prototype-based roots, perhaps as an attempt to make it more comprehensible to developers familiar with class-based inheritance. Unfortunately, we think it clouds the issue and makes some- thing that should be unfamiliar (and therefore studied) appear to be familiar, causing
Table 2.3 Multiple objects: class versus prototype
Class-based Prototype-based
/* step 1 */
public class Prisoner { public int sentence = 4;
public int probation = 2;
public string name;
public string id;
/* step 2 */
public Prisoner( string name, string id ) {
this.name = name;
this.id = id;
} }
/* step 3 */
Prisoner firstPrisoner
= new Prisoner("Joe","12A");
Prisoner secondPrisoner
= new Prisoner("Sam","2BC");
1 Define the class
2 Define the class constructor 3 Instantiate the objects
// * step 1 * var proto = {
sentence : 4, probation : 2 };
//* step 2 * var Prisoner =
function(name, id){
this.name = name;
this.id = id;
};
//* step 3 *
Prisoner.prototype = proto;
// * step 4 * var firstPrisoner =
new Prisoner('Joe','12A');
var secondPrisoner =
new Prisoner('Sam','2BC');
1 Define prototype object 2 Define the object constructor 3 Link constructor to prototype 4 Instantiate the objects
39 JavaScript objects and the prototype chain
developers to jump in until they run into issues and spend hours trying to figure out a bug caused by mistaking JavaScript for a class-based system.
As an alternative to using the new operator, the method Object.create has been developed and is used to add a more prototype-based feel to JavaScript object cre- ation. We use the Object.create method exclusively throughout the book. Creating prisoners from the prototype-based example from table 2.3 using Object.create would look like this:
var proto = { sentence : 4, probation : 2 };
var firstPrisoner = Object.create( proto );
firstPrisoner.name = 'Joe';
firstPrisoner.id = '12A';
var secondPrisoner = Object.create( proto );
secondPrisoner.name = 'Sam;
secondPrisoner.id = '2BC';
Object.create takes the prototype as an argument and returns an object; in this way you can define the common attributes and methods on a prototype object and use it to create many objects sharing the same properties. Having to set the name and id on each of them manually is a pain because having to repeat code isn’t very clean. As an alter- native, a common pattern for using Object.create is to use a factory function that cre- ates and returns the final object. We name all our factory functions make<object_name>.
var proto = { sentence : 4, probation : 2 };
var makePrisoner = function( name, id ) { var prisoner = Object.create( proto );
prisoner.name = name;
prisoner.id = id;
return prisoner;
};
var firstPrisoner = makePrisoner( 'Joe', '12A' );
var secondPrisoner = makePrisoner( 'Sam', '2BC' );
Though there are a number of alternative methods to create objects in JavaScript (it’s another oft-debated developer topic), it’s generally considered a best practice to use Object.create. We prefer this method as it clearly illustrates how the prototype is set.
The new operator is, unfortunately, perhaps the most commonly used method to create Listing 2.9 Using Object.create to create objects
Listing 2.10 Use of Object.create with a factory function
makePrisoner is the factory function; it creates prisoner objects.
The object creation is identical to the previous listing, just wrapped inside of the factory function.
Now we can create new prisoners by calling the makePrisoner function and passing in their name and id.
objects. We say unfortunate because it misleads developers into thinking the language is class-based and obscures the nuances of the prototype-based system.
Now that we’ve seen how JavaScript uses prototypes to create objects sharing the same properties, let’s dig into the prototype chain and talk about how the JavaScript engine implements finding the value of attributes on an object.
2.5.1 The prototype chain
Attributes on an object are implemented and function differently in prototype-based JavaScript than in a class-based system. There are enough similarities that most of the time we can get along without a clear understanding, but when the differences rear their ugly heads, we pay the price in frustration and lost productivity. Just like learning the basic differences between prototypes and classes is worth it up front, so is learning about the prototype chain.
JavaScript uses the prototype chain to resolve property values. The prototype chain describes how the JavaScript engine looks from object to the object's prototype to the prototype's prototype in order to locate the value of a property of the object. When we request an object’s property, the JavaScript engine first looks for the property directly on the object. If it can’t find the property there, it looks at the prototype (stored in the __proto__ property of objects) and sees if the prototype contains the requested property.
If the JavaScript engine can’t find the property in the objects prototype, it checks the prototype’s prototype (the prototype is just an object, so it has a prototype as well).
And so on. This prototype chain ends when JavaScript reaches the generic Object prototype. If JavaScript can’t find the requested property anywhere in the chain, it returns undefined. The details can get intricate as the JavaScript engine checks up the prototype chain, but for the purposes of this book we just need to remember that if a property isn’t found on the object, the prototype is checked.
Object.create for older browsers
Object.create works in IE 9+, Firefox 4+, Safari 5+, and Chrome 5+. In order to be compatible across older browsers (we’re looking at you IE 6, 7, and 8!), we need to define Object.create when it doesn’t exist and leave it unchanged for browsers that have already implemented it.
// Cross-browser method to support Object.create() var objectCreate = function ( arg ){
if ( ! arg ) { return {}; } function obj() {};
obj.prototype = arg;
return new obj;
};
Object.create = Object.create || objectCreate;
41 JavaScript objects and the prototype chain
This climb up the prototype chain is similar to the JavaScript engine’s climb up the scope chain to find a variable’s definition. As you can see in figure 2.5, the concept is nearly identical to the scope chain from figure 2.4.
You can climb the prototype chain manually with the __proto__ property.
var proto = { sentence : 4, probation : 2 };
var makePrisoner = function( name, id ) { var prisoner = Object.create( proto );
prisoner.name = name;
prisoner.id = id;
return prisoner;
};
var firstPrisoner = makePrisoner( 'Joe', '12A' );
// The entire object, including properties of the prototype // {"id": "12A", "name": "Joe", "probation": 2, "sentence": 4}
console.log( firstPrisoner );
// Just the prototype properties // {"probation": 2, "sentence": 4}
console.log( firstPrisoner.__proto__ );
// The prototype is an object with a prototype. Since one toString:function()
firstPrisoner.__proto__.__proto__
firstPrisoner.__proto__
firstPrisoner sentence: 4
name: joe
Figure 2.5 During runtime JavaScript searches the prototype chain to resolve property values.
// wasn't set, the prototype is the generic object prototype, // represented as empty curly braces.
// {}
console.log( firstPrisoner.__proto__.__proto__ );
// But the generic object prototype has no prototype // null
console.log( firstPrisoner.__proto__.__proto__.__proto__ );
// and trying to get the prototype of null is an error // "firstPrisoner.__proto__.__proto__.__proto__ is null"
console.log( firstPrisoner.__proto__.__proto__.__proto__.__proto__ );
If we request firstPrisoner.name, JavaScript will find the name of the prisoner directly on the object and return Joe. If we request firstPrisoner.sentence, JavaScript won’t find the property on the object, but would find it in the prototype and return the value of 4. And if we request firstPrisoner.toString(), we’ll get the string [object Object] because the base Object prototype has that method. Finally, if we request firstPrisoner.hopeless, we’ll get undefined, as that property is nowhere to be found in the prototype chain. These results are summarized in table 2.4.
Table 2.4 Prototype chain
Requested property Prototype chain
firstPrisoner {
id: '12A', name: 'Joe', __proto__: {
probation: 2, sentence: 4, __proto__: {
toString : function () {}
} } } firstPrisoner.name {
id: '12A', name: 'Joe', __proto__: {
probation: 2, sentence: 4, __proto__: {
toString : function () {}
} } }
firstPrisoner object created above, its prototype, and the prototype’s prototype, the JavaScript base object.
name is accessed directly on the firstPrisoner object.
43 JavaScript objects and the prototype chain
firstPrisoner.sentence {
id: '12A', name: 'Joe', __proto__: {
probation: 2, sentence: 4, __proto__: {
toString : function () {}
} } } firstPrisoner.toString {
id: '12A', name: 'Joe', __proto__: {
probation: 2, sentence: 4, __proto __ :
toString : function () { [ native code ]
} } } } firstPrisoner.hopeless {
id: '12A', name: 'Joe', __proto__: {
probation: 2, sentence: 4, __proto __ :
toString : function () { [ native code ]
} } } } Table 2.4 Prototype chain (continued)
Requested property Prototype chain
sentence attribute isn’t accessible on the
firstPrisoner object, so it looks to the prototype, where it finds it.
toString() isn’t available on the object or its prototype, so it looks at the prototype’s prototype, which happens to be the base JavaScript object.
hopeless isn't defined on the object . . .
. . . or the prototype . . .
. . . or the prototype's prototype, so its value is undefined.
Another way to demonstrate the prototype chain is to see what happens when we change a value on an object set by the prototype.
var proto = { sentence : 4, probation : 2 };
var makePrisoner = function( name, id ) { var prisoner = Object.create( proto );
prisoner.name = name;
prisoner.id = id;
return prisoner;
};
var firstPrisoner = makePrisoner( 'Joe', '12A' );
// Both of these output 4
console.log( firstPrisoner.sentence );
console.log( firstPrisoner.__proto__.sentence );
firstPrisoner.sentence = 10;
// Outputs 10
console.log( firstPrisoner.sentence );
// Outputs 4
console.log( firstPrisoner.__proto__.sentence );
delete firstPrisoner.sentence;
// Both of these output 4
console.log( firstPrisoner.sentence );
console.log( firstPrisoner.__proto__.sentence );
So what happens, I can hear you thinking, if we change the value of the attribute on the prototype object?
PROTOTYPE MUTATIONS
One powerful—and potentially dangerous—behavior that prototype inheritance pro- vides is the ability to mutate all objects based on a prototype at once. For those famil- iar with static variables, attributes on the prototype act like static variables for objects created from the prototype. Let’s check out our code one more time.
var proto = { sentence : 4, probation : 2 };
var makePrisoner = function( name, id ) { Listing 2.11 Overwriting the prototype
firstPrisoner .sentence doesn’t find a sentence attribute on the firstPrisoner object, so it looks at the object’s prototype
and finds it. Set the object’s
sentence property to 10.
Confirm that the value was set to 10 on the
object... ...but the prototype
of that object remains untouched and is still 4 To get the attribute
back to the value of the prototype, we delete the attribute from the object.
The next time, the JavaScript engine can no longer find the attribute on the object and must look back up the prototype chain to find the attribute on the prototype object.
45 Functions—a deeper look
var prisoner = Object.create( proto );
prisoner.name = name;
prisoner.id = id;
return prisoner;
};
var firstPrisoner = makePrisoner( 'Joe', '12A' );
var secondPrisoner = makePrisoner( 'Sam', '2BC' );
If, after the preceding example, we inspect firstPrisoner or secondPrisoner, we’ll find that the inherited property sentence is set to 4.
...
// Both of these output '4'
console.log( firstPrisoner.sentence );
console.log( secondPrisoner.sentence );
If we change the prototype object, for example by setting proto.sentence=5, then all objects created after and before will reflect this value. Thus firstPrisoner.sentence and secondPrisoner.sentence are set to 5.
...
proto.sentence = 5;
// Both of these output '5'
console.log( firstPrisoner.sentence );
console.log( secondPrisoner.sentence );
This behavior has good and bad points. The important thing is that it’s consistent across JavaScript environments and that we know about it so we can code accordingly.
Now that we know how objects inherit properties from other objects using proto- types, let’s look at how functions work, because they may also behave differently than you’d expect. We’ll also investigate how these differences can provide some useful capabilities that we take advantage of throughout the book.