JavaScript Objects, Prototypes, and Classes
- Description: Object literals, property descriptors, the prototype chain, classes, inheritance, private fields, and the built-in keyed collections (
Map,Set). - My Notion Note ID: K2A-F4-5
- Created: 2018-03-23
- Updated: 2026-05-17
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Objects as Bags of Properties
- 2. Property Access and Destructuring
- 3. Property Descriptors
- 4. The Prototype Chain
- 5. Classes
- 6. Inheritance and
super - 7. Private Fields and Methods
- 8. Built-In Keyed Collections
- 9. References
1. Objects as Bags of Properties
An object is an unordered (well, mostly — see § 1.3) collection of key/value pairs. Keys are strings or Symbols; values are anything.
const user = {
name: 'Yu',
age: 30,
greet() { return `Hi ${this.name}`; },
['computed-' + 'key']: 'value',
};
1.1 Shorthand
const x = 1, y = 2;
const point = { x, y }; // { x: 1, y: 2 }
const obj = { greet() { /* … */ } }; // method shorthand
1.2 Spread and Rest
const base = { a: 1, b: 2 };
const extended = { ...base, c: 3 }; // { a:1, b:2, c:3 }
const overridden = { ...base, a: 99 }; // { a:99, b:2 } — later wins
const { a, ...rest } = extended; // rest = { b: 2, c: 3 }
Spread is a shallow copy — nested objects share references.
1.3 Key Ordering
Modern engines order keys deterministically:
- Integer-like string keys (
'0','1', …) in numeric order. - Other string keys in insertion order.
- Symbol keys in insertion order.
Don't rely on this for semantic ordering — use an array if order is part of the meaning. For numeric-style keys, use Map.
2. Property Access and Destructuring
user.name; // dot — only valid identifier names
user['name']; // bracket — any expression
user[`${prefix}name`]; // computed
user.address?.city; // optional chaining — undefined if address is nullish
user.address?.city ?? 'unknown'; // combine with ??
delete user.age; // remove a property
'name' in user; // true
user.hasOwnProperty('name'); // true, own properties only (legacy)
Object.hasOwn(user, 'name'); // true — preferred since ES2022
Destructuring:
const { name, age = 0, address: { city } = {} } = user;
const { name: userName } = user; // rename
const [first, , third, ...rest] = arr; // array
Combine destructuring with rename + default:
const { port: p = 3000 } = config;
Spread vs rest in destructuring — same syntax, different position:
const [head, ...tail] = arr;
const { x, ...others } = obj;
3. Property Descriptors
Each property has a descriptor — visible via Object.getOwnPropertyDescriptor:
{
value: …,
writable: boolean,
enumerable: boolean,
configurable: boolean,
}
Plus accessor descriptors:
{
get: function,
set: function,
enumerable: boolean,
configurable: boolean,
}
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Yu',
writable: false,
enumerable: false,
configurable: false,
});
obj.name = 'X'; // silently fails (or throws in strict mode)
Getters / setters in object literals:
const temp = {
_celsius: 0,
get fahrenheit() { return this._celsius * 9/5 + 32; },
set fahrenheit(value) { this._celsius = (value - 32) * 5/9; },
};
temp.fahrenheit = 212;
temp._celsius; // 100
Locking objects:
| Method | Effect |
|---|---|
Object.preventExtensions(obj) |
Can't add new properties. |
Object.seal(obj) |
+ Can't delete or reconfigure. Values still mutable. |
Object.freeze(obj) |
+ Can't change values. Shallow. |
Use sparingly — TypeScript's readonly covers many practical use cases at compile time.
4. The Prototype Chain
Every object has an internal [[Prototype]] link to another object (or null). Property lookup walks the chain.
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
rabbit.eats; // true — found on animal
rabbit.jumps; // true — own
Object.getPrototypeOf(rabbit) === animal; // true
for (const key in rabbit) console.log(key);
// jumps, eats — inherited keys included
Object.keys(rabbit); // ['jumps'] — own keys only
rabbit.hasOwnProperty('eats'); // false
Object.hasOwn(rabbit, 'eats'); // false
Object.create(proto, props?) builds an object with the given prototype.
__proto__ (legacy accessor): same as [[Prototype]]. Avoid — use Object.getPrototypeOf / Object.setPrototypeOf. Changing a prototype after construction is slow; declare via class or Object.create.
4.1 Constructor Functions (Pre-Class Pattern)
function User(name) {
this.name = name;
}
User.prototype.greet = function () { return `Hi ${this.name}`; };
const u = new User('Yu');
u.greet(); // 'Hi Yu'
Object.getPrototypeOf(u) === User.prototype; // true
new does four things:
- Creates a fresh object.
- Sets its
[[Prototype]]toFn.prototype. - Calls
Fnwiththisbound to the new object. - Returns the new object (unless
Fnreturns its own object).
class syntax is sugar over this. Use class.
4.2 instanceof
u instanceof User; // true
u instanceof Object; // true — Object.prototype is on the chain
Walks the prototype chain looking for the right-hand side's .prototype. Configurable via Symbol.hasInstance.
5. Classes
class User {
// Fields (instance properties initialised on construction)
active = true;
loginCount = 0;
// Static fields (on the class itself)
static defaultRole = 'guest';
// Constructor
constructor(name, age) {
this.name = name;
this.age = age;
}
// Methods
greet() { return `Hi ${this.name}`; }
// Static methods
static create(data) { return new User(data.name, data.age); }
// Getter / setter
get isAdult() { return this.age >= 18; }
set ageStr(s) { this.age = Number(s); }
}
const u = new User('Yu', 30);
u.greet();
u.isAdult;
User.create({ name: 'Yu', age: 30 });
User.defaultRole;
Class bodies are strict-mode by default. Methods are non-enumerable. Class declarations are not hoisted in the way function declarations are — TDZ applies.
Class expressions:
const User = class {
greet() { /* ... */ }
};
Useful for factories or returning anonymous classes from functions.
6. Inheritance and super
class Admin extends User {
constructor(name, age, perms) {
super(name, age); // must come before using this
this.perms = perms;
}
greet() {
return `${super.greet()} [admin]`;
}
static create(data) {
const admin = super.create(data);
admin.perms = data.perms ?? [];
return admin;
}
}
Rules:
super(...)calls the parent constructor. Must be called beforethisin a subclass constructor.super.method(...)calls the parent method, withthisstill pointing at the subclass instance.- A class can extend any constructor — including
Array,Error,Map, native built-ins. - Multiple inheritance isn't supported; mixins do the job via functions returning class expressions.
6.1 Mixins
const Serializable = (Base) => class extends Base {
toJSON() { return JSON.stringify(this); }
};
const Loggable = (Base) => class extends Base {
log() { console.log(this.toJSON ? this.toJSON() : this); }
};
class Doc extends Loggable(Serializable(Object)) {}
Mixins are functions that take a base class and return a subclass. Composable, less ceremony than the diamond-problem-prone multiple inheritance.
7. Private Fields and Methods
class Counter {
#count = 0; // private field
#log() { console.log(this.#count); } // private method
increment() {
this.#count++;
this.#log();
}
}
const c = new Counter();
c.increment();
c.#count; // SyntaxError — really private
The # prefix is a true language-level private — not a convention. They're per-class (subclasses don't see them), hard-private (can't be reflected at runtime), and lexically scoped to the class body.
Pre-private-field codebases used _underscore as a convention. New code should use #.
Static private:
class Tracker {
static #count = 0;
static increment() { Tracker.#count++; }
static get count() { return Tracker.#count; }
}
8. Built-In Keyed Collections
8.1 Map
const m = new Map();
m.set('a', 1);
m.set(obj, 'value'); // any value as key — including objects
m.get('a'); // 1
m.has('a'); // true
m.delete('a');
m.size; // 1
m.clear();
for (const [k, v] of m) { /* … */ }
[...m.entries()]; // [[k, v], ...]
[...m.keys()];
[...m.values()];
new Map([['a', 1], ['b', 2]]); // initialise from pairs
new Map(Object.entries(obj)); // from object
Object.fromEntries(m); // back to object (string/symbol keys only)
Map vs plain object:
| Concern | Object | Map |
|---|---|---|
| Key types | string / symbol only | anything |
| Size lookup | manual (Object.keys().length) |
.size |
Prototype collisions (toString, constructor) |
risk | none |
| Order | spec'd (integer keys first) | insertion order, always |
| Iteration | for…in, Object.keys |
for…of directly |
| Performance | optimised hidden-class paths if shape stable | optimised general-purpose hash |
Use Map when keys aren't strings, when you need ordered iteration, or when you're using the object purely as a dictionary.
8.2 Set
const s = new Set([1, 2, 3]);
s.add(4);
s.has(2);
s.delete(2);
s.size;
[...s]; // [1, 3, 4]
// Dedupe an array
[...new Set(arr)];
// ES2025 set algebra
s.union(other);
s.intersection(other);
s.difference(other);
s.symmetricDifference(other);
s.isSubsetOf(other);
s.isSupersetOf(other);
s.isDisjointFrom(other);
Set algebra methods land in 2024-2025 browsers; check support if you target older runtimes.
8.3 WeakMap / WeakSet
Keys (Maps) or values (Sets) are held weakly — they don't prevent garbage collection. Not enumerable.
const cache = new WeakMap();
function annotate(obj, data) {
cache.set(obj, data);
}
// When obj is no longer referenced anywhere else, the cache entry vanishes.
Use cases: caches keyed on objects, private state for foreign objects (rare in modern code now that we have # fields).
9. References
- MDN Object — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
- MDN Classes — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
- MDN Inheritance and the prototype chain — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
- MDN Private class features — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties
- MDN Map / Set — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
- See also: javascript-functions-scope-and-closures