Javascript is a prototype-based object-oriented programming language. Unlike the conventional class-based object-oriented programming languages (e.g. C++ or Java), which ask programmers to write a class and then instantiate several similar objects from the class, Javascript adopts a different approach. In the world of Javascript, we have to craft a prototypal object first and then create similar objects from the prototypal object. In this article, we would like to give a gentle introduction to object-oriented programming in Javascript.
Objects and Methods
Let's start to create our first object in Javascript:
var robert = {
firstName: 'Robert',
lastName: 'Smith',
};
In the code snippet above, we created a Javascript object holding Robert's first
name and last name. Objects in Javascript are essentially dictionaries. We can
access the properties with the dot
operator. For example, the following
code snippet prints Robert's first name and the last name:
console.log(robert.firstName);
// Prints: Robert
console.log(robert.lastName);
// Prints: Smith
Now, we can write a function to get the full name:
function getFullName(employee) {
return employee.firstName + ' ' + employee.lastName;
}
console.log(getFullName(robert));
// Prints: Robert Smith
It works but it is suboptimal. Compared with getFullName(robert)
, it
will be better to define a method for the object which was referred by the
reference robert
and invoke robert.getFullName()
instead. To
achieve this, let's move getFullName()
into the object literal and
replace employee
argument with the implicit this
argument:
var robert = {
firstName: 'Robert',
lastName: 'Smith',
getFullName: function () {
return this.firstName + ' ' + this.lastName;
}
};
console.log(robert.getFullName());
// Prints: Robert Smith
The implicit this
argument will be bound to the object before the
dot
operator at the callsite. In this case, it will be bound to the
object which robert
refers to.
Notice that this mechanism will only work if the dot
operator and the
function invocation are combined together. If we write these operators
separately, then we will get a different result. For example, in the following
code snippet, this
will be bounded to the global host object of the
Javascript run-time environment (window
object in browsers and
global
object in Node.js) instead of the object which is referred by the
reference robert
:
var func = robert.getFullName;
console.log(func());
// Prints: undefined undefined
So far, we have learned to craft an object with several properties and methods.
However, in the real world, many objects may share the same method. For
example, getFullName()
is not specific to robert
. Are there
any mechanisms to re-use this method?
Prototypal Inheritance
Javascript supports prototypal inheritance. With prototypal inheritance, a
programmer can inherit properties and attributes from another prototypal
object (a.k.a. prototype). To inherit from an object proto
, we can
invoke Object.create(proto)
which will return a new object resembles to
the object proto
.
For example, the following code snippet creates a new object called mike
which inherits from the object robert
:
var mike = Object.create(robert);
mike.firstName = 'Mike';
mike.lastName = 'Williams'
console.log(mike.getFullName());
// Prints: Mike Williams
Notice that we can override the properties from the prototypal object in the
derived object. In the example, we overrode firstName
and
lastName
. Methods from the prototypal object (getFullName()
in
this example) can get the overridden properties through the this
keyword.
Overriding properties will not change the prototypal object. For example, the
firstName
and lastName
properties in robert
object
remains unchanged. The result of calling robert.getFullName()
remains
unchanged as well:
console.log(robert.firstName);
// Prints: Robert
console.log(robert.lastName);
// Prints: Smith
console.log(robert.getFullName());
// Prints: Robert Smith
However, changes to the prototypal object will be visible to derived objects if
such property is not overridden by the derived object. For example, if we
change the getFullName
property in the robert
object, then the
result of calling mike.getFullName()
will be changed:
robert.getFullName = function () {
return this.lastName + ', ' + this.firstName;
};
console.log(robert.getFullName());
// Prints: Smith, Robert
console.log(mike.getFullName());
// Prints: Williams, Mike
We can get the prototypal object of an object with
Object.getPrototypeOf()
. For example:
console.log(Object.getPrototypeOf(mike));
// Prints: { firstName: 'Robert',
// Prints: lastName: 'Smith',
// Prints: getFullName: [Function] }
So far, we have discussed how to inherit properties and methods from another
object. However, Object.create()
is a function introduced in ECMAScript
5, which was released in 2011. How did people use prototypal inheritance before
Object.create()
was introduced?
Constructor
Before the introduction of Object.create()
, the prototypal inheritance
has to be done indirectly through the new
operator and the constructor
function.
New Operator and Constructor
The new
operator will create a new object whose prototype is the
object referred by the prototype
property of the constructor function
and pass the newly created object through the implicit this
argument.
Constructor functions are functions which assume that the implicit
this
argument will bind to a newly created object. These constructors
will initialize the object by setting more properties. For example:
function Employee(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Employee.prototype = {
getFullName: function () {
return this.firstName + ' ' + this.lastName;
},
};
var robert = new Employee('Robert', 'Smith');
var mike = new Employee('Mike', 'Williams');
console.log(robert.getFullName());
// Prints: Robert Smith
console.log(mike.getFullName());
// Prints: Mike Williams
The following code show that the prototype object of robert
and
mike
is the object referred by Employee.prototype
:
console.log(Object.is(Object.getPrototypeOf(robert), Employee.prototype));
// Prints: true
console.log(Object.is(Object.getPrototypeOf(mike), Employee.prototype));
// Prints: true
To get more insights, let's add a console.log()
in the Employee
constructor function:
function Employee(firstName, lastName) {
console.log(Object.is(Object.getPrototypeOf(this), Employee.prototype));
this.firstName = firstName;
this.lastName = lastName;
}
new Employee('Ada', 'Lovelace');
// Prints: true
The result shows that the object referred by the implicit this
argument
is using Employee.prototype
as the prototype object.
Constructor Pitfalls
We have learned about the new
operator and the construction functions.
However, please be aware that constructor functions are only functions following
a specific convention. This abstraction will be broken if we don't call these
functions through the new
operator. For example:
var bob = Employee('Bob', 'Taylor');
// Prints: false
console.log(bob);
// Prints: undefined
In this code snippet, the implicit this
argument will be bound to the
global host object mentioned earlier, thus the prototype of the object won't be
Employee.prototype
. Besides, the value of bob
will be
undefiend
becaue the Employee
function does not return any
value.
Return values of the constructor functions might cause some confusions as well.
If a constructor does not return any value, then the new
expression will
become the newly created object referred by the implicit this
argument
as expected. However, if a constructor returns some value, then the new
expression will become the returned value. For example:
function Employee(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
if (firstName === 'Bad' && lastName == 'Example') {
return 'Surprise';
}
}
console.log(new Employee('Robert', 'Smith'));
// Prints: { firstName: 'Robert',
// Prints: lastName: 'Smith' }
console.log(new Employee('Bad', 'Example'));
// Prints: Surprise
This mechanism is designed to give the writer of constructor functions an opportunity to change the prototypal inheritance hierarchy. However, I personally don't use this feature in my code becuase it is too confusing.
Idiom for Constructor Functions
Now, let's get back to our first code snippet for Employee
:
function Employee(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Employee.prototype = {
getFullName: function () {
return this.firstName + ' ' + this.lastName;
},
};
In the our first version, we assigned an object to Employee.prototype
.
This is not necessary to create a new object if we are not planning to inherit
from other prototypal objects. Javascript run-time will create an object and
assign it to the prototype
property of the function object when the
function is defined. We can add properties to that object directly. Thus,
the idiomatic implementation for Employee
should be:
function Employee(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Employee.prototype.getFullName = function () {
return this.firstName + ' ' + this.lastName;
};
var robert = new Employee('Robert', 'Smith');
console.log(robert.getFullName());
// Prints: Robert Smith
Craft a Polyfill
The new
operator and the constructor function sound complex. How could
I implement an Object.create()
in ECMAScript 3? Here is a simple trick:
function create(proto) {
function f() {}
f.prototype = proto;
return new f();
}
This code snippet defines a fucntion f
locally, assign proto
to
prototype
property, and return the object constructed by the new
operator.
For example, we can copy the code snippet in the Prototypal Inheritance
section and replace Object.create()
with our version:
var robert = {
firstName: 'Robert',
lastName: 'Smith',
getFullName: function () {
return this.firstName + ' ' + this.lastName;
}
};
var mike = create(robert);
mike.firstName = 'Mike';
mike.lastName = 'Williams';
console.log(mike.getFullName());
// Prints: Mike Williams
It works, but this implementation can be more efficient. We don't have to
create a new function object f
whenever we invoke create()
.
We can create one instance and reuse it:
var create = (function () {
function f() {}
function create(proto) {
f.prototype = proto;
return new f();
}
return create;
})();
In addition, we can wrap our polyfill with a check, so that we can use the
built-in implementation if the Javascript run-time supports
Object.create()
:
if (typeof Object.create !== 'function') {
Object.create = (function () {
function f() {}
function create(proto) {
f.prototype = proto;
return new f();
}
return create;
})();
}
Of course, our naive implementation is not fully compliant to the one specified
in ECMAScript 5. Some features specified in ECMAScript 5 cannot be implemented
with ECMAScript 3 unless some run-time specific hacks are applied. But, this
polyfill helps us to relate Object.create()
function with the
constructor function.
Conclusion
In this post, we started from the object literals, properties, and methods. And
then, we introduced the prototypal inheritance with Object.create()
,
which creates an object inheriting from another object. After having a solid
idea on Object.create()
, we went further to learn the new
operator and constructor functions in ECMAScript 3. It was how people write
object-oriented program in ECMAScript in the past. Finally, we connected two
different methodologies by crafting an Object.create()
polyfill. Hope
you enjoy this post. See you next time.