Constructors and Prototypes - The Principles of Object-Oriented Javascript (2014)

The Principles of Object-Oriented Javascript (2014)

Chapter 4. Constructors and Prototypes

You might be able to get pretty far in JavaScript without understanding constructors and prototypes, but you won’t truly appreciate the language without a good grasp of them. Because JavaScript lacks classes, it turns to constructors and prototypes to bring a similar order to objects. But just because some of the patterns resemble classes doesn’t mean they behave the same way. In this chapter, you’ll explore constructors and prototypes in detail to see how JavaScript uses them to create objects.

Constructors

A constructor is simply a function that is used with new to create an object. Up to this point, you’ve seen several of the built-in JavaScript constructors, such as Object, Array, and Function. The advantage of constructors is that objects created with the same constructor contain the same properties and methods. If you want to create multiple similar objects, you can create your own constructors and therefore your own reference types.

Because a constructor is just a function, you define it in the same way. The only difference is that constructor names should begin with a capital letter, to distinguish them from other functions. For example, look at the following empty Person function:

function Person() {

// intentionally empty

}

This function is a constructor, but there is absolutely no syntactic difference between this and any other function. The clue that Person is a constructor is in the name—the first letter is capitalized.

After the constructor is defined, you can start creating instances, like the following two Person objects:

var person1 = new Person();

var person2 = new Person();

When you have no parameters to pass into your constructor, you can even omit the parentheses:

var person1 = new Person;

var person2 = new Person;

Even though the Person constructor doesn’t explicitly return anything, both person1 and person2 are considered instances of the new Person type. The new operator automatically creates an object of the given type and returns it. That also means you can use the instanceof operator to deduce an object’s type. The following code shows instanceof in action with the newly created objects:

console.log(person1 instanceof Person); // true

console.log(person2 instanceof Person); // true

Because person1 and person2 were created with the Person constructor, instanceof returns true when it checks whether these objects are instances of the Person type.

You can also check the type of an instance using the constructor property. Every object instance is automatically created with a constructor property that contains a reference to the constructor function that created it. For generic objects (those created via an object literal or the Objectconstructor), constructor is set to Object; for objects created with a custom constructor, constructor points back to that constructor function instead. For example, Person is the constructor property for person1 and person2:

console.log(person1.constructor === Person); // true

console.log(person2.constructor === Person); // true

The console.log function outputs true in both cases, because both objects were created with the Person constructor.

Even though this relationship exists between an instance and its constructor, you are still advised to use instanceof to check the type of an instance. This is because the constructor property can be overwritten and therefore may not be completely accurate.

Of course, an empty constructor function isn’t very useful. The whole point of a constructor is to make it easy to create more objects with the same properties and methods. To do that, simply add any properties you want to this inside of the constructor, as in the following example:

function Person(name) {

this.name = name;

this.sayName = function() {

console.log(this.name);

};

}

This version of the Person constructor accepts a single named parameter, name, and assigns it to the name property of the this object ❶. The constructor also adds a sayName() method to the object ❷. The this object is automatically created by new when you call the constructor, and it is an instance of the constructor’s type. (In this case, this is an instance of Person.) There’s no need to return a value from the function because the new operator produces the return value.

Now you can use the Person constructor to create objects with an initialized name property:

var person1 = new Person("Nicholas");

var person2 = new Person("Greg");

console.log(person1.name); // "Nicholas"

console.log(person2.name); // "Greg"

person1.sayName(); // outputs "Nicholas"

person2.sayName(); // outputs "Greg"

Each object has its own name property, so sayName() should return different values depending on the object on which you use it.

NOTE

You can also explicitly call return inside of a constructor. If the returned value is an object, it will be returned instead of the newly created object instance. If the returned value is a primitive, the newly created object is used and the returned value is ignored.

Constructors allow you to initialize an instance of a type in a consistent way, performing all of the property setup that is necessary before the object can be used. For example, you could also use Object.defineProperty() inside of a constructor to help initialize the instance:

function Person(name) {

Object.defineProperty(this, "name", {

get: function() {

return name;

},

set: function(newName) {

name = newName;

},

enumerable: true,

configurable: true

});

this.sayName = function() {

console.log(this.name);

};

}

In this version of the Person constructor, the name property is an accessor property that uses the name parameter for storing the actual name. This is possible because named parameters act like local variables.

Make sure to always call constructors with new; otherwise, you risk changing the global object instead of the newly created object. Consider what happens in the following code:

var person1 = Person("Nicholas"); // note: missing "new"

console.log(person1 instanceof Person); // false

console.log(typeof person1); // "undefined"

console.log(name); // "Nicholas"

