-
Notifications
You must be signed in to change notification settings - Fork 4
Inheritance
- A little class theory
- Creating "empty" objects:
- Performance and hasOwnProperty
- The Internal Prototype
-
Implementing Inheritance
- 1. Pseudo-classical inheritance using prototype chaining
- 2. Inherit only the prototype
- Inheritance with objects
- 3. Temporary constructor
- 4. Copying the prototype properties
- 5. Copy all properties (shallow copy)
- 6. Deep copy same as above, but recurse into objects Works with objects Copies properties Same as #5, but copies the objects by value Used in more recent versions of jQuery
- 7. Prototypal inheritance
- 8. Extend and augment
- 9. Multiple inheritance
- 10. Parasitic inheritance
- 11. Borrowing constructors
- 12. Borrow a constructor and copy the prototype
- Inheritance with Object.create()
- Recommended Inheritance Implementation
Class/inheritance describes a certain form of code organization and architecture — a way of modeling real world problem domains in our software. OO or class-oriented programming stresses that data intrinsically has associated behavior (of course, different depending on the type and nature of the data!) that operates on it, so proper design is to package up (aka encapsulate) the data and the behavior together. This is sometimes called data structures in formal computer science. For example, a series of characters that represents a word or phrase is usually called a string. The characters are the data. But you almost never just care about the data, you usually want to do things with the data, so the behaviors that can apply to that data (calculating its length, appending data, searching, etc.) are all designed as methods of a String class. - Simpson, Kyle (2014-07-11). You Don't Know JS: this & Object Prototypes (pp. 65-66). O'Reilly Media. Kindle Edition.
In JavaScript, all of the following will create a new "empty" object:
var newObject = {};
var newObject = Object.create( Object.prototype );
var newObject = new Object();
All of these are object constructors.
Once we've created an object, we can then assign properties with values on them in any of the following 4 ways:
// ECMAScript 3 compatible approaches
// 1. Dot syntax
newObject.someKey = 'Hello World'; // Set properties
var value = newObject.someKey; // Get properties
// 2. Square bracket syntax
newObject['someKey'] = 'Hello World'; // Set properties
var value = newObject['someKey']; // Get properties
// ECMAScript 5 compatible approaches see: http://kangax.github.com/es5-compat-table/
// 3. Object.defineProperty
Object.defineProperty( newObject, 'someKey', { // Set properties
value: 'for more control of the property\'s behavior',
writable: true,
enumerable: true,
configurable: true
});
// If the above feels a little difficult to read, a short-hand could
// be written as follows:
var defineProp = function ( obj, key, value ){
var config = {
value: value,
writable: true,
enumerable: true,
configurable: true
};
Object.defineProperty( obj, key, config );
};
// To use, we then create a new empty "person" object
var person = Object.create( Object.prototype );
// Populate the object with properties
defineProp( person, 'car', 'Delorean' );
defineProp( person, 'dateOfBirth', '1981' );
defineProp( person, 'hasBeard', false );
console.log(person);
// Outputs: Object {car: "Delorean", dateOfBirth: "1981", hasBeard: false}
// 4. Object.defineProperties
Object.defineProperties( newObject, { // Set properties
'someKey': {
value: 'Hello World',
writable: true
},
'anotherKey': {
value: 'Foo bar',
writable: false
}
});
We can use these same methods to achieve inheritance in JavaScript:
// Create a race car driver that inherits from the person object
var driver = Object.create( person );
defineProp(driver, 'topSpeed', '100mph'); // Set some properties for the driver
console.log( driver.dateOfBirth ); // Get an inherited property (1981)
console.log( driver.topSpeed ); // Get the property we set (100mph)
The lookup time for properties that are high up on the prototype chain can have a negative impact on performance, and this may be significant in code where performance is critical. Additionally, trying to access nonexistent properties will always traverse the full prototype chain.
Also, when iterating over the properties of an object, every enumerable property that is on the prototype chain will be enumerated.
To check whether an object has a property defined on itself and not somewhere on its prototype chain, it is necessary to use the hasOwnProperty method which all objects inherit from Object.prototype.
hasOwnProperty is the only thing in JavaScript which deals with properties and does not traverse the prototype chain.
Note: It is not enough to check whether a property is undefined. The property might very well exist, but its value just happens to be set to undefined.
[[Prototype]]
prototype and Object.getPrototypeOf JavaScript is a bit confusing for developers coming from Java or C++, as it's all dynamic, all runtime, and it has no classes at all. It's all just instances (objects). Even the "classes" we simulate are just a function object.
You probably already noticed that our function A has a special property called prototype. This special property works with the JavaScript new operator. The reference to the prototype object is copied to the internal [[Prototype]]
property of the new instance. For example, when you do var a1 = new A(), JavaScript (after creating the object in memory and before running function A() with this defined to it) sets a1.[[Prototype]]
= A.prototype. When you then access properties of the instance, JavaScript first checks whether they exist on that object directly, and if not, it looks in [[Prototype]]
. This means that all the stuff you define in prototype is effectively shared by all instances, and you can even later change parts of prototype and have the changes appear in all existing instances, if you wanted to.
If, in the example above, you do var a1 = new A(); var a2 = new A(); then a1.doSomething would actually refer to Object.getPrototypeOf(a1).doSomething, which is the same as the A.prototype.doSomething you defined, i.e. Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething.
In short, prototype is for types, while Object.getPrototypeOf() is the same for instances.
[[Prototype]]
is looked at recursively, i.e. a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., until it's found or Object.getPrototypeOf returns null.
So, when you call var o = new Foo();
JavaScript actually just does:
var o = new Object();
o.[[Prototype]] = Foo.prototype; // [[Prototype]] is an internal property (not accessible in code)
Foo.call(o);
and when you later do o.someProp;
it checks whether o has a property someProp. If not it checks Object.getPrototypeOf(o).someProp
and if that doesn't exist it checks Object.getPrototypeOf(Object.getPrototypeOf(o))
.someProp and so on.
In JavaScript, there are many ways to implement inheritance. As you'll see, although there is one in particular that I recommend you use when implementing inheritance, I rarely recommend implementing inheritance in JavaScript (all of these implementations can be found in greater detail in the course textbook, Stoyan Stefanov. Object-Oriented JavaScript). Most of the code and examples below come directly from the book:
Child.prototype = new Parent();
This method both works with constructors and uses the prototype chain. This the default method for implementing inheritance in JavaScript. Reusable properties should be added to the prototype, non-reusable properties should be added as own properties.
This, however, does not quite work correctly in JavaScript. The fact that we're calling new here in order to set the prototype of the child ought to strike you as a bit strange, too. Let's take a closer look with another example from OOJ:
function Shape() {
this.name = 'shape';
this.toString = function() {
return this.name;
};
}
function TwoDShape() {
this.name = '2D shape';
}
function Triangle(side, height) {
this.name = 'Triangle';
this.side = side;
this.height = height;
this.getArea = function(){
return this.side * this.height / 2;
};
}
The code that creates the inheritance hierarchy:
TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();
What is happening here? You take the object contained in the prototype property of TwoDShape and instead of augmenting it with individual properties, you completely overwrite it with another object, created by invoking the Shape() constructor with new. The same for Triangle: its prototype is replaced with an object created by new TwoDShape(). The important thing to note, especially if you are already familiar with a language such as Java, C++, or PHP, is that JavaScript works with objects, not classes. You need to create an instance using the new Shape() constructor and after that you can inherit its properties; you don't inherit from Shape() directly. Additionally, after inheriting, you can modify Shape(), overwrite it or even delete it, and this will have no effect on TwoDShape, because all you needed was one instance to inherit from. As you know from the previous chapter, when you completely overwrite the prototype (as opposed to just augmenting it), this has some negative side effects on the constructor property. Therefore, it's a good idea to reset the constructor after inheriting:
TwoDShape.prototype.constructor = TwoDShape;
Triangle.prototype.constructor = Triangle;
Now let's test what we have so far. Creating a Triangle object and calling its own getArea() method works as expected:
var my = new Triangle(5, 10);
my.getArea(); 25
Although the my object doesn't have its own toString() method, it inherited one and can call it. Note how the inherited method toString() binds the this object to my.
my.toString() "Triangle"
It's interesting to note what the JavaScript engine does when you call my.toString(): It loops through all of the properties of my and doesn't find a method called toString(). It looks at the object that my.__proto__ points to; this object is the instance new TwoDShape() created during the inheritance process. Now the JavaScript engine loops through the instance of TwoDShape and doesn't find a toString() method. It then checks the __proto__ of that object. This time __proto__ points to the instance created by new Shape(). The instance of new Shape() is examined and toString() is finally found! This method is invoked in the context of my, meaning that this points to my. If you ask my, "who's your constructor?" it will report it correctly because of the constructor property reset that we did after inheriting:
my.constructor Triangle(side, height)
Child.prototype = Parent.prototype;
This method "copies" the parent's prototype. What does that mean exactly? As with any object in JavaScript, copying here does not mean that the object itself is copied. It means that the value of Child.prototype
is set to a reference to Parent.prototype
. It should not be surprising, then, that when you use this method of inheritance, you're creating a situation in which all objects share the exact same prototype object. As a consequence, modifying a child's prototype will modify the parent's prototype, which is usually not what you want. Although I don't recommend you use this form of inheritance, there is one advantage to it. The fact that there is no prototype chain means that lookups are guaranteed to be fast.
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
Works with constructors Uses the prototype chain Unlike #1, it only inherits properties of the prototype. Own properties (created with this inside the constructor) are not inherited. Used in YUI and Ext.js libraries Provides convenient access to the parent (through uber)
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
Works with constructors Copies properties Uses the prototype chain All properties of the parent prototype become properties of the child prototype No need to create a new object only for inheritance Shorter prototype chains
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
Works with objects Copies properties Very simple Used in Firebug, earlier jQuery and Prototype.js versions Also known as shallow copy Doesn't use prototypes
6. Deep copy same as above, but recurse into objects Works with objects Copies properties Same as #5, but copies the objects by value Used in more recent versions of jQuery
function object(o){
function F() {}
F.prototype = o;
return new F();
}
Works with objects Uses the prototype chain No pseudo-classes; objects inherit from objects Leverages the benefits of the prototype
function objectPlus(o, stuff) {
var n;
function F() {}
F.prototype = o;
n = new F();
n.uber = o;
for (var i in stuff) {
n[i] = stuff[i];
}
return n;
}
Works with objects Uses the prototype chain Copies properties Mix of prototypal inheritance (#7) and copying properties (#5) One function call to inherit and extend at the same time
function multi() {
var n = {}, stuff, j = 0, len = arguments.length;
for (j = 0; j < len; j++) {
stuff = arguments[j];
for (var i in stuff) {
n[i] = stuff[i];
}
}
return n;
}
Works with objects Copies properties A mixin-style implementation Copies all the properties of all the parent objects in the order of appearance
function parasite(victim) {
var that = object(victim);
that.more = 1; return that;
}
Works with objects Uses the prototype chain Constructor-like function, creates objects Copies an object; augments and returns the copy
function Child() {
Parent.apply(this, arguments);
}
Works with constructors Inherits only own properties Can be combined with #1 to inherit prototype too Easy way to deal with the issues when a child inherits a property that is an object (and therefore passed by reference)
function Child() {
Parent.apply(this, arguments);
}
extend2(Child, Parent);
Works with constructors Uses the prototype chain Copies properties Combination of #11 and #4 Allows you to inherit both own properties and prototype properties without calling the parent
These are the 12 implementations of inheritance found in Object-Oriented JavaScript, Chapter 6. If you are going to use inheritance, I am going to recommend to you 2 ways of doing it, but neither of those can be found among these 12 methods. Nor are we going to be able to cover all of them - not only would it take too much time - I don't think it's actually necessary. Instead, I want to focus on why prototypal inheritance has caused so many problems and I want to take just a few of these implementations and dig in to how we can use prototypal inheritance effectively and productively.
The method I'm going to end up recommending to you uses Object.create(). It's available in ES5 but not in browser versions that don't support ES5 (old IEs). For browsers that don't have Object.create(), you can actually create a partial polyfill using:
if (!Object.create) {
Object.create = function(o) {
function F() {}
F.prototype = o;
return new F();
};
}
This is almost line-for-line the implementation of prototypal inheritance described in OOJ #7 above. How does it work:
This polyfill works by using a throwaway F function, and we override its .prototype property to point to the object we want to link to. Then we use new F() construction to make a new object that will be linked as we specified. - Simpson, Kyle (2014-07-11). You Don't Know JS: this & Object Prototypes (p. 108). O'Reilly Media. Kindle Edition.
If you are going to use inheritance in JavaScript, I recommend that you use this particular version of pseudo-classical prototypal inheritance:
function Person(who) {
this.me = who;
}
Person.prototype.identify = function() {
return 'I am ' + this.me;
};
function Speaker(who) {
Person.call(this, who);
}
Speaker.prototype = Object.create(Person.prototype);
Speaker.prototype.speak = function() {
alert('Hello, ' + this.identify() + '.');
};
var carrie = new Speaker('carrie');
var margaret = new Speaker('margaret');
carrie.speak();
margaret.speak();
I think it's a good compromise between producing readable code that makes sense from both a classical and a prototypal perspective while eliminating most of the problems that arise from other pseudoclassical implementations. As I've said several times already, however, I strongly recommend that when you get to a point where you're thinking about using inheritance, you think very carefully about what you're doing and try to make sure that there isn't a better way to solve your problem.
Object.create(proto[, propertiesObject])
Parameters
- proto The object which should be the prototype of the newly-created object.
- propertiesObject Optional. If specified and not undefined, an object whose enumerable own properties (that is, those properties defined upon itself and not enumerable properties along its prototype chain) specify property descriptors to be added to the newly-created object, with the corresponding property names. These properties correspond to the second argument of Object.defineProperties().
Using Object.create() to achieve classical inheritance:
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - subclass
function Rectangle() {
Shape.call(this); // call super constructor.
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
console.log('Is rect an instance of Rectangle? ' + (rect instanceof Rectangle)); // true
console.log('Is rect an instance of Shape? ' + (rect instanceof Shape)); // true
rect.move(1, 1); // Outputs, 'Shape moved.'