Prototype - Object-Oriented JavaScript Second Edition (2013)

Object-Oriented JavaScript Second Edition (2013)

Chapter 5. Prototype

In this chapter, you'll learn about the prototype property of the function objects. Understanding how the prototype works is an important part of learning the JavaScript language. After all, JavaScript is often classified as having a prototype-based object model. There's nothing particularly difficult about the prototype, but it's a new concept, and as such may sometimes take a bit of time to sink in. Like closures (see Chapter 3, Functions), the prototype is one of those things in JavaScript, which once you "get", they seem so obvious and make perfect sense. As with the rest of the book, you're strongly encouraged to type in and play around with the examples—this makes it much easier to learn and remember the concepts.

The following topics are discussed in this chapter:

· Every function has a prototype property and it contains an object

· Adding properties to the prototype object

· Using the properties added to the prototype

· The difference between own properties and properties of the prototype

· __proto__, the secret link every object keeps to its prototype

· Methods such as isPrototypeOf(), hasOwnProperty(), and propertyIsEnumerable()

· Enhancing built-in objects, such as arrays or strings (and why that can be a bad idea)

The prototype property

The functions in JavaScript are objects, and they contain methods and properties. Some of the methods that you're already familiar with are apply() and call(), and some of the other properties are length and constructor. Another property of the function objects isprototype.

If you define a simple function, foo(), you can access its properties as you would do with any other object.

> function foo(a, b) {

return a * b;

}

> foo.length;

2

> foo.constructor;

function Function() { [native code] }

The prototype property is a property that is available to you as soon as you define the function. Its initial value is an "empty" object.

> typeof foo.prototype;

"object"

It's as if you added this property yourself as follows:

> foo.prototype = {};

You can augment this empty object with properties and methods. They won't have any effect on the foo() function itself; they'll only be used if you call foo() as a constructor.

Adding methods and properties using the prototype

In the previous chapter, you learned how to define constructor functions that you can use to create (construct) new objects. The main idea is that inside a function invoked with new, you have access to the value this, which refers to the object to be returned by the constructor. Augmenting (adding methods and properties to) this is how you add functionality to the object being constructed.

Let's take a look at the constructor function Gadget(), which uses this to add two properties and one method to the objects it creates.

function Gadget(name, color) {

this.name = name;

this.color = color;

this.whatAreYou = function () {

return 'I am a ' + this.color + ' ' + this.name;

};

}

Adding methods and properties to the prototype property of the constructor function is another way to add functionality to the objects this constructor produces. Let's add two more properties, price and rating, as well as a getInfo() method. Since prototype already points to an object, you can just keep adding properties and methods to it as follows:

Gadget.prototype.price = 100;

Gadget.prototype.rating = 3;

Gadget.prototype.getInfo = function () {

return 'Rating: ' + this.rating +

', price: ' + this.price;

};

Alternatively, instead of adding properties to the prototype object one by one, you can overwrite the prototype completely, replacing it with an object of your choice.

Gadget.prototype = {

price: 100,

rating: ... /* and so on... */

};

Using the prototype's methods and properties

All the methods and properties you have added to the prototype are available as soon as you create a new object using the constructor. If you create a newtoy object using the Gadget() constructor, you can access all the methods and properties already defined.

> var newtoy = new Gadget('webcam', 'black');

> newtoy.name;

"webcam"

> newtoy.color;

"black"

> newtoy.whatAreYou();

"I am a black webcam"

> newtoy.price;

100

> newtoy.rating;

3

> newtoy.getInfo();

"Rating: 3, price: 100"

It's important to note that the prototype is "live". Objects are passed by reference in JavaScript, and therefore, the prototype is not copied with every new object instance. What does this mean in practice? It means that you can modify the prototype at any time and all the objects (even those created before the modification) will "see" the changes.

Let's continue the example by adding a new method to the prototype:

Gadget.prototype.get = function (what) {

return this[what];

};