When Person is called as a function without new, the value of this inside of the constructor is equal to the global this object. The variable person1 doesn’t contain a value because the Person constructor relies on new to supply a return value. Without new, Person is just a function without a return statement. The assignment to this.name actually creates a global variable called name, which is where the name passed to Person is stored. Chapter 6 describes a solution to both this problem and more complex object composition patterns.

NOTE

An error occurs if you call the Person constructor in strict mode without using new. This is because strict mode doesn’t assign this to the global object. Instead, this remains undefined, and an error occurs whenever you attempt to create a property on undefined.

Constructors allow you to configure object instances with the same properties, but constructors alone don’t eliminate code redundancy. In the example code thus far, each instance has had its own sayName() method even though sayName() doesn’t change. That means if you have 100 instances of an object, then there are 100 copies of a function that do the exact same thing, just with different data.

It would be much more efficient if all of the instances shared one method, and then that method could use this.name to retrieve the appropriate data. This is where prototypes come in.

Prototypes

You can think of a prototype as a recipe for an object. Almost every function (with the exception of some built-in functions) has a prototype property that is used during the creation of new instances. That prototype is shared among all of the object instances, and those instances can access properties of the prototype. For example, the hasOwnProperty() method is defined on the generic Object prototype, but it can be accessed from any object as if it were an own property, as shown in this example:

var book = {

title: "The Principles of Object-Oriented JavaScript"

};

console.log("title" in book); // true

console.log(book.hasOwnProperty("title")); // true

console.log("hasOwnProperty" in book); // true

console.log(book.hasOwnProperty("hasOwnProperty")); // false

console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); // true

Even though there is no definition for hasOwnProperty() on book, that method can still be accessed as book.hasOwnProperty() because the definition does exist on Object.prototype. Remember that the in operator returns true for both prototype properties and own properties.

IDENTIFYING A PROTOTYPE PROPERTY

You can determine whether a property is on the prototype by using a function such as:

function hasPrototypeProperty(object, name) {

return name in object && !object.hasOwnProperty(name);

}

console.log(hasPrototypeProperty(book, "title")); // false

console.log(hasPrototypeProperty(book, "hasOwnProperty")); // true

If the property is in an object but hasOwnProperty() returns false, then the property is on the prototype.

The [[Prototype]] Property

An instance keeps track of its prototype through an internal property called [[Prototype]]. This property is a pointer back to the prototype object that the instance is using. When you create a new object using new, the constructor’s prototype property is assigned to the [[Prototype]]property of that new object. Figure 4-1 shows how the [[Prototype]] property lets multiple instances of an object type refer to the same prototype, which can reduce code duplication.

The [[Prototype]] properties for person1 and person2 point to the same prototype.

Figure 4-1. The [[Prototype]] properties for person1 and person2 point to the same prototype.

You can read the value of the [[Prototype]] property by using the Object.getPrototypeOf() method on an object. For example, the following code checks the [[Prototype]] of a generic, empty object.

var object = {};

var prototype = Object.getPrototypeOf(object);

console.log(prototype === Object.prototype); // true

For any generic object like this one ❶, [[Prototype]] is always a reference to Object.prototype.

NOTE

Some JavaScript engines also support a property called __proto__ on all objects. This property allows you to both read from and write to the [[Prototype]] property. Firefox, Safari, Chrome, and Node.js all support this property, and __proto__ is on the path for standardization in ECMAScript 6.

You can also test to see if one object is a prototype for another by using the isPrototypeOf() method, which is included on all objects:

var object = {};

console.log(Object.prototype.isPrototypeOf(object)); // true

Because object is just a generic object, its prototype should be Object.prototype, meaning isPrototypeOf() should return true.

When a property is read on an object, the JavaScript engine first looks for an own property with that name. If the engine finds a correctly named own property, it returns that value. If no own property with that name exists on the target object, JavaScript searches the [[Prototype]] object instead. If a prototype property with that name exists, the value of that property is returned. If the search concludes without finding a property with the correct name, undefined is returned.

Consider the following, in which an object is first created without any own properties:

var object = {};

❶ console.log(object.toString()); // "[object Object]"

object.toString = function() {

return "[object Custom]";

};

❷ console.log(object.toString()); // "[object Custom]"

// delete own property

delete object.toString;

❸ console.log(object.toString()); // "[object Object]"

// no effect - delete only works on own properties

delete object.toString;

console.log(object.toString()); // "[object Object]"

In this example, the toString() method comes from the prototype and returns "[object Object]" ❶ by default. If you then define an own property called toString(), that own property is used whenever toString() is called on the object again ❷. The own property shadows the prototype property, so the prototype property of the same name is no longer used. The prototype property is used again only if the own property is deleted from the object ❸. (Keep in mind that you can’t delete a prototype property from an instance because the delete operator acts only on own properties.) Figure 4-2 shows what is happening in this example.

