Prototypes & Objects
JavaScript is fundamentally a prototype-based language. While modern ECMAScript introduced the class keyword in ES6, it is essential to realize that JavaScript classes are primarily syntactic sugar built on top of prototype delegation. Underneath every class, array, and object literal lies the Prototype Chain ([[Prototype]]).
Understanding prototypal inheritance, property attribute descriptors (enumerable, writable, configurable), accessor properties (getters and setters), and object immutability controls (Object.freeze, Object.seal) unlocks the ability to design high-performance architectures, build robust libraries, and debug runtime anomalies.
In this lesson, we will explore the Prototype Chain, object creation with Object.create(), constructor functions and .prototype, Property Descriptors, custom getters and setters with Object.defineProperty(), and integrity seals.
┌────────────────────────────────────────────────────────────────────────┐
│ The Prototype Chain │
├────────────────────────────────────────────────────────────────────────┤
│ Instance Object: dog │
│ ├── name: "Rex" │
│ └── [[Prototype]] ──> Dog.prototype │
│ ├── bark(): function │
│ └── [[Prototype]] ──> Animal.prototype │
│ ├── eat(): function │
│ └── [[Prototype]] ────> Object.prototype
│ └── null
└────────────────────────────────────────────────────────────────────────┘
The Prototype Chain and Property Delegation
Every JavaScript object has an internal hidden property called [[Prototype]] (accessible in browsers via the historical __proto__ property or standard Object.getPrototypeOf()).
When you attempt to access a property or method on an object, the JavaScript engine first checks if that property exists on the object itself (an own property). If it is not found, the engine traverses up the Prototype Chain to the object's [[Prototype]], continuing up until the property is found or null is reached (the end of the chain on Object.prototype). If not found, it evaluates to undefined.
const vehicle = {
wheels: 4,
drive() {
return "Moving forward";
}
};
// Object.create() creates a new object with 'vehicle' as its [[Prototype]]
const car = Object.create(vehicle);
car.make = "Tesla";
console.log(car.make); // "Tesla" (Own property)
console.log(car.wheels); // 4 (Delegated to vehicle prototype!)
console.log(car.drive());// "Moving forward" (Delegated)
Constructor Functions and .prototype
Before ES6 classes, constructor functions were the primary mechanism for instantiating multiple objects sharing shared prototype methods:
- Every function in JavaScript automatically has a
.prototypeobject property. - When invoked with
new FunctionName(), the newly created object's internal[[Prototype]]is linked directly toFunctionName.prototype.
Attaching methods to .prototype ensures that all instances share a single function reference in memory, rather than recreating duplicate function instances for every object.
function Account(accountNumber, balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
// Shared prototype method (allocated once in memory)
Account.prototype.deposit = function(amount) {
this.balance += amount;
return this.balance;
};
const acc1 = new Account("ACC-101", 1000);
const acc2 = new Account("ACC-102", 5000);
acc1.deposit(200);
console.log(acc1.balance); // 1200
console.log(acc1.deposit === acc2.deposit); // true (Shared prototype reference)
Property Descriptors: Enumerable, Writable, Configurable
Properties in JavaScript are not just values; they possess hidden configuration attributes known as Property Descriptors:
value: The actual data value stored.writable: Iftrue, the property value can be modified using an assignment operator.enumerable: Iftrue, the property appears infor...inloops andObject.keys().configurable: Iftrue, the property descriptor can be modified, and the property can be deleted withdelete.
Use Object.getOwnPropertyDescriptor(obj, prop) to inspect descriptors, and Object.defineProperty(obj, prop, descriptor) to configure them explicitly:
const product = { id: 101, title: "Phone" };
// Inspect property descriptor
console.log(Object.getOwnPropertyDescriptor(product, "title"));
// { value: 'Phone', writable: true, enumerable: true, configurable: true }
// Define a read-only, non-enumerable, non-deletable property
Object.defineProperty(product, "serialNumber", {
value: "SN-998822",
writable: false,
enumerable: false,
configurable: false
});
console.log(Object.keys(product)); // ["id", "title"] (serialNumber is hidden!)
// product.serialNumber = "NEW_SN"; // TypeError in strict mode
Accessor Properties: Getters and Setters
Accessor properties are methods that intercept getting and setting a property value. They allow data validation, encapsulation, and derived values without changing public property access syntax:
const temperatureSensor = {
_celsius: 25,
get celsius() {
return this._celsius;
},
set celsius(value) {
if (value < -273.15) {
throw new RangeError("Temperature below absolute zero is impossible");
}
this._celsius = value;
},
get fahrenheit() {
return (this._celsius * 9/5) + 32;
}
};
console.log(temperatureSensor.fahrenheit); // 77
temperatureSensor.celsius = 30;
console.log(temperatureSensor.fahrenheit); // 86
Object Integrity: Object.freeze() and Object.seal()
JavaScript provides three levels of object locking:
Object.preventExtensions(obj): Prevents adding new properties. Existing properties can still be modified or deleted.Object.seal(obj): Prevents adding new properties and marks all existing properties asconfigurable: false(cannot be deleted). Existing writable properties can still be modified.Object.freeze(obj): The highest level of immutability. Prevents adding, deleting, or modifying any existing properties (writable: false,configurable: false). Note:Object.freezeis shallow; nested objects remain mutable unless deeply frozen.
const immutableConfig = Object.freeze({
environment: "production",
port: 8080,
nested: { debug: false }
});
// immutableConfig.port = 9000; // TypeError in strict mode
// delete immutableConfig.environment; // TypeError in strict mode
// Shallow freeze pitfall: nested objects are still mutable!
immutableConfig.nested.debug = true;
console.log(immutableConfig.nested.debug); // true
Summary
JavaScript utilizes prototypal inheritance where objects delegate property lookups through the [[Prototype]] chain. Constructor functions instantiate objects linked to their .prototype objects. Property Descriptors configure writable, enumerable, and configurable flags via Object.defineProperty(). Accessor getters and setters control access to private state. Object.seal() prevents property addition/deletion, while Object.freeze() locks properties into a shallow read-only state.
Best Practices
- Attach Shared Methods to Prototypes: Always place instance methods on constructor prototypes or class declarations to conserve heap memory.
- Use
Object.create(null)for Dictionary Maps: Creating objects withnullprototype eliminates prototype pollution and avoids collisions withtoStringorhasOwnProperty. - Implement Deep Freeze for Configuration Objects: Write a recursive helper function to deeply freeze nested configuration trees.
- Use Getters/Setters for Input Validation: Encapsulate internal object invariants with getters and setters instead of exposing raw mutable properties.
- Use
Object.hasOwn()OverhasOwnProperty(): Use ES2022Object.hasOwn(obj, 'prop')to test own properties safely, avoiding prototype shadowing issues.