JavaScript — Objects & Prototypes
Objects are the fundamental building blocks of JavaScript. Almost everything in JavaScript is an object or behaves like one — arrays, functions, dates, regular expressions, and even primitive values get temporarily wrapped in objects when you access properties on them. Understanding objects and the prototype system is the key to mastering JavaScript.
Unlike class-based languages such as Java or C++, JavaScript uses a prototypal inheritance model. Objects inherit directly from other objects through a chain of prototypes. ES2015 introduced class syntax, but it remains syntactic sugar over the prototypal inheritance system — the underlying mechanism has not changed.
This page covers object literals, property descriptors, prototypal inheritance, the prototype chain, object creation patterns, and modern object methods. By the end, you will understand how JavaScript objects truly work under the hood.
| 1 | // Objects are collections of key-value pairs |
| 2 | const user = { |
| 3 | name: "Alice", |
| 4 | age: 30, |
| 5 | role: "admin", |
| 6 | greet() { |
| 7 | console.log(`Hello, I'm ${this.name}`); |
| 8 | }, |
| 9 | }; |
| 10 | |
| 11 | // Everything is an object (almost) |
| 12 | console.log(typeof {}); // "object" |
| 13 | console.log(typeof []); // "object" |
| 14 | console.log(typeof new Date()); // "object" |
| 15 | console.log(typeof /regex/); // "object" |
| 16 | console.log(typeof function(){}); // "function" (but callable object) |
| 17 | |
| 18 | // Primitives are wrapped temporarily |
| 19 | console.log("hello".toUpperCase()); // "HELLO" — string wrapped in String object |
| 20 | console.log((42).toString()); // "42" — number wrapped in Number object |
| 21 | |
| 22 | // Prototype chain in action |
| 23 | const arr = [1, 2, 3]; |
| 24 | console.log(arr.toString()); // "1,2,3" — from Array.prototype |
| 25 | console.log(arr.hasOwnProperty(0)); // true — from Object.prototype |
| 26 | // arr → Array.prototype → Object.prototype → null |
Object literals (curly braces ) are the most common way to create objects. ES2015 added several enhancements: shorthand property names, computed property keys, method definitions, and spread properties.
| 1 | // Basic object literal |
| 2 | const person = { |
| 3 | firstName: "John", |
| 4 | lastName: "Doe", |
| 5 | age: 25, |
| 6 | }; |
| 7 | |
| 8 | // Property shorthand — variable name becomes key |
| 9 | const name = "Alice"; |
| 10 | const role = "admin"; |
| 11 | const user = { name, role }; |
| 12 | console.log(user); // { name: "Alice", role: "admin" } |
| 13 | |
| 14 | // Method shorthand |
| 15 | const api = { |
| 16 | get(url) { |
| 17 | return fetch(url); |
| 18 | }, |
| 19 | async post(url, data) { |
| 20 | const res = await fetch(url, { |
| 21 | method: "POST", |
| 22 | body: JSON.stringify(data), |
| 23 | }); |
| 24 | return res.json(); |
| 25 | }, |
| 26 | }; |
| 27 | |
| 28 | // Computed property keys — dynamic property names |
| 29 | const key = "dynamicKey"; |
| 30 | const obj = { |
| 31 | [key]: "computed value", |
| 32 | [`prop_${Date.now()}`]: "timestamped", |
| 33 | ["method_" + "name"]() { |
| 34 | return "dynamic method"; |
| 35 | }, |
| 36 | }; |
| 37 | console.log(obj.dynamicKey); // "computed value" |
| 38 | |
| 39 | // Spread properties — merge objects (shallow) |
| 40 | const defaults = { theme: "dark", lang: "en", debug: false }; |
| 41 | const overrides = { lang: "fr", debug: true }; |
| 42 | const config = { ...defaults, ...overrides }; |
| 43 | console.log(config); // { theme: "dark", lang: "fr", debug: true } |
| 44 | |
| 45 | // Spread for immutability — creating copies |
| 46 | const original = { a: 1, b: { c: 2 } }; |
| 47 | const copy = { ...original }; |
| 48 | copy.a = 99; |
| 49 | copy.b.c = 42; |
| 50 | console.log(original.a); // 1 (not affected — shallow) |
| 51 | console.log(original.b.c); // 42 (affected — nested objects shared!) |
| 52 | |
| 53 | // Nested property access with destructuring |
| 54 | const data = { |
| 55 | user: { profile: { name: "Bob", age: 28 }, settings: { theme: "light" } }, |
| 56 | }; |
| 57 | const { user: { profile: { name, age }, settings: { theme } } } = data; |
| 58 | console.log(name, age, theme); // "Bob" 28 "light" |
| 59 | |
| 60 | // Rest properties — collect remaining properties |
| 61 | const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 }; |
| 62 | console.log(a); // 1 |
| 63 | console.log(b); // 2 |
| 64 | console.log(rest); // { c: 3, d: 4 } |
| 65 | |
| 66 | // Property existence check |
| 67 | console.log("toString" in {}); // true (inherited) |
| 68 | console.log({}.hasOwnProperty("toString")); // false (own only) |
| 69 | console.log(Object.hasOwn({}, "toString")); // false (modern way) |
| 70 | |
| 71 | // Object Sealing and Freezing |
| 72 | const sealed = Object.seal({ x: 1 }); // can't add/remove, can modify |
| 73 | sealed.x = 2; // OK |
| 74 | sealed.y = 3; // ignored (or TypeError in strict) |
| 75 | delete sealed.x; // ignored |
| 76 | |
| 77 | const frozen = Object.freeze({ x: 1 }); // completely immutable |
| 78 | frozen.x = 2; // ignored (or TypeError in strict) |
| 79 | console.log(frozen.x); // 1 |
info
Every property in JavaScript has a property descriptor — a set of attributes that define its behavior. These attributes control whether a property can be written, enumerated, or reconfigured. Understanding property descriptors gives you fine-grained control over object behavior.
| 1 | // Property descriptor attributes: |
| 2 | // value — the property value |
| 3 | // writable — can the value be changed? |
| 4 | // enumerable — does it show up in for...in / Object.keys? |
| 5 | // configurable — can the descriptor be changed or property deleted? |
| 6 | |
| 7 | // Getting a property descriptor |
| 8 | const obj = { x: 42 }; |
| 9 | const desc = Object.getOwnPropertyDescriptor(obj, "x"); |
| 10 | console.log(desc); |
| 11 | // { value: 42, writable: true, enumerable: true, configurable: true } |
| 12 | |
| 13 | // Default descriptor for Object.defineProperty |
| 14 | const controlled = {}; |
| 15 | Object.defineProperty(controlled, "readonly", { |
| 16 | value: "cannot change me", |
| 17 | writable: false, // cannot reassign |
| 18 | enumerable: true, // shows up in enumeration |
| 19 | configurable: false, // cannot delete or redefine |
| 20 | }); |
| 21 | |
| 22 | controlled.readonly = "try to change"; // silently ignored (or TypeError in strict) |
| 23 | console.log(controlled.readonly); // "cannot change me" |
| 24 | delete controlled.readonly; // false (cannot delete) |
| 25 | console.log("readonly" in controlled); // true — still there |
| 26 | |
| 27 | // Non-enumerable property |
| 28 | const hidden = {}; |
| 29 | Object.defineProperty(hidden, "secret", { |
| 30 | value: "hidden value", |
| 31 | enumerable: false, |
| 32 | }); |
| 33 | Object.defineProperty(hidden, "visible", { |
| 34 | value: "shown", |
| 35 | enumerable: true, |
| 36 | }); |
| 37 | |
| 38 | console.log(Object.keys(hidden)); // ["visible"] |
| 39 | console.log(hidden.secret); // "hidden value" |
| 40 | console.log("secret" in hidden); // true — exists but not enumerable |
| 41 | |
| 42 | // Property descriptors with getters/setters |
| 43 | const temperature = { |
| 44 | _celsius: 0, |
| 45 | get fahrenheit() { |
| 46 | return this._celsius * 9 / 5 + 32; |
| 47 | }, |
| 48 | set fahrenheit(value) { |
| 49 | this._celsius = (value - 32) * 5 / 9; |
| 50 | }, |
| 51 | }; |
| 52 | |
| 53 | temperature.fahrenheit = 212; |
| 54 | console.log(temperature._celsius); // 100 |
| 55 | console.log(temperature.fahrenheit); // 212 |
| 56 | |
| 57 | // Define multiple properties at once |
| 58 | Object.defineProperties(product, { |
| 59 | id: { value: 1, writable: false, configurable: false }, |
| 60 | name: { value: "Widget", writable: true, enumerable: true }, |
| 61 | price: { |
| 62 | get() { return this._price; }, |
| 63 | set(v) { |
| 64 | if (v < 0) throw new Error("Price cannot be negative"); |
| 65 | this._price = v; |
| 66 | }, |
| 67 | enumerable: true, |
| 68 | }, |
| 69 | }); |
| 70 | |
| 71 | // Prevent property addition |
| 72 | const locked = {}; |
| 73 | Object.preventExtensions(locked); |
| 74 | locked.a = 1; // fails silently (or TypeError in strict mode) |
| 75 | console.log("a" in locked); // false |
| 76 | |
| 77 | // Check property descriptor flags |
| 78 | console.log(Object.isExtensible(locked)); // false |
| 79 | console.log(Object.isSealed(controlled)); // false |
| 80 | console.log(Object.isFrozen(frozen)); // true |
pro tip
JavaScript's prototypal inheritance is fundamentally different from classical inheritance. Objects inherit directly from other objects via the [[Prototype]] internal slot (accessed via Object.getPrototypeOf or the deprecated __proto__ accessor). When you access a property that doesn't exist on the object itself, JavaScript walks up the prototype chain.
| 1 | // Every object has a prototype |
| 2 | const base = { type: "base" }; |
| 3 | const child = Object.create(base); |
| 4 | child.name = "child"; |
| 5 | |
| 6 | console.log(child.name); // "child" — own property |
| 7 | console.log(child.type); // "base" — inherited from prototype |
| 8 | console.log(child.toString); // function — inherited from Object.prototype |
| 9 | |
| 10 | // The prototype chain: |
| 11 | // child → base → Object.prototype → null |
| 12 | |
| 13 | // Creating objects with Object.create |
| 14 | const animal = { |
| 15 | init(name) { |
| 16 | this.name = name; |
| 17 | return this; |
| 18 | }, |
| 19 | speak() { |
| 20 | console.log(`${this.name} makes a sound`); |
| 21 | }, |
| 22 | }; |
| 23 | |
| 24 | const dog = Object.create(animal); |
| 25 | dog.init("Rex"); |
| 26 | dog.speak(); // "Rex makes a sound" |
| 27 | |
| 28 | // Setting and getting prototypes |
| 29 | const obj = {}; |
| 30 | console.log(Object.getPrototypeOf(obj) === Object.prototype); // true |
| 31 | |
| 32 | const newProto = { customMethod() { return "from proto"; } }; |
| 33 | Object.setPrototypeOf(obj, newProto); |
| 34 | console.log(obj.customMethod()); // "from proto" |
| 35 | |
| 36 | // Prototype chain traversal |
| 37 | function getPrototypeChain(obj) { |
| 38 | const chain = []; |
| 39 | let current = obj; |
| 40 | while (current) { |
| 41 | chain.push(current.constructor?.name || "null prototype"); |
| 42 | current = Object.getPrototypeOf(current); |
| 43 | } |
| 44 | return chain; |
| 45 | } |
| 46 | console.log(getPrototypeChain([])); |
| 47 | // ["Array", "Object", null] |
| 48 | |
| 49 | // Shadowing — own property overrides prototype |
| 50 | const proto = { value: 10 }; |
| 51 | const instance = Object.create(proto); |
| 52 | console.log(instance.value); // 10 (from proto) |
| 53 | |
| 54 | instance.value = 20; // creates own property — shadows proto |
| 55 | console.log(instance.value); // 20 (own) |
| 56 | console.log(proto.value); // 10 (unchanged) |
| 57 | delete instance.value; |
| 58 | console.log(instance.value); // 10 (proto is now visible again) |
| 59 | |
| 60 | // The instanceof operator checks the prototype chain |
| 61 | function Animal(name) { |
| 62 | this.name = name; |
| 63 | } |
| 64 | Animal.prototype.speak = function() { |
| 65 | console.log(`${this.name} says hello`); |
| 66 | }; |
| 67 | |
| 68 | const cat = new Animal("Whiskers"); |
| 69 | console.log(cat instanceof Animal); // true |
| 70 | console.log(cat instanceof Object); // true |
| 71 | console.log(cat.constructor === Animal); // true |
best practice
The Object constructor provides a rich set of static methods for interacting with objects. These methods cover property enumeration, copying, sealing, freezing, and more. Modern JavaScript development relies heavily on these methods.
| 1 | // Object.keys() — returns enumerable own property names |
| 2 | const user = { name: "Alice", age: 30, role: "admin" }; |
| 3 | console.log(Object.keys(user)); // ["name", "age", "role"] |
| 4 | |
| 5 | // Object.values() — returns enumerable own property values |
| 6 | console.log(Object.values(user)); // ["Alice", 30, "admin"] |
| 7 | |
| 8 | // Object.entries() — returns [key, value] pairs |
| 9 | console.log(Object.entries(user)); |
| 10 | // [["name", "Alice"], ["age", 30], ["role", "admin"]] |
| 11 | |
| 12 | // Object.fromEntries() — create object from entries |
| 13 | const entries = [["a", 1], ["b", 2], ["c", 3]]; |
| 14 | const obj = Object.fromEntries(entries); |
| 15 | console.log(obj); // { a: 1, b: 2, c: 3 } |
| 16 | |
| 17 | // Practical: transform object properties |
| 18 | const doubled = Object.fromEntries( |
| 19 | Object.entries(obj).map(([key, value]) => [key, value * 2]) |
| 20 | ); |
| 21 | console.log(doubled); // { a: 2, b: 4, c: 6 } |
| 22 | |
| 23 | // Practical: filter object properties |
| 24 | const filtered = Object.fromEntries( |
| 25 | Object.entries(user).filter(([_, value]) => typeof value === "string") |
| 26 | ); |
| 27 | console.log(filtered); // { name: "Alice", role: "admin" } |
| 28 | |
| 29 | // Object.assign() — copy properties (mutable) |
| 30 | const target = { a: 1 }; |
| 31 | const source1 = { b: 2 }; |
| 32 | const source2 = { c: 3, a: 99 }; // a will overwrite target.a |
| 33 | Object.assign(target, source1, source2); |
| 34 | console.log(target); // { a: 99, b: 2, c: 3 } |
| 35 | |
| 36 | // Object.assign for cloning |
| 37 | const clone = Object.assign({}, target); |
| 38 | console.log(clone); // { a: 99, b: 2, c: 3 } |
| 39 | |
| 40 | // Object.is() — strict equality for special cases |
| 41 | console.log(Object.is(NaN, NaN)); // true (=== would be false) |
| 42 | console.log(Object.is(0, -0)); // false (=== would be true) |
| 43 | console.log(Object.is(0, 0)); // true |
| 44 | console.log(Object.is("hello", "hello")); // true |
| 45 | |
| 46 | // Object.hasOwn() — modern check for own property (ES2022) |
| 47 | console.log(Object.hasOwn(user, "name")); // true |
| 48 | console.log(Object.hasOwn(user, "toString")); // false (inherited) |
| 49 | |
| 50 | // Compare with hasOwnProperty |
| 51 | console.log(user.hasOwnProperty("name")); // true |
| 52 | // Object.create(null) objects don't have hasOwnProperty |
| 53 | const nullProto = Object.create(null); |
| 54 | nullProto.x = 1; |
| 55 | // console.log(nullProto.hasOwnProperty("x")); // TypeError! |
| 56 | console.log(Object.hasOwn(nullProto, "x")); // true — works safely |
| 57 | |
| 58 | // Object.getOwnPropertyNames() — all own properties (including non-enumerable) |
| 59 | const withHidden = { visible: 1 }; |
| 60 | Object.defineProperty(withHidden, "hidden", { |
| 61 | value: 2, |
| 62 | enumerable: false, |
| 63 | }); |
| 64 | console.log(Object.keys(withHidden)); // ["visible"] |
| 65 | console.log(Object.getOwnPropertyNames(withHidden)); // ["visible", "hidden"] |
| 66 | |
| 67 | // Object.getOwnPropertySymbols() — symbol-keyed properties |
| 68 | const sym = Symbol("secret"); |
| 69 | const hasSymbol = { [sym]: "hidden by symbol" }; |
| 70 | console.log(Object.getOwnPropertySymbols(hasSymbol)); // [Symbol(secret)] |
| 71 | console.log(hasSymbol[sym]); // "hidden by symbol" |
note
The prototype chain is JavaScript's mechanism for inheritance and shared behavior. When you access a property on an object, JavaScript first checks the object's own properties. If not found, it walks up the prototype chain until the property is found or the chain ends at null.
| 1 | // Visualizing the prototype chain |
| 2 | // Object.prototype is at the top of most prototype chains |
| 3 | console.log(Object.prototype); |
| 4 | // { constructor: Object, hasOwnProperty: fn, toString: fn, ... } |
| 5 | |
| 6 | // Function objects have a prototype property |
| 7 | // (used when the function is called with new) |
| 8 | function Foo() {} |
| 9 | console.log(Foo.prototype); // { constructor: Foo } |
| 10 | console.log(typeof Foo.prototype); // "object" |
| 11 | |
| 12 | // The prototype chain for instances: |
| 13 | const foo = new Foo(); |
| 14 | // foo → Foo.prototype → Object.prototype → null |
| 15 | console.log(Object.getPrototypeOf(foo) === Foo.prototype); // true |
| 16 | console.log(Object.getPrototypeOf(Foo.prototype) === Object.prototype); // true |
| 17 | console.log(Object.getPrototypeOf(Object.prototype)); // null — end of chain |
| 18 | |
| 19 | // Constructor functions and prototypes |
| 20 | function Vehicle(type) { |
| 21 | this.type = type; |
| 22 | this.miles = 0; |
| 23 | } |
| 24 | |
| 25 | Vehicle.prototype.drive = function(miles) { |
| 26 | this.miles += miles; |
| 27 | console.log(`Driving ${miles} miles`); |
| 28 | }; |
| 29 | |
| 30 | Vehicle.prototype.getInfo = function() { |
| 31 | return `${this.type} with ${this.miles} miles`; |
| 32 | }; |
| 33 | |
| 34 | const car = new Vehicle("Car"); |
| 35 | car.drive(100); |
| 36 | console.log(car.getInfo()); // "Car with 100 miles" |
| 37 | |
| 38 | // Subclassing via prototype chain |
| 39 | function Motorcycle(type) { |
| 40 | Vehicle.call(this, type); // call parent constructor |
| 41 | this.wheels = 2; |
| 42 | } |
| 43 | |
| 44 | // Set up prototype chain: Motorcycle.prototype → Vehicle.prototype |
| 45 | Motorcycle.prototype = Object.create(Vehicle.prototype); |
| 46 | Motorcycle.prototype.constructor = Motorcycle; |
| 47 | |
| 48 | Motorcycle.prototype.wheelie = function() { |
| 49 | console.log("Popping a wheelie!"); |
| 50 | }; |
| 51 | |
| 52 | const bike = new Motorcycle("Sport"); |
| 53 | bike.drive(50); // inherited from Vehicle |
| 54 | bike.wheelie(); // own method |
| 55 | console.log(bike.getInfo()); // "Sport with 50 miles" |
| 56 | |
| 57 | // instanceof walks the prototype chain |
| 58 | console.log(bike instanceof Motorcycle); // true |
| 59 | console.log(bike instanceof Vehicle); // true |
| 60 | console.log(bike instanceof Object); // true |
| 61 | console.log(bike instanceof Array); // false |
| 62 | |
| 63 | // Checking the prototype chain |
| 64 | console.log(Vehicle.prototype.isPrototypeOf(bike)); // true |
| 65 | console.log(Object.prototype.isPrototypeOf(bike)); // true |
| 66 | |
| 67 | // Property resolution order: |
| 68 | // 1. Own properties |
| 69 | // 2. Prototype properties |
| 70 | // 3. Prototype's prototype properties |
| 71 | // ... until null |
| 72 | |
| 73 | // Override methods by shadowing |
| 74 | Motorcycle.prototype.drive = function(miles) { |
| 75 | console.log("Motorcycle driving differently"); |
| 76 | Vehicle.prototype.drive.call(this, miles); |
| 77 | }; |
| 78 | bike.drive(30); // "Motorcycle driving differently" then "Driving 30 miles" |
warning
JavaScript offers multiple patterns for creating objects and defining reusable object structures. Each pattern has different trade-offs regarding memory, performance, encapsulation, and inheritance.
| 1 | // Pattern 1: Object Literal — simple, one-off objects |
| 2 | const point = { x: 10, y: 20 }; |
| 3 | |
| 4 | // Pattern 2: Factory Function — returns a new object each call |
| 5 | function createUser(name, age) { |
| 6 | return { |
| 7 | name, |
| 8 | age, |
| 9 | greet() { |
| 10 | console.log(`Hi, I'm ${this.name}`); |
| 11 | }, |
| 12 | }; |
| 13 | } |
| 14 | const user1 = createUser("Alice", 30); |
| 15 | const user2 = createUser("Bob", 25); |
| 16 | // Each user gets its own copy of greet() — more memory |
| 17 | |
| 18 | // Pattern 3: Factory with shared methods |
| 19 | const userMethods = { |
| 20 | greet() { console.log(`Hi, I'm ${this.name}`); }, |
| 21 | isAdult() { return this.age >= 18; }, |
| 22 | }; |
| 23 | |
| 24 | function createUserEfficient(name, age) { |
| 25 | const user = Object.create(userMethods); |
| 26 | user.name = name; |
| 27 | user.age = age; |
| 28 | return user; |
| 29 | } |
| 30 | // Methods are shared via prototype — less memory |
| 31 | |
| 32 | // Pattern 4: Constructor Function |
| 33 | function User(name, age) { |
| 34 | this.name = name; |
| 35 | this.age = age; |
| 36 | } |
| 37 | User.prototype.greet = function() { |
| 38 | console.log(`Hi, I'm ${this.name}`); |
| 39 | }; |
| 40 | User.prototype.isAdult = function() { |
| 41 | return this.age >= 18; |
| 42 | }; |
| 43 | const user3 = new User("Charlie", 28); |
| 44 | |
| 45 | // Pattern 5: Class Syntax (ES2015) — syntactic sugar over constructors |
| 46 | class Person { |
| 47 | constructor(name, age) { |
| 48 | this.name = name; |
| 49 | this.age = age; |
| 50 | } |
| 51 | |
| 52 | greet() { |
| 53 | console.log(`Hello, I'm ${this.name}`); |
| 54 | } |
| 55 | |
| 56 | static createAnonymous() { |
| 57 | return new Person("Anonymous", 0); |
| 58 | } |
| 59 | } |
| 60 | const person1 = new Person("Diana", 32); |
| 61 | const anonymous = Person.createAnonymous(); |
| 62 | |
| 63 | // Pattern 6: Object.create with mixins |
| 64 | const canEat = { |
| 65 | eat(food) { console.log(`Eating ${food}`); }, |
| 66 | }; |
| 67 | const canWalk = { |
| 68 | walk(steps) { console.log(`Walking ${steps} steps`); }, |
| 69 | }; |
| 70 | const canTalk = { |
| 71 | talk(words) { console.log(`Says: ${words}`); }, |
| 72 | }; |
| 73 | |
| 74 | function createHuman(name) { |
| 75 | const human = Object.create(Object.assign({}, canEat, canWalk, canTalk)); |
| 76 | human.name = name; |
| 77 | return human; |
| 78 | } |
| 79 | const alice = createHuman("Alice"); |
| 80 | alice.eat("pizza"); |
| 81 | alice.walk(100); |
| 82 | alice.talk("Hello!"); |
| 83 | |
| 84 | // Pattern 7: Object.assign for composition |
| 85 | function createLogger(prefix) { |
| 86 | return { |
| 87 | log(msg) { console.log(`[${prefix}] ${msg}`); }, |
| 88 | info(msg) { console.log(`[${prefix}] INFO: ${msg}`); }, |
| 89 | error(msg) { console.log(`[${prefix}] ERROR: ${msg}`); }, |
| 90 | }; |
| 91 | } |
| 92 | |
| 93 | function createStore() { |
| 94 | const logger = createLogger("Store"); |
| 95 | return Object.assign({}, logger, { |
| 96 | state: {}, |
| 97 | setState(key, value) { |
| 98 | this.state[key] = value; |
| 99 | this.log(`State updated: ${key} = ${JSON.stringify(value)}`); |
| 100 | }, |
| 101 | }); |
| 102 | } |
best practice
JavaScript has evolved several approaches for creating private properties. From closures and conventions to true private fields (ES2022), each approach offers different trade-offs between privacy, performance, and debugging experience.
| 1 | // Approach 1: Convention — underscore prefix (no real privacy) |
| 2 | class Person { |
| 3 | constructor(name) { |
| 4 | this._name = name; // convention: private, but still accessible |
| 5 | } |
| 6 | getName() { return this._name; } |
| 7 | } |
| 8 | // const p = new Person("Alice"); |
| 9 | // p._name — still accessible, just a convention |
| 10 | |
| 11 | // Approach 2: Closure-based privacy |
| 12 | function createCounter() { |
| 13 | let count = 0; // truly private via closure |
| 14 | return { |
| 15 | increment() { return ++count; }, |
| 16 | decrement() { return --count; }, |
| 17 | getCount() { return count; }, |
| 18 | }; |
| 19 | } |
| 20 | const counter = createCounter(); |
| 21 | counter.increment(); |
| 22 | counter.increment(); |
| 23 | console.log(counter.getCount()); // 2 |
| 24 | console.log(counter.count); // undefined — truly private |
| 25 | |
| 26 | // Approach 3: WeakMap-based privacy |
| 27 | const _private = new WeakMap(); |
| 28 | |
| 29 | class BankAccount { |
| 30 | constructor(owner, balance) { |
| 31 | _private.set(this, { owner, balance }); |
| 32 | } |
| 33 | |
| 34 | getBalance() { |
| 35 | return _private.get(this).balance; |
| 36 | } |
| 37 | |
| 38 | deposit(amount) { |
| 39 | const data = _private.get(this); |
| 40 | data.balance += amount; |
| 41 | console.log(`Deposited ${amount}. Balance: ${data.balance}`); |
| 42 | } |
| 43 | |
| 44 | withdraw(amount) { |
| 45 | const data = _private.get(this); |
| 46 | if (amount > data.balance) throw new Error("Insufficient funds"); |
| 47 | data.balance -= amount; |
| 48 | return amount; |
| 49 | } |
| 50 | } |
| 51 | const account = new BankAccount("Alice", 1000); |
| 52 | console.log(account.getBalance()); // 1000 |
| 53 | console.log(account.balance); // undefined — private! |
| 54 | |
| 55 | // Approach 4: Private class fields (ES2022) — best in modern code |
| 56 | class Wallet { |
| 57 | #balance = 0; // private field |
| 58 | #owner; // private field |
| 59 | |
| 60 | constructor(owner, initialBalance = 0) { |
| 61 | this.#owner = owner; |
| 62 | this.#balance = initialBalance; |
| 63 | } |
| 64 | |
| 65 | #logTransaction(type, amount) { // private method |
| 66 | console.log(`${type}: ${amount}. Balance: ${this.#balance}`); |
| 67 | } |
| 68 | |
| 69 | deposit(amount) { |
| 70 | this.#balance += amount; |
| 71 | this.#logTransaction("Deposit", amount); |
| 72 | } |
| 73 | |
| 74 | withdraw(amount) { |
| 75 | if (amount > this.#balance) throw new Error("Insufficient funds"); |
| 76 | this.#balance -= amount; |
| 77 | this.#logTransaction("Withdrawal", amount); |
| 78 | return amount; |
| 79 | } |
| 80 | |
| 81 | get owner() { return this.#owner; } |
| 82 | } |
| 83 | |
| 84 | const wallet = new Wallet("Bob", 500); |
| 85 | wallet.deposit(200); |
| 86 | // wallet.#balance — SyntaxError! Truly private at language level |
| 87 | // wallet.#logTransaction — SyntaxError! Cannot access private method |
| 88 | |
| 89 | console.log(wallet.owner); // "Bob" — via getter |
pro tip
Mastering JavaScript objects requires not just understanding the mechanics but also applying them wisely. These best practices will help you write robust, maintainable object-oriented JavaScript.
best practice
Pattern Comparison Table
| Pattern | Memory | Encapsulation | Inheritance | Best For |
|---|---|---|---|---|
| Object Literal | Per-instance | None | Prototype chain | Simple data, configs |
| Factory Function | Per-instance | Closure | Object.create | Private state, composition |
| Constructor | Shared methods | None | Prototype chain | Classic OOP patterns |
| Class | Shared methods | # fields | extends | Modern OOP, complex models |
| Object.create | Shared prototype | None | Direct prototypal | Mixins, dynamic prototypes |