This example also highlights an important concept: You cannot assign a value to a prototype property from an instance. As you can see in the middle section of Figure 4-2, assigning a value to toString creates a new own property on the instance, leaving the property on the prototype untouched.

An object with no own properties (top) has only the methods of its prototype. Adding a toString() property to the object (middle) replaces the prototype property until you delete it (bottom).

Figure 4-2. An object with no own properties (top) has only the methods of its prototype. Adding a toString() property to the object (middle) replaces the prototype property until you delete it (bottom).

Using Prototypes with Constructors

The shared nature of prototypes makes them ideal for defining methods once for all objects of a given type. Because methods tend to do the same thing for all instances, there’s no reason each instance needs its own set of methods.

It’s much more efficient to put the methods on the prototype and then use this to access the current instance. For example, consider the following new Person constructor:

function Person(name) {

this.name = name;

}

❶ Person.prototype.sayName = function() {

console.log(this.name);

};

var person1 = new Person("Nicholas");

var person2 = new Person("Greg");

console.log(person1.name); // "Nicholas"

console.log(person2.name); // "Greg"

person1.sayName(); // outputs "Nicholas"

person2.sayName(); // outputs "Greg"

In this version of the Person constructor, sayName() is defined on the prototype ❶ instead of in the constructor. The object instances work exactly the same as the example from earlier in this chapter, even though sayName() is now a prototype property instead of an own property. Becauseperson1 and person2 are each base references for their calls to sayName(), the this value is assigned to person1 and person2, respectively.

You can also store other types of data on the prototype, but be careful when using reference values. Because these values are shared across instances, you might not expect one instance to be able to change values that another instance will access. This example shows what can happen when you don’t watch where your reference values are pointing:

function Person(name) {

this.name = name;

}

Person.prototype.sayName = function() {

console.log(this.name);

};

❶ Person.prototype.favorites = [];

var person1 = new Person("Nicholas");

var person2 = new Person("Greg");

person1.favorites.push("pizza");

person2.favorites.push("quinoa");

console.log(person1.favorites); // "pizza,quinoa"

console.log(person2.favorites); // "pizza,quinoa"

The favorites property ❶ is defined on the prototype, which means person1.favorites and person2.favorites point to the same array. Any values you add to either person’s favorites will be elements in that array on the prototype. That may not be the behavior that you actually want, so it’s important to be very careful about what you define on the prototype.

Even though you can add properties to the prototype one by one, many developers use a more succinct pattern that involves replacing the prototype with an object literal:

function Person(name) {

this.name = name;

}

Person.prototype = {

❶ sayName: function() {

console.log(this.name);

},

❷ toString: function() {

return "[Person " + this.name + "]";

}

};

This code defines two methods on the prototype, sayName() ❶ and toString() ❷. This pattern has become quite popular because it eliminates the need to type Person.prototype multiple times. There is, however, one side effect to be aware of:

var person1 = new Person("Nicholas");

console.log(person1 instanceof Person); // true

console.log(person1.constructor === Person); // false

❶ console.log(person1.constructor === Object); // true

Using the object literal notation to overwrite the prototype changed the constructor property so that it now points to Object ❶ instead of Person. This happened because the constructor property exists on the prototype, not on the object instance. When a function is created, itsprototype property is created with a constructor property equal to the function. This pattern completely overwrites the prototype object, which means that constructor will come from the newly created (generic) object that was assigned to Person.prototype. To avoid this, restore the constructor property to a proper value when overwriting the prototype:

function Person(name) {

this.name = name;

}

Person.prototype = {

❶ constructor: Person,

sayName: function() {

console.log(this.name);

},

toString: function() {

return "[Person " + this.name + "]";

}

};

var person1 = new Person("Nicholas");

var person2 = new Person("Greg");

console.log(person1 instanceof Person); // true

console.log(person1.constructor === Person); // true

console.log(person1.constructor === Object); // false

console.log(person2 instanceof Person); // true

console.log(person2.constructor === Person); // true

console.log(person2.constructor === Object); // false

In this example, the constructor property is specifically assigned on the prototype ❶. It’s good practice to make this the first property on the prototype so you don’t forget to include it.

Perhaps the most interesting aspect of the relationships among constructors, prototypes, and instances is that there is no direct link between the instance and the constructor. There is, however, a direct link between the instance and the prototype and between the prototype and the constructor.Figure 4-3 illustrates this relationship.

An instance and its constructor are linked via the prototype.

Figure 4-3. An instance and its constructor are linked via the prototype.

This nature of this relationship means that any disruption between the instance and the prototype will also create a disruption between the instance and the constructor.

Changing Prototypes

