JavaScript often gets a bad rap. For those learning to use it for the first time coming from a past in other object-oriented languages like C++ and Java, the most common complaint is that JavaScript “isn’t an object-oriented language” and that it “doesn’t work as it should.” The root of these issues between newcomers and the styles of others languages is often in a misunderstanding how JavaScript works and the way it handles objects internally.
Functions are Objects
In order to create an object as a function in JavaScript, its constructor is called as the function describing it. Anything bound to its ‘this’ function scope is exposed as property members and can be access by referencing the object and name of a property.
var A = { | |
value: 1; | |
} | |
//A is an object | |
var B = function() { | |
this.value = 2; | |
} | |
//B is an object as a function |
JavaScript is not a typed language. For those coming from other, typed languages, this can be confusing. JavaScript is loose with what can be set as the value of properties. They can be Numbers, Strings, Functions, or even other Objects.
var A = function() { | |
this.value = 1; //This value is a number | |
} | |
var B = function() { | |
this.value = "Two"; //This value is a String | |
} | |
var C = function() { | |
this.value = {'value': 1}; //This value is an Object | |
} | |
var D = function() { | |
this.value = function(){}; //This value is a function | |
} |
By using the ‘new’ keyword, more instantiations of an object can be created from a preexisting constructor function. Instead of assigning a function to a variable, it can be defined and then used multiple times.
function A() { | |
this.value = 1; | |
} | |
var B = new A(); | |
var C = new A(); | |
var D = new A(); |
All future instantiations of an object have all properties that were described as being bound to it as well as their initial set values.
However, because each instantiation is an object in its own right, its properties can be inherited and then changed too.
function A() { | |
this.value = 1; | |
} | |
var B = new A(); | |
B.value = 2; | |
var C = new A(); | |
C.value = "Three"; |
In this way, new variables can be copies of preexisting objects and act as parent objects, defining the properties of future, cloned children.
Constructor Calling
While it can be convenient to define constructors and other functions for objects as bound to its ‘this’ scope, it is often useful to describe numerous objects and have them inherit from each other. Instead of an object merely being an instance of another object, it can inherit and then add or overwrite its parent’s properties. It can even inherit from multiple parent objects too, mixing their properties together as part of its own.
The easiest way to create this relationship between objects is by using two properties of all Functions: call and apply.
In JavaScript, all objects inherit from an internal Object and come with built-in function as bound to it. Two of these, call and apply, act to change the calling scope of a function. By passing an object to them, the functions being called act as if they were called from that object instead.
function A() { | |
this.value = 1; | |
} | |
function B(){ | |
A.call(this); | |
this.secondValue = 2; | |
} | |
var C = new B(); | |
//C has both value (from A) and secondValue (from B) |
(Note: the difference between call and apply is in their arguments. call takes a list and apply uses an array or array-like object.)
Prototype Chains
JavaScript is a prototypical language. All objects have properties bound to its ‘this’ scope as well as those defined as part of its prototype. Every object in JavaScript has a prototype property and all of its own properties execute within the same function scope as the object itself. If a property is referenced from an object, its ‘this’ is checked first and, if the property is not found, the properties of an object’s prototype are checked next.
An object’s interface, then, can be defined by binding properties to its prototype to act like functions and properties of an object’s ‘this’ without actually being so.
var A = function() { | |
this.value = 1; | |
}; | |
A.prototype.getValue = function() { | |
return this.value; | |
}; | |
var B = new A(); | |
B.getValue(); |
Every new object that is instantiated from an existing object also inherits all of its previously defined prototype properties too.
Implicit (Single) Prototype Inheritance
Just like using call and apply to build objects from the properties of other preexisting objects, it is also possible to bind all of the properties of an object’s prototype to another object. It can inherit not only all of its properties, but its prototype functionality too.
To do this, another function of all objects is used: create.
Object.create uses an object prototype passed to it to build a new object, connecting the enumerable properties of the passed object prototype as the property descriptors of the new object. Through setting an object’s prototype to the Object.create‘d version of another object’s prototype, it binds all of the prototype properties of one object as those of another.
As the final step, the constructor of the newly created object needs to be set. By using the object’s own constructor, it connects back the newly created prototype object as created by its own object.
function A() { | |
this.value = 1; | |
} | |
A.prototype.getValue = function() { | |
return this.value; | |
}; | |
function B() { | |
A.call(this); | |
} | |
B.prototype = Object.create(A.prototype); | |
B.prototype.constructor = B; |
This method is classical inheritance, allowing one or more children to inherit from (be instances of) both themselves and their parent super-classes.
Explicit (Multiple) Prototype Inheritance
JavaScript itself does not provide easy native functionality to mixin, inherit from multiple object prototype chains. However, while all the prototype properties of multiple objects cannot be easily bound, those functions specifically wanted as part of any parent or other objects can be explicitly described and call-ed directly, creating object polymorphism during the function execution.
Because call and apply are part of all functions, they can be used to directly reference parent or other objects. As long as the calling object has the same properties as those referenced within the function being called, the scope can change and return without error.
function A() { | |
this.value = 1; | |
} | |
A.prototype.getValue = function() { | |
return this.value; | |
}; | |
A.prototype = Object.create(A.prototype); | |
A.prototype.constructor = A; | |
function B() { | |
A.call(this); | |
this.secondValue = 2; | |
} | |
B.prototype.getSecondValue = function() { | |
return this.secondValue; | |
}; | |
B.prototype = Object.create(B.prototype); | |
B.prototype.constructor = B; | |
function C() { | |
A.call(this); | |
B.call(this); | |
} | |
C.prototype = { | |
getValue: function() { | |
return A.prototype.getValue.call(this); | |
}, | |
getSecondValue: function() { | |
return B.prototype.getSecondValue.call(this); | |
} | |
}; | |
C.prototype = Object.create(C.prototype); | |
C.prototype.constructor = C; |
Defining Read-only, Write-only, and Computed Properties
While it can be useful to bind properties to objects using its ‘this’ scope, all properties added or mutated this way are configurable. Without explicitly defining their mutators and options, they can be overwritten or outright deleted during execution without warning.
However, JavaScript does have a way to explicitly mark properties as configurable (changed or deleted), enumerable (listed as a property), or even writable (able to be assigned a value) through another built-in function of Object: createProperty.
By using createProperty (single) or createProperties (multiple), one of more properties can be added to any object and how that property is accessed or changed can be explicitly set.
function A() { | |
this.value = 2; | |
} | |
//Property is explicitly set and cannot be changed directly | |
Object.defineProperty(A, "noValueChange", { | |
configurable: false, //This value cannot be changed or directly deleted | |
enumerable: false, //This property will not not be listed with other properties | |
writable: false, //This property cannot be assigned a new value | |
value: "NoValue" //The value | |
}); | |
//Property can be read and written to (assigned a value) | |
Object.defineProperty(A, "changeableValue", { | |
get: function() { | |
return this.value; | |
}, | |
set: function(newValue) { | |
this.value = newValue; | |
} | |
}); | |
//A read-only property with a computed value | |
Object.defineProperty(A, "valueSquared", { | |
get: function() { | |
return this.value ^ 2; //Returns a computed value | |
} | |
}); | |
//A write-only property with a computed value | |
Object.defineProperty(A, "valueMultiplied", { | |
set: function(value) { | |
this.value *= value; //Sets a computed value | |
} | |
}); |
Defining Hybrid-class Prototype Properties
Just as createProperty can be used on any object, it can also be used on any object’s prototype as well. By defining new properties on an object’s prototype, computed values can be returned or function polymorphism performed through the explicitly called functionality of other objects. In effect, any object, as long as it has the same internal properties reference by another function, can adopt as its own that object’s interface or reference its functionality as properties through using call or apply.
function Node(name) { | |
this.name = name; | |
this.childNodes = new Array(); | |
} | |
Node.prototype = { | |
_lastChild: function() { | |
if (this.childNodes.length > 0) { | |
return this.childNodes[this.childNodes.length - 1]; | |
} | |
} | |
}; | |
Node.prototype = Object.create(Node.prototype); | |
Node.prototype.constructor = Node; | |
function Element() { | |
Node.call(this); | |
this.childNodes.push(new Node('lastChildNode')); | |
} | |
Element.prototype = Object.create(Element.prototype); | |
Element.prototype.constructor = Element; | |
Object.defineProperty(Element.prototype, 'lastChild', { | |
enumerable: true, | |
get: function() { | |
return Node.prototype._lastChild.call(this); | |
} | |
}); | |
//A new Element will have the property 'lastChild' | |
// and that property will return the last child node | |
// as computed using Node's _lastChild function | |
// in the scope of Element with the property | |
// 'childNodes' as inherited from Node itself. |