Even though newtoy was created before the get() method was defined, newtoy still has access to the new method:

> newtoy.get('price');

100

> newtoy.get('color');

"black"

Own properties versus prototype properties

In the preceding example, getInfo() was used internally to access the properties of the object. It could've also used Gadget.prototype to achieve the same output.

Gadget.prototype.getInfo = function () {

return 'Rating: ' + Gadget.prototype.rating +

', price: ' + Gadget.prototype.price;

};

What's the difference? To answer this question, let's examine how the prototype works in more detail.

Let's take the newtoy object again.

var newtoy = new Gadget('webcam', 'black');

When you try to access a property of newtoy, say, newtoy.name, the JavaScript engine looks through all of the properties of the object searching for one called name, and if it finds it, it returns its value.

> newtoy.name;

"webcam"

What if you try to access the rating property? The JavaScript engine examines all of the properties of newtoy and doesn't find the one called rating. Then, the script engine identifies the prototype of the constructor function used to create this object (the same as if you do newtoy.constructor.prototype). If the property is found in the prototype object, the following property is used:

> newtoy.rating;

3

You can do the same and access the prototype directly. Every object has a constructor property, which is a reference to the function that created the object, so in this case:

> newtoy.constructor === Gadget;

true

> newtoy.constructor.prototype.rating;

3

Now, let's take this lookup one step further. Every object has a constructor. The prototype is an object, so it must have a constructor too, which, in turn, has a prototype. You can go up the prototype chain and you will eventually end up with the built-in Object() object, which is the highest-level parent. In practice, this means that if you try newtoy.toString() and newtoy doesn't have its own toString() method and its prototype doesn't either, in the end you'll get the object's toString() method:

> newtoy.toString();

"[object Object]"

Overwriting a prototype's property with an own property

As the above discussion demonstrates, if one of your objects doesn't have a certain property of its own, it can use one (if it exists) somewhere up the prototype chain. What if the object does have its own property and the prototype also has one with the same name? Then, the own property takes precedence over the prototype's.

Consider a scenario where a property name exists as both an own property and a property of the prototype object.

> function Gadget(name) {

this.name = name;

}

> Gadget.prototype.name = 'mirror';

Creating a new object and accessing its name property gives you the object's own name property.

> var toy = new Gadget('camera');

> toy.name;

"camera"

You can tell where the property was defined by using hasOwnProperty().

> toy.hasOwnProperty('name');

true

If you delete the toy object's own name property, the prototype's property with the same name "shines through".

> delete toy.name;

true

> toy.name;

"mirror"

> toy.hasOwnProperty('name');

false

Of course, you can always recreate the object's own property.

> toy.name = 'camera';

> toy.name;

"camera"

You can play around with the method hasOwnProperty() to find out the origins of a particular property you're curious about. The method toString() was mentioned earlier. Where is it coming from?

> toy.toString();

"[object Object]"

> toy.hasOwnProperty('toString');

false

> toy.constructor.hasOwnProperty('toString');

false

> toy.constructor.prototype.hasOwnProperty('toString');

false

> Object.hasOwnProperty('toString');

false

> Object.prototype.hasOwnProperty('toString');

true

Ahaa!

Enumerating properties

If you want to list all the properties of an object, you can use a for-in loop. In Chapter 2, Primitive Data Types, Arrays, Loops, and Conditions, you saw that you can also loop through all the elements of an array with for-in, but as mentioned there, for is better suited for arrays and for-in is for objects. Let's take an example of constructing a query string for a URL from an object:

var params = {

productid: 666,

section: 'products'

};

var url = 'http://example.org/page.php?',

i,

query = [];

for (i in params) {

query.push(i + '=' + params[i]);

}

url += query.join('&');

This produces the url string as follows:

"http://example.org/page.php?productid=666&section=products"

There are a few details to be aware of:

· Not all properties show up in a for-in loop. For example, the length (for arrays) and constructor properties don't show up. The properties that do show up are called enumerable. You can check which ones are enumerable with the help of thepropertyIsEnumerable() method that every object provides. In ES5, you can specify which properties are enumerable, while in ES3 you don't have that control.