Because all instances of a particular type reference a shared prototype, you can augment all of those objects together at any time. Remember, the [[Prototype]] property just contains a pointer to the prototype, and any changes to the prototype are immediately available on any instance referencing it. That means you can literally add new members to a prototype at any point and have those changes reflected on existing instances, as in this example:

function Person(name) {

this.name = name;

}

Person.prototype = {

constructor: Person,

❶ sayName: function() {

console.log(this.name);

},

❷ toString: function() {

return "[Person " + this.name + "]";

}

};

var person1 = new Person("Nicholas");

var person2 = new Person("Greg");

console.log("sayHi" in person1); // false

console.log("sayHi" in person2); // false

// add a new method

❹ Person.prototype.sayHi = function() {

console.log("Hi");

};

❺ person1.sayHi(); // outputs "Hi"

person2.sayHi(); // outputs "Hi"

In this code, the Person type starts out with only two methods, sayName() ❶ and toString() ❷. Two instances of Person are created ❸, and then the sayHi() ❹ method is added to the prototype. After that point, both instances can now access sayHi() ❺. The search for a named property happens each time that property is accessed, so the experience is seamless.

The ability to modify the prototype at any time has some interesting repercussions for sealed and frozen objects. When you use Object.seal() or Object.freeze() on an object, you are acting solely on the object instance and the own properties. You can’t add new own properties or change existing own properties on frozen objects, but you can certainly still add properties on the prototype and continue extending those objects, as demonstrated in the following listing.

var person1 = new Person("Nicholas");

var person2 = new Person("Greg");

❶ Object.freeze(person1);

❷ Person.prototype.sayHi = function() {

console.log("Hi");

};

person1.sayHi(); // outputs "Hi"

person2.sayHi(); // outputs "Hi"

In this example, there are two instances of Person. The first (person1) is frozen ❶, while the second is a normal object. When you add sayHi() to the prototype ❷, both person1 and person2 attain a new method, seemingly contradicting person1’s frozen status. The [[Prototype]]property is considered an own property of the instance, and while the property itself is frozen, the value (an object) is not.

NOTE

In practice, you probably won’t use prototypes this way very often when developing in JavaScript. However, it’s important to understand the relationships that exist between objects and their prototype, and strange examples like this help to illuminate the concepts.

Built-in Object Prototypes

At this point, you might wonder if prototypes also allow you to modify the built-in objects that come standard in the JavaScript engine. The answer is yes. All built-in objects have constructors, and therefore, they have prototypes that you can change. For instance, adding a new method for use on all arrays is as simple as modifying Array.prototype.

Array.prototype.sum = function() {

return this.reduce(function(previous, current) {

return previous + current;

});

};

var numbers = [ 1, 2, 3, 4, 5, 6 ];

var result = numbers.sum();

console.log(result); // 21

This example creates a method called sum() on Array.prototype that simply adds up all of the items in the array and returns the result. The numbers array automatically has access to that method through the prototype. Inside of sum(), this refers to numbers, which is an instance ofArray, so the method is free to use other array methods such as reduce().

You may recall that strings, numbers, and Booleans all have built-in primitive wrapper types that are used to access primitive values as if they were objects. If you modify the primitive wrapper type prototype as in this example, you can actually add more functionality to those primitive values:

String.prototype.capitalize = function() {

return this.charAt(0).toUpperCase() + this.substring(1);

};

var message = "hello world!";

console.log(message.capitalize()); // "Hello world!"

This code creates a new method called capitalize() for strings. The String type is the primitive wrapper for strings, and modifying its prototype means that all strings automatically get those changes.

NOTE

While it may be fun and interesting to modify built-in objects to experiment with functionality, it’s not a good idea to do so in a production environment. Developers expect built-in objects to behave a certain way and have certain methods. Deliberately altering built-in objects violates those expectations and makes other developers unsure how the objects should work.

Summary

Constructors are just normal functions that are called with the new operator. You can define your own constructors anytime you want to create multiple objects with the same properties. You can identify objects created from constructors using instanceof or by accessing theirconstructor property directly.

Every function has a prototype property that defines any properties shared by objects created with a particular constructor. Shared methods and primitive value properties are typically defined on prototypes, while all other properties are defined within the constructor. The constructorproperty is actually defined on the prototype because it is shared among object instances.

The prototype of an object is stored internally in the [[Prototype]] property. This property is a reference, not a copy. If you change the prototype at any point in time, those changes will occur on all instances because of the way JavaScript looks up properties. When you try to access a property on an object, that object is searched for any own property with the name you specify. If an own property is not found, the prototype is searched. This searching mechanism means the prototype can continue to change, and object instances referencing that prototype will reflect those changes immediately.

Built-in objects also have prototypes that can be modified. While it’s not recommended to do this in production, it can be helpful for experimentation and proofs of concept for new functionality.