Object Prototypes

There is a difference between the class-based approach to object instantiation and JavaScript’s object constructor function. In our previous example, each Account object has its own copy of the deposit function. The balance may differ from one account to the next, but deposit (and any other method we write) is the same in every instance. So every instance created through the Account function carries its own copy of every method, and that uses a lot of memory if we create many instances.

JavaScript has a way around this. Each function has a property called prototype. When you create objects using a constructor function with the new keyword, those objects inherit properties and methods from the constructor’s prototype. That is how all instances of Account can share one copy of each method.

Let’s rewrite the Account example so the methods live on the prototype:

function Account() {
  this.balance = 0;
}

// Attach methods to the prototype, shared by all instances
Account.prototype.deposit = function (amount) {
  this.balance += amount;
};
Account.prototype.getBalance = function () {
  return this.balance;
};

const acc1 = new Account();
const acc2 = new Account();

acc1.deposit(10);
acc2.deposit(20);

console.log(acc1.getBalance()); // 10
console.log(acc2.getBalance()); // 20

Here, deposit and getBalance are defined on the Account prototype. Instead of each instance of Account carrying its own copies of these methods, all instances share them through the prototype. That uses less memory, and the difference grows as you create more instances.

Implementing Inheritance with Prototypes

Prototypes also let us extend an existing object type. Suppose we want a SavingAccount that adds a bonus to every deposit. We can do that by having SavingAccount inherit from Account:

function Account() {
  this.balance = 0;
}

// Attach methods to the prototype, shared by all instances
Account.prototype.deposit = function (amount) {
  this.balance += amount;
};

function SavingAccount() {
  Account.call(this); // Call the parent constructor, ensuring 'this' is set properly
}

// Inheriting from Account
SavingAccount.prototype = Object.create(Account.prototype);
SavingAccount.prototype.constructor = SavingAccount;

// Extend or override the existing deposit method
SavingAccount.prototype.deposit = function (amount) {
  this.balance += amount + 5; // Add a bonus of $5
};

const acc1 = new Account();
const acc2 = new SavingAccount();

acc1.deposit(10);
acc2.deposit(10);

console.log(acc1); // {"balance":10}
console.log(acc2); // {"balance":15}

In this version, SavingAccount inherits all the methods from Account, and it overrides deposit to add the bonus. Object.create gives SavingAccount its own prototype object, so we can add or override methods there without changing anything on the parent Account constructor.

ES6 classes use the same prototype chain for methods.

Factory Functions

A factory function is a function that returns an object. We could use a factory function to mimic instantiating objects.

function createAccount() {
  return {
    balance: 0,
    deposit: function (amount) {
      this.balance += amount;
    },
  };
}

const acc1 = createAccount();
const acc2 = createAccount();

acc1.deposit(10);

console.log(acc1); // {"balance":10}
console.log(acc2); // {"balance":0}

A factory function differs from an object constructor function in a couple of ways. It returns an object directly when you call it, and it does not need the new keyword. That makes the code easier to read, and it avoids the bugs you get when someone forgets to write new.

Objects created by a factory function do not have a shared prototype, though, so they do not share methods. Each object gets its own copy of every method, which again uses more memory if you create many of them. Objects created by a constructor function can share methods through the constructor’s prototype, so they use less.

What factory functions do give you is closures, and closures let you hide information. This is the same closure pattern we saw in the Functional Programming chapter. Here is an example where count is really private:

function createCounter(start = 0) {
  let count = start; // private variable, not accessible from outside

  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    getCount() {
      return count;
    },
  };
}

const counter = createCounter(10);
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 11
console.log(counter.count); // undefined (not accessible)

In this example, count is a local variable in the factory function, not a property of the returned object. The returned methods form closures over count, so they can read and change it, but code outside the factory function cannot see it. That is real encapsulation. Constructor functions could not do this until private fields were added in ES2022.