Ad – 728Γ—90
πŸ—οΈ OOP

JavaScript Constructors – Building Objects with new

The constructor method is the heartbeat of every JavaScript class. It runs automatically when you use new, wires up this, and lets you set initial state on every fresh instance. In this lesson you will see exactly what the four-step new algorithm does under the hood, how new.target lets you guard against misuse, how super() chains parent constructors, and when a factory function is a smarter alternative to a class.

⏱️ 20 min read 🎯 Intermediate πŸ“… Updated 2026

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.

JavaScript
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);
β–Ά Output
2024 Tesla Model 3 @ 80 km/h
true
ℹ️
One constructor per class

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:

StepWhat happensEquivalent code
1. CreateA brand-new empty object is created in memoryconst obj = Object.create(null)
2. Link prototypeThe new object's [[Prototype]] is set to Constructor.prototypeObject.setPrototypeOf(obj, Car.prototype)
3. Bind thisInside the constructor, this refers to the new objectconstructor.call(obj, ...args)
4. ReturnThe new object is returned (unless the constructor returns a different object)return obj
JavaScript
// 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
β–Ά Output
(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; this is returned as usual.
  • Returning an object β†’ that object replaces this as the result of new.
JavaScript
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
β–Ά Output
replaced
false
original
true
⚠️
Avoid returning objects from constructors

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.

JavaScript
// 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
β–Ά Output
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.

JavaScript
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
β–Ά Output
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.

JavaScript
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
β–Ά Output
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.

JavaScript
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
β–Ά Output
Happy birthday, Alice! Now 30.
Alice (age 30)
undefined
πŸ’‘
Class vs Factory – when to choose

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.

JavaScript
// 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')
β–Ά Output
Ford Mustang
1
true
function
Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Create a Queue class using private class fields that models a first-in-first-out queue:

  • Private field #items initialised to an empty array in the constructor.
  • Public methods: enqueue(item), dequeue() (throws if empty), peek() (returns front without removing), get size().
  • Use new.target to throw an error if someone tries to call Queue without new.
  • 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 new keyword performs when creating an object?
  • What happens if a constructor explicitly returns a primitive? What about an object?
  • Why must super() be called before this in a derived class constructor?
  • How does new.target help 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 constructor method runs automatically on new and sets up instance state.
  • new performs 4 steps: create empty object β†’ link prototype β†’ bind this β†’ return object.
  • Returning a primitive from a constructor is ignored; returning an object replaces this.
  • new.target equals the constructor when called with new, undefined otherwise.
  • Derived class constructors must call super() before any use of this.
  • 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

Can I have multiple constructors in a JavaScript class? +

No. JavaScript class bodies allow exactly one constructor method. If you need optional parameters, use default parameter values or destructuring inside the single constructor.

What is the default constructor if I omit it? +

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.

Do private class fields work in all modern browsers? +

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.

Is a class declaration hoisted like a function declaration? +

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.