Constructor Functions
Constructor functions are the equivalent of classes in many programming languages. Sometimes people will refer to them as reference types, classes, data types, or simply constructors. If you aren't familiar with classes, they are a construct that allows you to specify some properties and behaviors (functions), and multiple objects can be created with those properties and behaviors. A common analogy you'll often hear is, a class is to a blueprint as an object is to a house. Multiple houses can be created from a single blueprint, as multiple objects can be created from a class.
Let's define a constructor function:
function Person(name, position) {
this.name = name;
this.position = position;
}
There is really nothing special about this function except that the function name starts with a capital letter. This isn't mandatory, but it is a popular convention so that other developers know to invoke it as a constructor function. So how is the invocation different? Normally, functions are invoked with parenthesis. Constructor functions instead are invoked using the new
operator:
const david = new Person('David Tang', 'Lecturer');
const patrick = new Person('Patrick Dent', 'Associate Professor');
Here we are constructing two Person
objects.
When a constructor function is invoked with the new
keyword, this refers to the object that is being constructed.
Methods
Methods can be defined on constructor functions by assigning a function to a property.
function Person(name) {
this.name = name;
this.hi = function () {
console.log(`Hi! My name is ${this.name}.`);
};
}
const eminem = new Person('Slim Shady');
eminem.hi(); // Hi! My name is Slim Shady.
In this example, the hi
property is assigned a function. When it is invoked off of a Person
object, the keyword this will correspond to the newly constructed Person
object.
Although methods can be defined this way, this approach does have a downside. Every time an instance of Person
is created, a new function is defined and assigned to the hi
property of that object. If we create 5 Person
objects, they will all have their own hi
method that does the same thing. A more efficient way to do this is to define hi
once, and have each Person
object use that same function reference. To do this, we can use a function's prototype
.
function Person(name) {
this.name = name;
}
Person.prototype.hi = function () {
console.log(`Hi! My name is ${this.name}.`);
};
const david = new Person('David');
david.hi(); // Hi! My name is David.
const patrick = new Person('Patrick');
patrick.hi(); // Hi! My name is Patrick.
Every function in JavaScript has a property called prototype
which contains an almost empty object (more on this later). Whenever a Person
instance is created, the object will inherit any properties or methods defined on Person.prototype
.
We could have written the example above like this:
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
hi() {
console.log(`Hi! My name is ${this.name}.`);
},
};
Rather than adding new methods to Person.prototype
in several statements, we can just redefine the Person.prototype
object. There is one caviat though. Remember when I said that the prototype is an "almost empty" object? Technically it has a property on it named constructor
that points back to its constructor function. If we override the prototype by setting it to a completely new object, we should reset this constructor
property.
What Should Be Set On prototype
?
Because anything on the prototype is shared across all object instances of that constructor, typically you only see methods defined on the prototype and properties stored on the constructed object itself. Methods are shared behaviors so each object doesn't need its own unique method. However, each object often needs its own unique set of properties. Properties defined on the an object itself and not the prototype are referred to as "own properties".
ES6 Classes
Thanks to ES6, we can write the above constructor function using a common class syntax:
class Person {
constructor(name) {
this.name = name;
}
hi() {
console.log(`Hi! My name is ${this.name}.`);
}
}
In this example, hi
will be stored on Person.prototype
.
Property Lookups
What happens if we define a property with the same name on an object and its constructor's prototype? For example:
function Person(name) {
this.name = name;
this.walk = function () {
console.log('moon walking');
};
}
Person.prototype.walk = function () {
console.log('normal walking');
};
const mj = new Person('Michael Jackson');
mj.walk();
In this example, a walk
method is defined both on a Person
instance and Person.prototype
. What do you think will get logged to the console?
JavaScript will first try and look up a property on the object itself (an "own property"). If it exists, that property is used. If not, it will look at the prototype of the function that created the object.
So in the example above, walk
is found on the mj
object itself so it will log "moon walking" to the console. If our Person
function looked like the following, then "normal walking" would be logged to the console because a walk
method was not found on the object itself, so JavaScript looked next on Person.prototype
in which the walk
method was found.
function Person(name) {
this.name = name;
}
Inheritance
If you've come from a class-based language, you might be wondering how inheritance works. Let's say we have an Animal
constructor.
function Animal() {}
Animal.prototype.eat = function () {
console.log('eating');
};
Now let's say we have a Cat
constructor:
function Cat() {}
Cat.prototype.meow = function () {
console.log('meowing');
};
A cat is a type of animal and we want Cat
to extend from Animal
. One way to achieve inheritance is like this:
function Animal() {}
Animal.prototype.eat = function () {
console.log('eating');
};
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
Cat.prototype.meow = function () {
console.log('meowing');
};
If you've used classes in other languages before, you're probably reading this and thinking "what the ...". Yes, it is very clunky. Thankfully, ES6 / ES2015 classes make this much much cleaner:
class Animal {
eat() {
console.log('eating');
}
}
class Cat extends Animal {
meow() {
console.log('meowing');
}
}
Native Constructor Functions & Their Shorthand (literal) Counterparts
JavaScript has several built in functions that can be used as constructors including String
, Number
, Boolean
, Array
, Function
, Object
, RegExp
, Date
.
const str = new String('some string');
// OR
const str = 'some string'; // literal syntax
const age = new Number(26);
// OR
const age = 26; // literal syntax
const person = new Object();
// OR
const person = {}; // literal syntax
const x = new Boolean(false);
// OR
const x = false; // literal syntax
const add = new Function('a', 'b', 'return a + b;');
const add = function (a, b) {
return a + b;
}; // literal syntax
Even though you can technically create numbers, strings, objects, booleans, and functions using the native constructors, it is almost always simpler and more straightforward to use the literal syntax.
Extending Native Constructors
Native constructor functions can be extended as well. This is often considered a bad practice because if you create a custom String
method and browsers in the future implement that method slightly differently, code may not work as you'd expect. Despite that, it is still a good exercise and worthwhile to learn for understanding prototypes.
String.prototype.dasherize = function () {
return this.replace(/\s/g, '-');
};
'hello world'.dasherize(); // hello-world