What Is a Constructor?
A constructor is a special method inside a class that JavaScript calls automatically whenever you create a new instance with the new keyword. Its job is simple: receive arguments, set up instance properties, and leave this in a valid initial state.
class Car {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.speed = 0; // default β not passed in
}
accelerate(amount) {
this.speed += amount;
return this; // enable chaining
}
toString() {
return `${this.year} ${this.make} ${this.model} @ ${this.speed} km/h`;
}
}
const tesla = new Car('Tesla', 'Model 3', 2024);
tesla.accelerate(60).accelerate(20);
console.log(tesla.toString());
console.log(tesla instanceof Car);
2024 Tesla Model 3 @ 80 km/h true
A class body can contain exactly one method named constructor. Defining a second one throws a SyntaxError. If you omit it entirely, JavaScript generates a default (empty) constructor automatically.
The 4 Steps the new Keyword Performs
When you write new Car('Tesla', 'Model 3', 2024), the JavaScript engine performs these four steps in order β every time, without exception:
| Step | What happens | Equivalent code |
|---|---|---|
| 1. Create | A brand-new empty object is created in memory | const obj = Object.create(null) |
| 2. Link prototype | The new object's [[Prototype]] is set to Constructor.prototype | Object.setPrototypeOf(obj, Car.prototype) |
3. Bind this | Inside the constructor, this refers to the new object | constructor.call(obj, ...args) |
| 4. Return | The new object is returned (unless the constructor returns a different object) | return obj |
// Manually simulating what `new` does
function simulateNew(Constructor, ...args) {
// Step 1 + 2: create object and link prototype
const obj = Object.create(Constructor.prototype);
// Step 3: call constructor with obj as this
const result = Constructor.apply(obj, args);
// Step 4: return object (or override if constructor returns an object)
return (typeof result === 'object' && result !== null) ? result : obj;
}
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return `(${this.x}, ${this.y})`;
};
const p1 = new Point(3, 4);
const p2 = simulateNew(Point, 3, 4);
console.log(p1.toString()); // (3, 4)
console.log(p2.toString()); // (3, 4)
console.log(p1 instanceof Point); // true
console.log(p2 instanceof Point); // true
(3, 4) (3, 4) true true
Returning from a Constructor
Normally you do not return anything from a constructor β JavaScript returns this automatically (step 4 above). But what if you explicitly return something?
- Returning a primitive (string, number, boolean, null, undefined) β ignored;
thisis returned as usual. - Returning an object β that object replaces
thisas the result ofnew.
class Quirky {
constructor() {
this.name = 'original';
return { name: 'replaced', bonus: true }; // returns a plain object
}
}
class Normal {
constructor() {
this.name = 'original';
return 42; // returning a primitive β ignored
}
}
const q = new Quirky();
console.log(q.name); // 'replaced'
console.log(q instanceof Quirky); // false β prototype not linked!
const n = new Normal();
console.log(n.name); // 'original' (return 42 was ignored)
console.log(n instanceof Normal); // true
replaced false original true
Returning a plain object from a constructor breaks instanceof and loses the prototype chain. This is almost always a bug. The only legitimate use-case is enforcing a singleton pattern.
new.target β Detecting How a Constructor Was Called
new.target is a meta-property available inside functions and constructors. It equals the constructor being called when invoked with new, and is undefined when called without new. This lets you enforce that a class is only ever instantiated with new, or create abstract base classes.
// Guard: must be called with new
function SafeCounter(start = 0) {
if (new.target === undefined) {
throw new TypeError('SafeCounter must be called with new');
}
this.count = start;
}
const c = new SafeCounter(10);
console.log(c.count); // 10
try {
SafeCounter(5); // throws
} catch (e) {
console.log(e.message);
}
// Abstract base class pattern
class Shape {
constructor(color) {
if (new.target === Shape) {
throw new Error('Shape is abstract β extend it, do not instantiate directly');
}
this.color = color;
}
}
class Circle extends Shape {
constructor(color, radius) {
super(color);
this.radius = radius;
}
area() { return Math.PI * this.radius ** 2; }
}
try {
new Shape('red'); // throws
} catch (e) {
console.log(e.message);
}
const circle = new Circle('blue', 5);
console.log(circle.area().toFixed(2)); // 78.54
10 SafeCounter must be called with new Shape is abstract β extend it, do not instantiate directly 78.54
super() in Derived Class Constructors
When a class uses extends, its constructor must call super() before accessing this. This is because the parent constructor is responsible for creating and initialising the this object β until super() returns, this is in a "dead zone" and any access to it throws a ReferenceError.
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} says ${this.sound}!`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name, 'Woof'); // must come before any use of `this`
this.breed = breed;
}
fetch(item) {
return `${this.name} (${this.breed}) fetches the ${item}!`;
}
}
class GoldenRetriever extends Dog {
constructor(name) {
super(name, 'Golden Retriever'); // chains up two levels
this.friendly = true;
}
}
const buddy = new GoldenRetriever('Buddy');
console.log(buddy.speak());
console.log(buddy.fetch('ball'));
console.log(buddy.friendly);
console.log(buddy instanceof Animal); // true β full prototype chain
Buddy says Woof! Buddy (Golden Retriever) fetches the ball! true true
Private Class Fields (#name Syntax)
ES2022 introduced private class fields using the # prefix. Private fields are truly inaccessible outside the class β unlike the older convention of prefixing with _, which was never enforced by the runtime.
class BankAccount {
#balance; // private field declaration (required before use)
#owner;
constructor(owner, initialBalance) {
this.#owner = owner;
this.#balance = initialBalance >= 0 ? initialBalance : 0;
}
deposit(amount) {
if (amount <= 0) throw new RangeError('Deposit must be positive');
this.#balance += amount;
return this;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
return this;
}
get balance() { return this.#balance; } // public read-only accessor
toString() {
return `${this.#owner}: $${this.#balance.toFixed(2)}`;
}
}
const acct = new BankAccount('Alice', 100);
acct.deposit(50).withdraw(30);
console.log(acct.toString()); // Alice: $120.00
console.log(acct.balance); // 120
// Direct access is a SyntaxError at parse time:
// console.log(acct.#balance); // SyntaxError
Alice: $120.00 120
Factory Functions β An Alternative to Classes
A factory function is a regular function that creates and returns an object without using new or class. Factories naturally support true private data via closures and avoid the pitfalls of this binding.
function createPerson(name, age) {
// `name` and `age` are closed over β truly private
let _age = age;
return {
getName() { return name; },
getAge() { return _age; },
birthday() {
_age++;
console.log(`Happy birthday, ${name}! Now ${_age}.`);
},
toString() { return `${name} (age ${_age})`; }
};
}
const alice = createPerson('Alice', 29);
alice.birthday();
console.log(alice.toString());
console.log(alice._age); // undefined β truly private
Happy birthday, Alice! Now 30. Alice (age 30) undefined
Use a class when you need instanceof checks, inheritance hierarchies, or you are building a library with a well-defined public API. Use a factory when you want true closure-based privacy, functional composition (mixins), or you want to avoid this confusion entirely.
Function Constructors (Pre-ES6 Pattern)
Before ES6, the idiomatic way to create "classes" in JavaScript was to write a capitalised function and add methods to its .prototype. ES6 classes are syntactic sugar over this pattern β understanding it helps you read older codebases.
// Pre-ES6 constructor function
function Vehicle(make, model) {
this.make = make;
this.model = model;
}
Vehicle.prototype.describe = function () {
return `${this.make} ${this.model}`;
};
// "Static" method on the constructor function itself
Vehicle.compare = function (a, b) {
return a.make.localeCompare(b.make);
};
const v1 = new Vehicle('Ford', 'Mustang');
const v2 = new Vehicle('Audi', 'A4');
console.log(v1.describe()); // Ford Mustang
console.log(Vehicle.compare(v1, v2)); // 1 (F comes after A)
// ES6 class β identical runtime behaviour:
class VehicleES6 {
constructor(make, model) {
this.make = make;
this.model = model;
}
describe() { return `${this.make} ${this.model}`; }
static compare(a, b) { return a.make.localeCompare(b.make); }
}
console.log(v1 instanceof Vehicle); // true
console.log(typeof Vehicle); // 'function' (class is also 'function')
Ford Mustang 1 true function
ποΈ Practical Exercise
Create a Queue class using private class fields that models a first-in-first-out queue:
- Private field
#itemsinitialised to an empty array in the constructor. - Public methods:
enqueue(item),dequeue()(throws if empty),peek()(returns front without removing),get size(). - Use
new.targetto throw an error if someone tries to callQueuewithoutnew. - Test: enqueue 3 items, peek, dequeue twice, log the remaining size.
π₯ Challenge Exercise
Implement a Singleton pattern using a class constructor that caches the first instance and returns the same object on every subsequent new call. Hint: return the cached instance as an object from the constructor (step 4 of new). Verify that new Singleton() === new Singleton() is true.
Interview Questions
- What are the four steps the
newkeyword performs when creating an object? - What happens if a constructor explicitly returns a primitive? What about an object?
- Why must
super()be called beforethisin a derived class constructor? - How does
new.targethelp implement abstract base classes? - What is the difference between a private class field (
#field) and a convention-only private property (_field)? - When would you choose a factory function over a class?
π Summary
- The
constructormethod runs automatically onnewand sets up instance state. newperforms 4 steps: create empty object β link prototype β bindthisβ return object.- Returning a primitive from a constructor is ignored; returning an object replaces
this. new.targetequals the constructor when called withnew,undefinedotherwise.- Derived class constructors must call
super()before any use ofthis. - Private class fields (
#field) are enforced by the runtime, not just by convention. - Factory functions provide closure-based privacy and avoid
this-binding issues.
Frequently Asked Questions
No. JavaScript class bodies allow exactly one constructor method. If you need optional parameters, use default parameter values or destructuring inside the single constructor.
For a base class the engine inserts constructor() {}. For a derived class it inserts constructor(...args) { super(...args); }, which forwards all arguments to the parent constructor.
Yes. Private class fields (the # syntax) are fully supported in Chrome 74+, Firefox 90+, Safari 14.1+, and Node.js 12+. They are part of the ES2022 specification.
Classes are hoisted (the binding is created) but not initialised β they sit in a temporal dead zone until the class declaration is evaluated. Accessing a class before its declaration throws a ReferenceError, unlike function declarations which are fully hoisted.