Objects and Prototypes
An object is a collection of key/value pairs where keys are strings or symbols. Objects are handled by reference, and they inherit from other objects through a hidden link called the prototype — the mechanism that class syntax is built on top of.
Object literals
Section titled “Object literals”const user = { name: 'Ada', age: 36, 'multi word': true, // quotes needed for non-identifier keys greet() { // method shorthand return `Hi ${this.name}`; },};Keys are always strings or symbols. Any other key is converted to a string:
const obj = { 1: 'one', true: 'yes' };Object.keys(obj); // => ['1', 'true']obj[1] === obj['1']; // => trueAccess
Section titled “Access”user.name; // dot notation — fixed, identifier-like keyuser['multi word']; // bracket notation — any string, or a variableconst key = 'age';user[key]; // => 36user.missing; // => undefined (no error)user.a?.b; // => undefined (optional chaining guards the next link)Shorthand and computed keys
Section titled “Shorthand and computed keys”const name = 'Ada';const age = 36;const person = { name, age }; // => { name: 'Ada', age: 36 }
const field = 'status';const record = { [field]: 'active', [`${field}_at`]: Date.now(),};// => { status: 'active', status_at: 1754... }Adding, removing, checking
Section titled “Adding, removing, checking”user.email = 'ada@example.com'; // adddelete user.age; // remove
'name' in user; // => true (includes inherited keys)Object.hasOwn(user, 'name'); // => true (own keys only; ES2022)user.hasOwnProperty('name'); // => true (older equivalent)Enumerating
Section titled “Enumerating”const o = { a: 1, b: 2 };
Object.keys(o); // => ['a', 'b']Object.values(o); // => [1, 2]Object.entries(o); // => [['a', 1], ['b', 2]]
Object.fromEntries([['a', 1], ['b', 2]]); // => { a: 1, b: 2 }Key order is specified: integer-like keys first in ascending numeric order, then string keys in insertion order, then symbols.
Object.keys({ b: 1, 2: 2, a: 3, 1: 4 }); // => ['1', '2', 'b', 'a']Reference vs value
Section titled “Reference vs value”Primitives are copied by value. Objects are copied by reference — the variable holds a pointer, and assignment copies the pointer, not the data.
let a = 1;let b = a;b = 2;a; // => 1 (independent)
const x = { n: 1 };const y = x;y.n = 2;x.n; // => 2 (same object)x === y; // => trueThis is the single most common source of “why did my data change?” bugs. It applies to function arguments too:
function mutate(obj) { obj.changed = true; }function reassign(obj) { obj = { fresh: true }; } // only rebinds the local parameter
const target = {};mutate(target); // target => { changed: true }reassign(target); // target unchangedThe object is passed by sharing: the function gets a copy of the reference. It can mutate what the reference points at, but reassigning the parameter has no effect outside.
Copying objects
Section titled “Copying objects”Shallow copy
Section titled “Shallow copy”{ ...obj } and Object.assign copy own enumerable properties one level deep. Nested objects are still shared.
const original = { name: 'Ada', tags: ['a', 'b'] };
const copy = { ...original };copy.name = 'Grace'; // independentcopy.tags.push('c'); // shared! original.tags is now ['a','b','c']Object.assign({}, original); // same as spreadObject.assign(target, src1, src2); // mutates target, later sources winconst withOverride = { ...original, name: 'Grace' }; // later keys winDeep copy with structuredClone
Section titled “Deep copy with structuredClone”structuredClone (built into modern browsers and Node 17+) creates a genuine deep copy using the structured clone algorithm.
const original = { name: 'Ada', nested: { tags: ['a'] }, when: new Date() };const deep = structuredClone(original);
deep.nested.tags.push('b');original.nested.tags; // => ['a'] — untoucheddeep.when instanceof Date; // => trueIt handles cycles, Date, RegExp, Map, Set, ArrayBuffer, and typed arrays. It throws on functions, symbols, DOM nodes, and class instances lose their prototype (they come back as plain objects).
structuredClone({ fn: () => {} }); // DataCloneErrorThe old JSON.parse(JSON.stringify(obj)) trick still works for pure JSON data but silently drops undefined, functions, and symbols, turns Date into a string, and throws on cycles.
Property descriptors
Section titled “Property descriptors”Every property has a descriptor: either a data descriptor (value, writable) or an accessor descriptor (get, set), plus enumerable and configurable.
const o = { a: 1 };Object.getOwnPropertyDescriptor(o, 'a');// => { value: 1, writable: true, enumerable: true, configurable: true }Properties created with defineProperty default every flag to false:
const config = {};Object.defineProperty(config, 'version', { value: '1.0', writable: false, enumerable: false, configurable: false,});
config.version = '2.0'; // silently ignored (TypeError in strict mode)Object.keys(config); // => [] (not enumerable)JSON.stringify(config); // => '{}'config.version; // => '1.0'| Flag | Meaning when false |
|---|---|
writable |
assignment to the property is rejected |
enumerable |
hidden from Object.keys, for..in, spread, JSON.stringify |
configurable |
cannot be deleted or redefined (and writable cannot go back to true) |
Getters and setters
Section titled “Getters and setters”Accessor properties look like data but run a function.
const account = { first: 'Ada', last: 'Lovelace',
get fullName() { return `${this.first} ${this.last}`; },
set fullName(value) { [this.first, this.last] = value.split(' '); },};
account.fullName; // => 'Ada Lovelace' (no parentheses)account.fullName = 'Grace Hopper';account.first; // => 'Grace'Use them for computed values and validation:
const temp = { _celsius: 0, get fahrenheit() { return this._celsius * 9 / 5 + 32; }, set celsius(v) { if (typeof v !== 'number') throw new TypeError('celsius must be a number'); this._celsius = v; },};Preventing changes
Section titled “Preventing changes”Three levels, from weakest to strongest:
const o = { a: 1, nested: { b: 2 } };
Object.preventExtensions(o); // no new properties; existing ones still writable/deletableObject.seal(o); // + no deletions, no reconfiguration; values still writableObject.freeze(o); // + values immutableconst frozen = Object.freeze({ a: 1, nested: { b: 2 } });
frozen.a = 99; // ignored (TypeError in strict mode)delete frozen.a; // ignoredfrozen.nested.b = 99; // WORKS — freeze is shallowObject.isFrozen(frozen); // => trueA recursive deep freeze:
function deepFreeze(obj) { for (const value of Object.values(obj)) { if (value && typeof value === 'object') deepFreeze(value); } return Object.freeze(obj);}Prototypes
Section titled “Prototypes”Every object has an internal link, [[Prototype]], to another object or null. When you read a property that the object does not own, the engine follows that link, and keeps following until it finds the property or reaches null. That is the prototype chain.
const animal = { eats: true, describe() { return `eats: ${this.eats}`; },};
const rabbit = Object.create(animal);rabbit.jumps = true;
rabbit.jumps; // => true (own)rabbit.eats; // => true (from animal)rabbit.describe(); // => 'eats: true' — `this` is still rabbitObject.hasOwn(rabbit, 'eats'); // => falseThe chain for a plain object literal is obj → Object.prototype → null. That is where toString, hasOwnProperty, and valueOf come from.
Object.getPrototypeOf({}) === Object.prototype; // => trueObject.getPrototypeOf(Object.prototype); // => nullObject.getPrototypeOf([]) === Array.prototype; // => trueWrites never traverse the chain. Assigning creates an own property that shadows the inherited one (unless the prototype defines a setter):
rabbit.eats = false;animal.eats; // => true — unaffectedproto vs prototype
Section titled “proto vs prototype”These two names are confused constantly.
obj.__proto__— a legacy accessor for the object’s own prototype link. Every object has it. Deprecated; useObject.getPrototypeOf/Object.setPrototypeOf.Fn.prototype— a plain property on functions. It is the object that will become the[[Prototype]]of instances created withnew Fn(). A function’sprototypeis not the function’s own prototype.
function Dog(name) { this.name = name; }Dog.prototype.speak = function () { return `${this.name} barks`; };
const rex = new Dog('Rex');rex.speak(); // => 'Rex barks'Object.getPrototypeOf(rex) === Dog.prototype; // => truerex.__proto__ === Dog.prototype; // => true (same thing, legacy)Dog.prototype.constructor === Dog; // => trueWhat new Dog('Rex') does, in order:
- Create an empty object.
- Set its
[[Prototype]]toDog.prototype. - Call
Dogwiththisbound to that object. - Return that object — unless the constructor explicitly returns a different object.
Object.create(null) makes an object with no prototype — a clean dictionary with no inherited keys, safe for arbitrary user-supplied keys.
const dict = Object.create(null);dict.toString; // => undefined'toString' in dict; // => falseClasses
Section titled “Classes”class is syntax over the prototype system. Methods go on Class.prototype; there is no new inheritance mechanism underneath.
class Animal { constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }}
const a = new Animal('Cat');a.speak(); // => 'Cat makes a sound'Object.getPrototypeOf(a) === Animal.prototype; // => truetypeof Animal; // => 'function'Class bodies are always strict mode, and classes are not hoisted for use — they sit in the TDZ, so new Foo() before the declaration throws. Calling a class without new also throws.
extends and super
Section titled “extends and super”class Dog extends Animal { constructor(name, breed) { super(name); // must run before using `this` this.breed = breed; }
speak() { return `${super.speak()} — a bark`; // call the parent method }}
const d = new Dog('Rex', 'terrier');d.speak(); // => 'Rex makes a sound — a bark'd instanceof Dog; // => trued instanceof Animal; // => trueextends wires two chains: instance methods (Dog.prototype → Animal.prototype) and statics (Dog → Animal).
Fields, static members, and private fields
Section titled “Fields, static members, and private fields”class Counter { count = 0; // instance field, set before the constructor body #secret = 'hidden'; // private field static instances = 0; // static field static #registry = new Map(); // static private field
constructor() { Counter.instances++; }
increment() { this.count++; this.#log(); return this.count; }
#log() { // private method // only callable from inside the class }
get doubled() { // getter return this.count * 2; }
static create() { // static method — called on the class return new Counter(); }
static { // static initialization block (ES2022) this.#registry.set('init', true); }}
const c = Counter.create();c.increment(); // => 1c.doubled; // => 2Counter.instances; // => 1c.#secret; // SyntaxError — not accessible outside the class body# fields are hard private, enforced by the language: not visible in Object.keys, JSON.stringify, or a debugger’s property list, and inaccessible even via bracket notation. This is unlike the _underscore convention, which is only a hint.
The in operator does a safe brand check on private fields:
class Point { #x = 0; static isPoint(o) { return #x in o; }}Point.isPoint(new Point()); // => truePoint.isPoint({}); // => falseinstanceof
Section titled “instanceof”x instanceof C walks x’s prototype chain looking for C.prototype.
[] instanceof Array; // => true[] instanceof Object; // => true (Array.prototype inherits from Object.prototype)// Equivalent to:Object.prototype.isPrototypeOf.call(Array.prototype, []); // => trueCustom errors
Section titled “Custom errors”Extending Error is the main practical use of extends in application code:
class ValidationError extends Error { constructor(field, message) { super(message); this.name = 'ValidationError'; this.field = field; }}
try { throw new ValidationError('email', 'must contain @');} catch (err) { if (err instanceof ValidationError) { console.log(err.name, err.field, err.message); // => ValidationError email must contain @ }}Key points
Section titled “Key points”- Object keys are strings or symbols; integer-like keys enumerate first, then string keys in insertion order.
- Objects are handled by reference. Assignment, function arguments, and shallow copies all share nested data.
{ ...obj }andObject.assignare one level deep;structuredCloneis a real deep copy but throws on functions and drops class prototypes.- Descriptors control
writable,enumerable, andconfigurable;definePropertydefaults them all tofalse. Object.freezeis shallow — writedeepFreezeif you need more.- Property reads walk the prototype chain; writes always create an own property.
__proto__is an object’s own prototype link;Fn.prototypeis what instances ofFnwill inherit from. Never reassign prototypes at runtime.classis prototype syntax: methods on the prototype, fields per instance,#fields genuinely private,super()required beforethisin a derived constructor.instanceofbreaks across realms; useArray.isArrayfor arrays.