8.1 Prototypes & the Prototype Chain
Unlike languages like Java or C++ that feature classical class-based inheritance where blueprints are copied into new instances, JavaScript implements prototypal inheritance. Every JavaScript object possesses an internal hidden link (known in specifications as [[Prototype]] and exposed in browser runtimes via __proto__) pointing to another object from which it inherits properties and methods.
Figure 8.1: When evaluating dog.bark(), the engine checks dog, traverses __proto__ to Dog.prototype, executes the method, and terminates at null.
1. How Property Lookup Works
When you access any property or method obj.property, the V8 engine executes the following recursive algorithm:
- Check if the property exists directly on
objas an own property. If found, return its value immediately. - If not found, inspect
Object.getPrototypeOf(obj)(the prototype linked via__proto__). - Repeat step 1 on the prototype object. Continue ascending the chain until the property is found or until
Object.prototype.__proto__resolves tonull. - If the end of the chain is reached without finding the property, evaluate to
undefined.
2. Property Shadowing
Assigning a value to an object property (dog.bark = customBark) does NOT overwrite the method on Dog.prototype. Instead, it creates an own property directly on dog, effectively shadowing (masking) the prototype method for that single instance.
3. Function.prototype vs. Object.__proto__
A classic JavaScript interview distinction:
ConstructorFunction.prototype: The template object attached to a constructor function that will become the[[Prototype]]for all instances created vianew ConstructorFunction().instance.__proto__(orObject.getPrototypeOf(instance)): The actual link on an instance pointing up to its prototype.
// 1. Classical Prototypal Model with Constructor Functions
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}
// Attach shared method to prototype (Shared in memory across all instances!)
Dog.prototype.bark = function() {
console.log(this.name + " says Woof!");
};
const rex = new Dog("Rex", "German Shepherd");
rex.bark(); // "Rex says Woof!"
// Verifying prototype links:
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true (Terminal node)
// Checking Own vs Prototype properties:
console.log(Object.hasOwn(rex, "name")); // true (Own property)
console.log(Object.hasOwn(rex, "bark")); // false (Inherited from prototype!)
8.2 ES6 Classes
Introduced in ECMAScript 2015 (ES6), the class keyword provides clean, declarative syntactic sugar over JavaScript's existing prototype system. It offers a standardized syntax that developers accustomed to Java, C++, or Python find familiar, while maintaining JavaScript's dynamic prototypal foundation.
Figure 8.2: ES6 classes map to a constructor function, a shared prototype holding methods, and instance objects with optional private #fields.
1. Core Features of ES6 Classes
- Constructor: Special method invoked during
new ClassName()to instantiate state. Only one constructor is permitted per class. - Instance Methods: Defined cleanly in the class body without the
functionkeyword. Automatically attached toClassName.prototype. - Static Methods & Fields: Prefixed with
static. Called directly on the class identifier (e.g.User.validate()). They cannot access instance properties viathis. - Getters and Setters: Use
getandsetkeywords to bind object properties to functions, enabling validation and computed attributes. - Private Fields (ES2022): Prefixed with a hash symbol
#. They are completely inaccessible from outside the class instance — offering true hard encapsulation enforced by the runtime.
2. Classes vs. Functions: Key Differences
- No Hoisting: Class declarations are not hoisted like regular functions; accessing a class before declaration throws a
ReferenceError(Temporal Dead Zone). - Strict Mode: All code inside a class body automatically runs in
'use strict'mode. - Mandatory
new: Calling a class without thenewoperator immediately throws aTypeError: Class constructor cannot be invoked without 'new'.
class Account {
// 1. Private fields (Encapsulated in memory)
#balance;
#accountNumber;
// 2. Static class property
static totalAccountsCreated = 0;
constructor(accountNumber, initialDeposit = 0) {
this.#accountNumber = accountNumber;
this.#balance = initialDeposit;
Account.totalAccountsCreated++;
}
// 3. Getter for computed / controlled read access
get balance() {
return "$" + this.#balance.toFixed(2);
}
// 4. Public instance method
deposit(amount) {
if (amount <= 0) throw new Error("Deposit must be positive");
this.#balance += amount;
return this.#balance;
}
// 5. Static factory method
static createZeroBalance(accountNumber) {
return new Account(accountNumber, 0);
}
}
const myAcc = new Account("ACT-1002", 500);
myAcc.deposit(250);
console.log(myAcc.balance); // "$750.00"
// console.log(myAcc.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
8.3 Class Inheritance
Subclassing in JavaScript is implemented via the extends keyword, allowing a child class to inherit properties, methods, and static utilities from a base parent class. Inside a derived class constructor, the super() operator must be invoked to initialize the parent instance state and bind the this context.
Figure 8.3: extends links both the instance prototype chain AND the constructor function chain, enabling static and instance method inheritance.
1. The Golden Rule of super()
In a derived subclass constructor, you must invoke super() before accessing this. In JavaScript, derived classes do not allocate memory for this themselves; the base parent class constructor is responsible for allocating and binding the initial instance object. Attempting to read or write this.property before calling super() throws a ReferenceError: Must call super constructor in derived class before accessing 'this'.
2. Method Overriding & Polymorphism
A child class can provide a specialized implementation of an inherited method. If the child wishes to augment rather than completely replace the parent logic, it can invoke super.methodName() to reuse the base implementation.
3. Static Method Inheritance
A unique feature of JavaScript class inheritance: static methods declared on the parent class are automatically inherited by the subclass because Subclass.__proto__ === ParentClass. This allows calling Subclass.parentStaticMethod() directly.
class Vehicle {
constructor(brand, speed) {
this.brand = brand;
this.speed = speed;
}
move() {
console.log(this.brand + " travels at " + this.speed + " km/h.");
}
static compareSpeed(v1, v2) {
return v1.speed - v2.speed;
}
}
class ElectricCar extends Vehicle {
constructor(brand, speed, batteryCapacity) {
super(brand, speed); // MUST be first: allocates 'this'
this.batteryCapacity = batteryCapacity;
}
// Method overriding with super delegation:
move() {
super.move(); // Reuses base logic
console.log("Running silently on " + this.batteryCapacity + " kWh battery.");
}
}
const tesla = new ElectricCar("Tesla Model 3", 200, 75);
tesla.move();
// Output:
// "Tesla Model 3 travels at 200 km/h."
// "Running silently on 75 kWh battery."
// Static methods are inherited by subclasses:
console.log(ElectricCar.compareSpeed(tesla, { speed: 180 })); // 20