Constructor Execution Steps
When a constructor function is invoked with the new keyword, the following steps occur:
- A new empty object is created in memory.
- The
[[Prototype]]of that new object is linked to the constructor'sprototypeproperty. - The
thisbinding inside the constructor is set to the newly created object. - The constructor's code executes, adding properties and methods to
this. - If the constructor returns a non-primitive value (an object), that value is returned instead of the newly created object. Otherwise, the new object is returned.
Every instance created by a constructor has a constructor property that points back to the constructor function:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
While constructor can identify the type, the instanceof operator is generally more reliable for type checking:
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(person2 instanceof Person); // true
console.log(person2 instanceof Object); // true
Custom constructors allow instances to be identified as a specific type, which is a key improvement over the factory pattern. All custom objects inherit from Object by default.
Funcsion Expressions as Constructors
Constructors can be defined using function expressions as well as declarations:
let Person = function(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
};
let person1 = new Person("Nicholas", 29, "Software Engineer");
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
Optional Parentheses
If no arguments are required, the parentheses after the constructor name are optional when using new:
function Person() {
this.name = "Jake";
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person();
let person2 = new Person;
person1.sayName(); // Jake
person2.sayName(); // Jake
Constructors Are Regular Functions
The only difference between a constructor and a regular function is the invocation context. Any function can act as a constructor if called with new, or as a regular function if called without it:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
// Called as constructor
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// Called as regular function (adds properties to global object in non-strict mode)
Person("Greg", 27, "Doctor");
window.sayName(); // "Greg"
// Called with explicit scope via call()
let o = {};
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
When a function is called without an explicit this context (no object method, no call/apply), this defaults to the global object (window in browsers). Using call() or apply() allows you to set this to any object, effectively copying the constructor's properties to that object.
The Method Duplication Problem
A significant drawback of constructors is that methods are recreated for every instance. Each instance gets its own copy of the functon, which is wasteful:
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
// Behind the scenes, this is equivalent to:
// this.sayName = new Function("console.log(this.name)");
let person1 = new Person("Alice");
let person2 = new Person("Bob");
console.log(person1.sayName === person2.sayName); // false
Each sayName function is a distinct Function instance, consuming extra memory. This issue is addressed by sharing methods through the prototype chain, which is covered in subsequent chapters on prtootypal inheritance.