Overview
This article reviews the use of Javascript objects, prototypes, constructors, and inheritance. Although there is a significant amount of documentation on the Web already about this topic, we have found a good deal of it to be confusing or incomplete. Therefore, we're providing this article as our own reference, which we'll refer to in other articles dealing with web browsers, our library functions, and privacy. The flow and content of this particular article is inspired by the excellent book Javascript Ninja, Second Edition by Resig, Bear, and Maras.
In this article, we take the approach of embedding a commented, non-minimized script within the HTML of this page "static/src/js_tst_objs.js", and we encourage the reader to step through the code line by line in the developer console of their browser in a split screen fashion as shown below in Figure 1. The entire script is also included at the bottom (as HTML) and core snippets of the code are shown throughout the page.
As you may know, classes were added in ES6 (ECMAScript 2015), but this is just basically window dressing over JavaScript's existing prototype-based inheritance language mechanisms. Also, many popular Javascript libraries don't use the class syntax. Therefore, it's very important to have a firm grasp of these core prototype-related Javascript concepts.
If you plan to debug along while reading this article, go ahead and open your developer console. On Windows & Linux: CTRL+SHIFT+S should work, and on MacOS: COMMAND+OPTION+I should do it. Note that we provide references at the bottom of this article for both Firefox's Debugger and Safari's Web Inspector. These are the two that we use extensively and like very much.
The article and script are divided into three parts:
- Basic object creation with literal notation
- Object creation with a constructor function
- Methodology for inheritance
Core snippets are shown below but make sure to examine, test, and experiment with the other lines of code in the script that provide further introspection and manipuation of the objects we're creating.
Part 1: Basic object creation with literal notation
The first thing we do is create an object obj1 with literal notation.
let obj1 = { p1: 1, p2: function(){}, p3: {} }
Create a second object obj2 to work with.
let obj2= { p4: 'hi' };
Note that both of these objects were constructed using the core Object constructor and inherit the properties and methods of the Object.prototype object (e.g, isPrototypeOf). See Figure 2 below for an inspection of obj2.
obj1 and obj2 both have an implicit prototype property that we denote [[prototype]] in the object diagrams below. We can set an object's prototype directly by using Object.setPrototypeOf(), which is a method of the Object constructor:
// obj2 inherits obj1 properties through prototype Object.setPrototypeOf(obj2, obj1);
After performing the step above, inspect obj2 in your developer console and see how obj2 now has the properties in obj1.
Before we go onto the next part of this article, step through the rest of part 1 in the script and make sure you understand the following:
- Basic operations on object properties like assigning properties dynamically and deleting properties
- Use instanceof to test whether a particular function's prototype is in the prototype chain of an object
- Use in to test membership of a property in an object
Part 2: A Constructor function and object creation
As shown below, we create a new object obj3 with our function constructor: ObjCreator().
function ObjCreator() { this.p6 = "hi"; } let obj3 = new ObjCreator();
Note that every function constructor we define has a prototype object that initially references an empty object. This prototype object is automatically set as the prototype of the objects created with ObjCreator(), and the prototype gains a constructor property that points back to the ObjCreator() function. The newly created object (i.e., obj3) can use the constructor property to identify the function that created it. This relationship is shown in Figure 3.
In this example, we also added a mult method to our prototype, which obj3 can access through its prototype chain.
ObjCreator.prototype.mult = function(a,b) { return a*b; }
Figures 4 and 5 below show the inspection of obj3 and its prototype chain using both Safari Web Inspector and Firefox Debugger.
Part 3: Methodology for inheritance
In this section we provide an example of inheritance by having the child object inherit the properties of its parent object. child is constructed from function ChildObjCreator(), and the parent object is constructed from function ObjCreator().
Below we use an instance of the parent object as the child constructor's prototype, and this will establish the prototype chain and inheritance that we seek. child will have access to its parent's properties and methods all the way up the prototype chain through Object.prototype.
ChildObjCreator.prototype = new ObjCreator(); let child = new ChildObjCreator();
As recommended in Javascript Ninja, Second Edition, let's fix up the child constructor, so we can determine programmatically which function constructed the child object. Otherwise, it will point to ObjCreator.
Object.defineProperty(ChildObjCreator.prototype, "constructor", { enumerable: false, value: ChildObjCreator, writable: true });
Figure 6 below depicts the relationship between the objects, constructors, and prototypes we have created.
Lastly, Figure 7 shows an inspection of the child object using Safari Web Inspector.
There are a number of corner cases to be aware of when working with Javascript prototypes, such as what happens when a constructor's prototype is set to a new object after the constructor creates an object. We hope to expand this document in the future to cover such cases.
/* * Copyright 2019 Mind Chasers Inc. * file: js_tst_obj.js */ function ObjCreator() { this.p6 = 'hi'; } function ChildObjCreator() { this.p7 = 'there'; } window.addEventListener("load",function(e) { // PART 1: create local object obj1 with literal notation let obj1 = { p1: 1, p2: function(){}, p3: {} } // All objects inherit methods and properties from Object.prototype console.log(obj1 instanceof Object); // true console.log(obj1.hasOwnProperty('p1')); // true console.log(obj1.hasOwnProperty('p4')); // false // create a second object to work with let obj2= { p4: 'hi' }; // basic object property manipulation examples obj2.p5 = 1.63; // add a new property dynamically delete obj1.p3; // remove the property console.log(Object.keys(obj1)); // returns array of object's enumerable properties // obj2 inherits obj1 properties through its implicit prototype // See Figure 1 Object.setPrototypeOf(obj2, obj1); console.log('p1' in obj2); // test for membership of a property in an object console.log('obj2:', obj2); if (obj2.prototype === undefined ) { console.log("we can't access the object's prototype property directly" ); } // PART 2: Create object with Constructor let obj3 = new ObjCreator(); ObjCreator.prototype.mult = function(a,b) { return a*b; } console.log(obj3.mult(2,3)); console.log(obj3.constructor === ObjCreator); console.log(obj3.constructor.prototype instanceof Object); console.log('obj3', obj3); // PART 3: Inheritance ChildObjCreator.prototype = new ObjCreator(); let child = new ChildObjCreator(); console.log(child instanceof ChildObjCreator); // true console.log(child instanceof ObjCreator); // true console.log('p6' in child); // true // by overwriting the prototype, we lost the connection of our child.constructor and ChildObjCreator console.log(child.constructor === ChildObjCreator); // false Object.defineProperty(ChildObjCreator.prototype, "constructor", { enumerable: false, value: ChildObjCreator, writable: true }); console.log(child.constructor === ChildObjCreator); // true console.log(child instanceof ObjCreator); // true });
References
- Book Reference: Javascript Ninja, Second Edition
- MDN: Object reference
- Safari Web Inspector
- Firefox Debugger
- MDN: Javascript Classes