· Prototypes that come through the prototype chain also show up, provided they are enumerable. You can check if a property is an object's own property or a prototype's property using the hasOwnProperty() method.

· propertyIsEnumerable() returns false for all of the prototype's properties, even those that are enumerable and show up in the for-in loop.

Let's see these methods in action. Take this simplified version of Gadget():

function Gadget(name, color) {

this.name = name;

this.color = color;

this.getName = function () {

return this.name;

};

}

Gadget.prototype.price = 100;

Gadget.prototype.rating = 3;

Create a new object as follows:

var newtoy = new Gadget('webcam', 'black');

Now, if you loop using a for-in loop, you see all of the object's properties, including those that come from the prototype:

for (var prop in newtoy) {

console.log(prop + ' = ' + newtoy[prop]);

}

The result also contains the object's methods (since methods are just properties that happen to be functions):

name = webcam

color = black

getName = function () {

return this.name;

}

price = 100

rating = 3

If you want to distinguish between the object's own properties and the prototype's properties, use hasOwnProperty(). Try the following first:

> newtoy.hasOwnProperty('name');

true

> newtoy.hasOwnProperty('price');

false

Let's loop again, but this time showing only the object's own properties.

for (var prop in newtoy) {

if (newtoy.hasOwnProperty(prop)) {

console.log(prop + '=' + newtoy[prop]);

}

}

The result is as follows:

name=webcam

color=black

getName = function () {

return this.name;

}

Now let's try propertyIsEnumerable(). This method returns true for the object's own properties that are not built-in.

> newtoy.propertyIsEnumerable('name');

true

Most built-in properties and methods are not enumerable.

> newtoy.propertyIsEnumerable('constructor');

false

Any properties coming down the prototype chain are not enumerable.

> newtoy.propertyIsEnumerable('price');

false

Note, however, that such properties are enumerable if you reach the object contained in the prototype and invoke its propertyIsEnumerable() method.

> newtoy.constructor.prototype.propertyIsEnumerable('price');

true

isPrototypeOf()

Objects also have the isPrototypeOf() method. This method tells you whether that specific object is used as a prototype of another object.

Let's take a simple object named monkey.

var monkey = {

hair: true,

feeds: 'bananas',

breathes: 'air'

};

Now let's create a Human() constructor function and set its prototype property to point to monkey.

function Human(name) {

this.name = name;

}

Human.prototype = monkey;

Now if you create a new Human object called george and ask "is monkey the prototype of george?", you'll get true.

> var george = new Human('George');

> monkey.isPrototypeOf(george);

true

Note that you have to know, or suspect, who the prototype is and then ask "is it true that your prototype is monkey?" in order to confirm your suspicion. But what if you don't suspect anything and you have no idea? Can you just ask the object to tell you its prototype? The answer is you can't in all browsers, but you can in most of them. Most recent browsers have implemented the addition to ES5 called Object.getPrototypeOf().

> Object.getPrototypeOf(george).feeds;

"bananas"

> Object.getPrototypeOf(george) === monkey;

true

For some of the pre-ES5 environments that don't have getPrototypeOf(), you can use the special property __proto__.

The secret __proto__ link

As you already know, the prototype property is consulted when you try to access a property that does not exist in the current object.

Consider another object called monkey and use it as a prototype when creating objects with the Human() constructor.

> var monkey = {

feeds: 'bananas',

breathes: 'air'

};

> function Human() {}

> Human.prototype = monkey;

Now, let's create a developer object and give it some properties.

> var developer = new Human();

> developer.feeds = 'pizza';

> developer.hacks = 'JavaScript';

Now let's access these properties. For example, hacks is a property of the developer object.

> developer.hacks;

"JavaScript"

feeds could also be found in the object.

> developer.feeds;

"pizza"

breathes doesn't exist as a property of the developer object, so the prototype is looked up, as if there is a secret link, or a secret passageway, that leads to the prototype object.

> developer.breathes;

"air"

The secret link is exposed in most modern JavaScript environments as the __proto__ property (the word "proto" with two underscores before and two after).

> developer.__proto__ === monkey;

true

You can use this secret property for learning purposes, but it's not a good idea to use it in your real scripts because it does not exist in all browsers (notably Internet Explorer), so your scripts won't be portable.

Be aware that __proto__ is not the same as prototype, since __proto__ is a property of the instances (objects), whereas prototype is a property of the constructor functions used to create those objects.

> typeof developer.__proto__;

"object"

> typeof developer.prototype;

"undefined"

> typeof developer.constructor.prototype;

"object"

Once again, you should use __proto__ only for learning or debugging purposes. Or, if you're lucky enough and your code only needs to work in ES5-compliant environments, you can use Object.getPrototypeOf().

Augmenting built-in objects

The objects created by the built-in constructor functions such as Array, String, and even Object and Function can be augmented (or enhanced) through the use of prototypes. This means that you can, for example, add new methods to the Array prototype, and in this way you can make them available to all arrays. Let's see how to do this.

In PHP, there is a function called in_array(), which tells you if a value exists in an array. In JavaScript, there is no inArray() method (although in ES5 there's indexOf(), which you can use for the same purpose). So, let's implement it and add it to Array.prototype.

Array.prototype.inArray = function (needle) {

for (var i = 0, len = this.length; i < len; i++) {

if (this[i] === needle) {

return true;

}

}

return false;

};

Now all arrays have access to the new method. Let's test this.

> var colors = ['red', 'green', 'blue'];

> colors.inArray('red');

true

> colors.inArray('yellow');

false

That was nice and easy! Let's do it again. Imagine your application often needs to spell words backwards and you feel there should be a built-in reverse() method for string objects. After all, arrays have reverse(). You can easily add a reverse() method to the Stringprototype by borrowing Array.prototype.reverse() (there was a similar exercise at the end of Chapter 4, Objects).

String.prototype.reverse = function () {

return Array.prototype.reverse.

apply(this.split('')).join('');

};

This code uses split() to create an array from a string, then calls the reverse() method on this array, which produces a reversed array. The resulting array is then turned back into a string using join(). Let's test the new method.

> "bumblebee".reverse();

"eebelbmub"

That is a nice name for a big and scary (and potentially hairy) mythical creature, isn't it?

Augmenting built-in objects – discussion

Augmenting built-in objects through the prototype is a powerful technique, and you can use it to shape JavaScript in any way you like. Because of its power, though, you should always thoroughly consider your options before using this approach.

The reason is that once you know JavaScript, you're expecting it to work the same way, no matter which third-party library or widget you're using. Modifying core objects could confuse the users and maintainers of your code and create unexpected errors.

JavaScript evolves and browser's vendors continuously support more features. What you consider a missing method today and decide to add to a core prototype could be a built-in method tomorrow. In this case, your method is no longer needed. Additionally, what if you have already written a lot of code that uses the method and your method is slightly different from the new built-in implementation?

The most common and acceptable use case for augmenting built-in prototypes is to add support for new features (ones that are already standardized by the ECMAScript committee and implemented in new browsers) to old browsers. One example would be adding an ES5 method to old versions of IE. These extensions are known as shims or polyfills.

When augmenting prototypes, you first check if the method exists before implementing it yourself. This way, you use the native implementation in the browser if one exists. For example, let's add the trim() method for strings, which is a method that exists in ES5 but is missing in older browsers.

if (typeof String.prototype.trim !== 'function') {

String.prototype.trim = function () {

return this.replace(/^\s+|\s+$/g,'');

};

}

> " hello ".trim();

"hello"

Tip

Best practice

If you decide to augment a built-in object or its prototype with a new property, do check for the existence of the new property first.

Prototype gotchas

There are two important behaviors to consider when dealing with prototypes:

· The prototype chain is live except when you completely replace the prototype object

· prototype.constructor is not reliable

Let's create a simple constructor function and two objects.

> function Dog() {

this.tail = true;

}

> var benji = new Dog();

> var rusty = new Dog();

Even after you've created the objects benji and rusty, you can still add properties to the prototype of Dog() and the existing objects will have access to the new properties. Let's throw in the method say().

> Dog.prototype.say = function () {

return 'Woof!';

};

Both objects have access to the new method.

> benji.say();

"Woof!"

rusty.say();

"Woof!"

Up to this point, if you consult your objects asking which constructor function was used to create them, they'll report it correctly.

> benji.constructor === Dog;

true

> rusty.constructor === Dog;

true

Now, let's completely overwrite the prototype object with a brand new object.

> Dog.prototype = {

paws: 4,

hair: true

};

It turns out that the old objects do not get access to the new prototype's properties; they still keep the secret link pointing to the old prototype object.

> typeof benji.paws;

"undefined"

> benji.say();

"Woof!"

> typeof benji.__proto__.say;

"function"

> typeof benji.__proto__.paws;

"undefined"

Any new objects you create from now on will use the updated prototype.

> var lucy = new Dog();

> lucy.say();

TypeError: lucy.say is not a function

> lucy.paws;

4

The secret __proto__ link points to the new prototype object.

> typeof lucy.__proto__.say;

"undefined"

> typeof lucy.__proto__.paws;

"number"

Now the constructor property of the new object no longer reports correctly. You would expect it to point to Dog(), but instead it points to Object().

> lucy.constructor;

function Object() { [native code] }

> benji.constructor;

function Dog() {

this.tail = true;

}

You can easily prevent this confusion by resetting the constructor property after you overwrite the prototype completely.

> function Dog() {}

> Dog.prototype = {};

> new Dog().constructor === Dog;

false

> Dog.prototype.constructor = Dog;

> new Dog().constructor === Dog;

true

Tip

Best practice

When you overwrite the prototype, remember to reset the constructor property.

Summary

Let's summarize the most important topics you have learned in this chapter:

· All functions have a property called prototype. Initially it contains an "empty" object (an object without any own properties).

· You can add properties and methods to the prototype object. You can even replace it completely with an object of your choice.

· When you create an object using a function as a constructor (with new), the object gets a secret link pointing to the prototype of the constructor, and can access the prototype's properties.

· An object's own properties take precedence over a prototype's properties with the same name.

· Use the method hasOwnProperty() to differentiate between an object's own properties and prototype properties.

· There is a prototype chain. When you execute foo.bar, and if your object foo doesn't have a property called bar, the JavaScript interpreter looks for a bar property in the prototype. If none is found, it keeps searching in the prototype's prototype, then the prototype of the prototype's prototype, and it will keep going all the way up to Object.prototype.

· You can augment the prototypes of built-in constructor functions and all objects will see your additions. Assign a function to Array.prototype.flip and all arrays will immediately get a flip() method, as in [1,2,3].flip(). But do check if the method/property you want to add already exists, so you can future-proof your scripts.

Exercises

1. Create an object called shape that has the type property and a getType() method.

2. Define a Triangle() constructor function whose prototype is shape. Objects created with Triangle() should have three own properties—a, b, and c, representing the lengths of the sides of a triangle.

3. Add a new method to the prototype called getPerimeter().

4. Test your implementation with the following code:

5. > var t = new Triangle(1, 2, 3);

6. > t.constructor === Triangle;

7.

8. true

9. > shape.isPrototypeOf(t);

10. true

11.> t.getPerimeter();

12. 6

13.> t.getType();

14.

15."triangle"

16. Loop over t showing only own properties and methods (none of the prototype's).

17. Make the following code work:

18.> [1, 2, 3, 4, 5, 6, 7, 8, 9].shuffle();

19.

20.[2, 4, 1, 8, 9, 6, 5, 3, 7]