JavaScript NotesRohit’s interview study guide
Chapter 03

Objects & Prototypes

How objects are created, how the prototype chain works, own vs inherited properties, descriptors, symbols and proxies.

20 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#Ways to create objects

A JavaScript object is two things: a set of own properties (key → value pairs stored on the object itself) and one hidden link, [[Prototype]], to another object it falls back to when a key is missing. Every way of creating an object is really answering two questions: what own properties does it start with, and what does its prototype link point to?

That's the mental model to carry through this whole chapter. An object literal links to Object.prototype. Object.create(x) links to whatever x you pass. new Fn() links to Fn.prototype. A class does the same as new Fn() with nicer syntax. A factory function just returns a literal. Once you see the six forms below as "different ways of setting that one link", they stop feeling like six unrelated features.

JavaScript
// 1. Object literal — the everyday way
const person = { firstName: "John", lastName: "Doe", age: 50 };

// 2. Object constructor — same result, more typing
const p2 = new Object();
p2.firstName = "John";

// 3. Object.create — choose the prototype explicitly
const personProto = {
  greet() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age}.`);
  },
};
const p3 = Object.create(personProto);
p3.name = "John";
p3.age = 30;
p3.greet(); // Hello, my name is John and I'm 30.

// 4. Constructor function + new
function Person(name) {
  this.name = name;
}
const p4 = new Person("Alice");

// 5. ES2015 class (sugar over 4)
class PersonClass {
  constructor(name) {
    this.name = name;
  }
}

// 6. Factory function — no `new`, no `this`
const createPerson = (name) => ({ name, greet: () => `Hi ${name}` });
What’s happening
  1. Literal: person gets three own properties (firstName, lastName, age) and its [[Prototype]] is set to Object.prototype automatically. That link is why person.toString() works even though you never wrote a toString.
  2. new Object() builds exactly the same thing as {} — an empty object linked to Object.prototype — and then p2.firstName = "John" adds one own property. There's no advantage over the literal, which is why nobody writes it.
  3. Object.create(personProto) makes an empty object whose [[Prototype]] is personProto. The two assignments give p3 own properties name: "John" and age: 30. When you call p3.greet(), JavaScript doesn't find greet on p3, follows the link to personProto, finds it there, and calls it with this = p3 (the object left of the dot) — so it prints John and 30.
  4. new Person("Alice") creates an empty object linked to Person.prototype, runs Person with this pointing at that object (so this.name = "Alice" becomes an own property), and returns it. p4 is { name: "Alice" } with Person.prototype behind it — see What new actually does.
  5. class PersonClass produces the same shape as step 4: a constructor function plus a PersonClass.prototype object. The differences are guard rails — calling PersonClass() without new throws TypeError: Class constructor PersonClass cannot be invoked without 'new', and the class body always runs in strict mode.
  6. The factory is a plain function that returns a new literal, so the result is linked to Object.prototype. greet is an arrow that closes over the parameter name instead of reading this.name, so it keeps working even when detached: const { greet } = createPerson("Ann"); greet() returns "Hi Ann". The cost is that every call builds a new greet function (see Methods go on .prototype).
AdvancedWhich creation style to reach for

Which one to reach for:

  • Literal for data, config and one-off objects — almost all objects in real code.
  • Object.create(proto) when you want to pick the prototype yourself, most often Object.create(null) for a null-prototype dictionary.
  • class (or a constructor function in older code) when you'll create many objects that share behaviour. Methods live once on the prototype and every instance uses them.
  • Factory when you want privacy through closures and no this pitfalls, and you're not creating so many objects that the per-object functions matter.

#Functions inside objects: two ways

A method is just a property whose value is a function. There's no separate "method" type: what makes it feel like a method is calling it with a dot, obj.greet(), which sets this to obj for that call. Both syntaxes below give you a property holding a function, and both get this from the call site. They differ in two small but interview-worthy ways.

JavaScript
const obj = {
  // 1. Method shorthand — has its own `this`, can use `super`
  greet() {
    return `Hi ${this.name}`;
  },
  // 2. Property holding a function
  greet2: function () {
    return `Hi ${this.name}`;
  },
  // (an arrow function here would NOT get `this` — see the `this` chapter)
  name: "Rohit",
};
What’s happening
  1. obj.greet() and obj.greet2() both return "Hi Rohit". In each case the call has obj left of the dot, so this is obj and this.name reads "Rohit".
  2. The order of keys doesn't matter: name is defined after the functions, but by the time either function runs, the whole literal has been built and name exists.
  3. Neither function is permanently tied to obj. Pull one out (const { greet } = obj; greet()) and this is no longer obj — it's undefined in strict mode (so this.name throws) or the global object in sloppy mode. this is decided by how the function is called, not where it was written.
  4. An arrow function as a property (greet3: () => "Hi " + this.name) has no this of its own. It uses the this of the code surrounding the object literal — and an object literal doesn't create a new this. At the top of an ES module that's undefined, so it throws; in a CommonJS file it's module.exports, so it returns "Hi undefined". Neither is obj.
AdvancedShorthand methods and super

Shorthand methods are the modern default. One subtle difference: shorthand methods can't be used with new and can call super.method() when the object has a prototype.

JavaScript
const base = { hello() { return "base hello"; } };
const child = {
  __proto__: base, // in a literal, this sets the prototype
  hello() {
    return `child → ${super.hello()}`;
  },
};
child.hello(); // "child → base hello"

new obj.greet2(); // greet2 {} — an ordinary function can be a constructor
new obj.greet();  // TypeError: obj.greet is not a constructor
What’s happening
  1. __proto__: base inside an object literal is special syntax that sets child's [[Prototype]] to base (it does not create a property called "__proto__").
  2. Shorthand methods remember the object they were defined in (internally, their [[HomeObject]] is child). super.hello() means "look up hello starting from the prototype of my home object" — so it skips child's own hello, finds base.hello, and calls it with the same this. Result: "child → base hello".
  3. A function expression has no home object, so writing super inside hello: function () { … } isn't even allowed — it's a SyntaxError: 'super' keyword unexpected here when the code is parsed.
  4. new obj.greet2() works because greet2 is an ordinary function: new creates an empty object linked to greet2.prototype, runs the function, ignores the returned string (it's a primitive), and gives back the empty object.
  5. new obj.greet() throws because shorthand methods are created without a [[Construct]] behaviour and without a prototype property. The language is telling you that a method is meant to be called, not used to build objects.
AdvancedShorthand vs arrow methods in practice

In practice: use shorthand for methods, and never use arrows for methods that need this. Arrows are the right choice for callbacks inside a method (this.items.map((i) => this.format(i))), because there you want the method's this, not a new one.

#Computed property names

Square brackets in an object literal make the key an expression that's evaluated to produce the property name.

Before ES2015 you had to build the object first and then add a dynamic key on a second line (obj[key] = value). Computed keys let you do it inside the literal, which matters most when you're returning a new object in one expression — reducers, state updates, building lookups. The expression in the brackets can be anything: a variable, a template literal, a function call. Its result is then turned into a property key: symbols stay symbols, and every other value is converted to a string.

JavaScript
const field = "email";
const id = 3;

const form = {
  [field]: "a@b.com",          // email: "a@b.com"
  [`item_${id}`]: true,        // item_3: true
  [Symbol.iterator]: function* () {}, // symbol keys must use brackets
};

// Handy for updating state by input name
const update = (state, e) => ({ ...state, [e.target.name]: e.target.value });
What’s happening
  1. [field] evaluates the variable field → "email", so the property is email: "a@b.com". Without brackets, field: … would create a key literally called "field".
  2. The key item_${id} is a template literal; it evaluates to "item_3", giving item_3: true.
  3. [Symbol.iterator] evaluates to the built-in symbol, so the key is that symbol, not a string. A symbol has no spelling you could type as a plain key, so brackets are the only way to use one in a literal. This particular generator yields nothing, so [...form] is [] — but it makes form iterable (see Well-known (built-in) symbols).
  4. update(state, e) is called from an onChange handler. If state is { email: "", password: "" } and the event comes from <input name="password"> with value "hunter2", then e.target.name is "password" and the result is a new object { email: "", password: "hunter2" }. One handler serves every input in the form, because the key comes from the input itself.
  5. The spread ...state copies the old keys first, then the computed key comes later and overwrites the matching one. If you reversed the order ({ [name]: value, ...state }), the old value would win and the update would be lost.
AdvancedObjects as computed keys

Because non-symbol keys are converted to strings, an object used as a computed key does not work the way you might hope:

JavaScript
const a = { id: 1 };
const b = { id: 2 };
const lookup = { [a]: "first", [b]: "second" };

Object.keys(lookup); // ["[object Object]"] — both keys became the same string
lookup[a];           // "second"
What’s happening
  1. [a] converts a to a string with String(a), which is "[object Object]". So the first entry is "[object Object]": "first".
  2. [b] converts to the same string, so the second entry overwrites the first: "[object Object]": "second".
  3. lookup[a] converts a again → "[object Object]" → "second". In fact lookup[{}] also returns "second" — any plain object maps to that one key.
  4. This is the main reason Map exists: a Map keys by identity, so a and b stay separate entries. Use computed keys for strings and symbols, and a Map when the keys are objects.

#The prototype chain

Every object has a hidden link, [[Prototype]], to another object (or null). When you read a property that the object doesn't have, JavaScript walks up this chain until it finds it or hits null. That's prototypal inheritance: objects inherit directly from other objects.

Think of it as asking a series of people: "Do you have greet?" The object asks itself first; if it doesn't have the key, it asks its prototype, then that object's prototype, and so on. The first object that has the key answers. If nobody has it and the chain ends at null, the answer is undefined. Nothing is copied — instances keep a reference to the prototype, so a method added to the prototype later is immediately visible to every existing instance.

This lookup only applies to reading. Writing a property (obj.x = 1) normally creates or updates an own property on obj itself and never touches the prototype — see Shadowing prototype properties.

JavaScript
function Person(name) {
  this.name = name; // own property — one per instance
}
Person.prototype.greet = function () { // shared — one copy for all instances
  console.log("Hello, " + this.name);
};

const alice = new Person("Alice");
alice.greet();                  // found on Person.prototype
alice.toString();               // found on Object.prototype
alice.fly;                      // undefined — reached null

Object.getPrototypeOf(alice) === Person.prototype;      // true
Object.getPrototypeOf(Person.prototype) === Object.prototype; // true
Object.getPrototypeOf(Object.prototype);                // null
What’s happening
  1. new Person("Alice") creates alice with one own property, name: "Alice", and sets alice.[[Prototype]] to Person.prototype. Person.prototype is an ordinary object that already had a constructor property, and now also has greet.
  2. alice.greet(): look on alice — own keys are just name, no greet. Follow the link to Person.prototype — greet found. It's called with this = alice (the object left of the dot, not the object where the method was found), so it logs Hello, Alice.
  3. alice.toString(): alice — no. Person.prototype — no (only constructor and greet). Object.prototype — yes. It returns "[object Object]". Three hops.
  4. alice.fly: alice — no. Person.prototype — no. Object.prototype — no. Its [[Prototype]] is null, so the search stops and the result is undefined. No error is thrown for a missing property; you only get a TypeError if you then try to call it (alice.fly()).
  5. The three getPrototypeOf lines just print the chain link by link: alice → Person.prototype → Object.prototype → null. Arrays work the same way: [] → Array.prototype → Object.prototype → null, which is why [].map and [].hasOwnProperty both work.
AdvancedDiagram: the alice prototype chain
alice name: "Alice" own properties Person.prototype greet() constructor: Person Object.prototype toString() hasOwnProperty() null [[Prototype]] [[Prototype]] alice.toString() → not on alice → not on Person.prototype → found on Object.prototype
Property lookup walks right until it finds the key or reaches null.
AdvancedWhy the prototype design matters

Why this design matters in practice:

  • Memory: a million Person instances share one greet function. Only the per-instance data (name) is stored a million times.
  • Live updates: adding Person.prototype.wave = … later makes alice.wave() work immediately, because alice looks it up at call time. This is also how polyfills add Array.prototype.at to old browsers — and why modifying built-in prototypes in app code is dangerous: you change behaviour for every array everywhere.
  • Classes are built on this: class methods go on ClassName.prototype and extends links one prototype to another. There's no separate class-based mechanism underneath.

#Methods go on .prototype

If you define a method inside the constructor, every instance gets its own copy. Put it on the prototype and all instances share one.

The constructor body runs once per new, so anything it creates — including a function expression — is created again for every instance. The prototype object is created once, when the constructor function is defined, so anything you put there exists exactly once.

JavaScript
function Bad(name) {
  this.name = name;
  this.greet = function () {}; // new function per instance ❌
}
function Good(name) {
  this.name = name;
}
Good.prototype.greet = function () {}; // shared ✅

new Bad("a").greet === new Bad("b").greet;   // false
new Good("a").greet === new Good("b").greet; // true
What’s happening
  1. new Bad("a") runs the constructor body: it sets name and evaluates function () {}, producing a brand-new function object stored as an own property greet. Object.keys(new Bad("a")) is ["name", "greet"].
  2. new Bad("b") runs the body again, producing a second, different function object. Two function objects are never === to each other even with identical source, so the comparison is false.
  3. Good.prototype.greet = … runs once, at definition time. Instances get only name as an own property.
  4. new Good("a").greet isn't found on the instance, so the lookup goes to Good.prototype and returns that one function. new Good("b").greet walks to the same place and gets the same function, so === is true.
  5. The cost of Bad is one extra function object per instance (memory and creation time). For a handful of objects it doesn't matter; for thousands of list items or game entities it does.
AdvancedClass methods vs arrow fields

ES classes do this automatically: methods in a class body go on the prototype.

The trade-off still exists inside classes. A class field holding an arrow function — handleClick = () => { … } — is an own property created per instance, exactly like Bad. People do it on purpose because the arrow captures this, so button.addEventListener("click", this.handleClick) works without bind. That's a fine trade for a few UI components; for objects you create in bulk, prefer a normal method on the prototype. Per-instance functions also have one genuine advantage: they can close over private constructor variables (the closure-based privacy pattern from the Closures topic).

#__proto__ vs prototype

They're easy to mix up:

What it isExists on
fn.prototypeThe object that will become the [[Prototype]] of instances created with new fn()Regular functions and classes (not arrows, shorthand methods, async functions or bound functions)
obj.__proto__A (legacy) getter/setter for the object's own [[Prototype]] linkEvery object that inherits from Object.prototype

The way to keep them apart: prototype is a normal property that only functions have, and it's a blueprint for future instances. It says nothing about the function's own parent. __proto__ reads the actual link of whatever object you call it on. So Dog.prototype is "what Dog's instances will inherit from", while Dog.__proto__ is "what Dog itself inherits from" — and since Dog is a function, that's Function.prototype.

JavaScript
function Dog() {}
const rex = new Dog();

rex.__proto__ === Dog.prototype;        // true
Dog.prototype.constructor === Dog;      // true
Dog.__proto__ === Function.prototype;   // true — Dog is itself a function object
rex.prototype;                          // undefined — instances don't have one
What’s happening
  1. Declaring function Dog() {} creates two objects: the function Dog, and a fresh object Dog.prototype that contains one property, constructor, pointing back to Dog.
  2. new Dog() creates rex and sets its link to Dog.prototype. rex.__proto__ runs the getter inherited from Object.prototype and returns that link → true.
  3. Dog.prototype.constructor === Dog is the back-pointer from step 1. It's also why rex.constructor is Dog: rex has no own constructor, so the lookup finds it on Dog.prototype.
  4. Dog.__proto__ is the link of the function object Dog. All functions are created linked to Function.prototype — that's where call, apply and bind come from.
  5. rex.prototype is undefined: rex is a plain object, not a function, so it has no prototype property, and nothing on its chain has one either. Instances have a link (__proto__), not a blueprint (prototype).
AdvancedModern replacements for __proto__
Advanced__proto__ on null-prototype objects

The getter lives on Object.prototype, so it's missing on objects that don't inherit from it: on Object.create(null), obj.__proto__ is just undefined. Object.getPrototypeOf works on everything, which is one more reason to prefer it. The one place __proto__ is still standard and fine is the { __proto__: base } syntax inside an object literal.

Replacing the prototype after creating an instance
JavaScript
function F() {}
const a = new F();
F.prototype = { x: 1 };
const b = new F();

console.log(a.x, b.x);
console.log(a instanceof F, b instanceof F);
Show answer

Prints undefined 1, then false true.

What’s happening
  1. new F() links a to the object that F.prototype pointed to at that moment — call it the original prototype. a stores a reference to that object, not to the property F.prototype.
  2. F.prototype = { x: 1 } makes the property point to a brand-new object. The original prototype still exists, and a is still linked to it.
  3. new F() now links b to the new object { x: 1 }.
  4. a.x: a → original prototype (only constructor) → Object.prototype → null, so undefined. b.x: b → { x: 1 } → found, 1.
  5. instanceof checks whether the current F.prototype is on the object's chain. It's on b's chain but not on a's, so a instanceof F is false even though a was created by F.
  6. Bonus: b.constructor is Object, not F, because the replacement object has no constructor of its own and the lookup falls through to Object.prototype.constructor. That's why code that replaces a prototype wholesale should add constructor: F back.

#What new actually does

Added

new is not magic — it's four steps you could write yourself, and interviewers like to ask you to. Writing it out is the clearest way to see how a constructor, its prototype property and the resulting instance are connected:

  • Create a new empty object.
  • Link its [[Prototype]] to Constructor.prototype.
  • Run the constructor with this set to the new object, so this.x = … adds own properties.
  • Return the new object — unless the constructor explicitly returned a different object.
JavaScript
function myNew(Constructor, ...args) {
  // 1. Create an empty object linked to Constructor.prototype
  const obj = Object.create(Constructor.prototype);
  // 2. Run the constructor with `this` = the new object
  const result = Constructor.apply(obj, args);
  // 3. If the constructor returned an object, use that; otherwise use obj
  return result !== null && (typeof result === "object" || typeof result === "function") ? result : obj;
}

const a = myNew(Person, "Alice");
a instanceof Person; // true
What’s happening
  1. myNew(Person, "Alice") — Constructor is Person (from the prototype-chain topic) and args is ["Alice"].
  2. Object.create(Person.prototype) does the first two jobs at once: it creates an empty object obj and links it to Person.prototype. Right now obj is {}, but obj.greet already works through the link.
  3. Person.apply(obj, ["Alice"]) calls Person with this = obj. The body runs this.name = "Alice", so obj becomes { name: "Alice" }. Person has no return, so result is undefined.
  4. The return line checks whether result is an object or function (and not null, because typeof null is "object"). undefined isn't, so myNew returns obj.
  5. a instanceof Person is true because Person.prototype is the first link on a's chain. a behaves exactly like new Person("Alice").
  6. If you dropped the return-value check and always returned obj, constructors that deliberately return a different object (some singletons and caching patterns do) would behave differently from the real new.
AdvancedWriting instanceof by hand

instanceof simply checks whether Constructor.prototype appears anywhere in the object's prototype chain.

That means instanceof is a question about links, not about which function created the object. Here's the algorithm written out:

JavaScript
function myInstanceOf(obj, Constructor) {
  if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) return false;
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

myInstanceOf(a, Person); // true
myInstanceOf(a, Object); // true
myInstanceOf(a, Array);  // false
myInstanceOf(5, Number); // false — same as 5 instanceof Number
What’s happening
  1. The guard returns false for primitives straight away, matching the real instanceof. Without it, Object.getPrototypeOf(5) would box 5 and return Number.prototype, and myInstanceOf(5, Number) would wrongly say true.
  2. myInstanceOf(a, Person): proto starts as Person.prototype, which equals Person.prototype on the first comparison → true.
  3. myInstanceOf(a, Object): first proto is Person.prototype — not Object.prototype. Step up: Object.prototype — match → true. Every ordinary object is an instanceof Object for this reason.
  4. myInstanceOf(a, Array): Person.prototype → Object.prototype → null, never equal to Array.prototype, so the loop ends and returns false.
  5. Because it only compares links, instanceof can be fooled: change Person.prototype and old instances stop matching (see the quiz in the previous topic), and arrays from another iframe fail instanceof Array because they have a different Array.prototype. That's why Array.isArray exists.
What does a constructor's return do?
JavaScript
function R1() {
  this.a = 1;
  return { b: 2 };
}
function R2() {
  this.a = 1;
  return 42;
}

console.log(new R1());
console.log(new R2());
Show answer

Prints { b: 2 }, then R2 { a: 1 }.

What’s happening
  1. new R1() creates an object linked to R1.prototype and runs the body, which sets a: 1 on it.
  2. The body then returns { b: 2 } — an object. Step 4 of new says an explicitly returned object replaces the new one, so the result is { b: 2 }. The object with a: 1 is thrown away, and the result isn't even linked to R1.prototype (new R1() instanceof R1 is false).
  3. new R2() sets a: 1 the same way, then returns 42. A primitive return value is ignored by new, so you get the new object, which Node prints as R2 { a: 1 } (the R2 prefix comes from its constructor).
  4. This is exactly the typeof result === "object" check in myNew above.

#Inheritance between constructor functions

Before class, inheritance took two steps: call the parent constructor for the own properties, and link the prototypes for the shared methods.

These are two separate problems, which is why there are two steps. Own properties (name, breed) are created by running constructor code against the new object, so the child must run the parent's constructor on its this. Shared methods (eat, bark) live on prototype objects, so the child's prototype must link to the parent's prototype to make the chain instance → Dog.prototype → Animal.prototype → Object.prototype. class … extends does both for you, but it's the same structure underneath.

JavaScript
function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function () {
  return `${this.name} is eating`;
};

function Dog(name, breed) {
  Animal.call(this, name); // 1. inherit own properties ("super(name)")
  this.breed = breed;
}

// 2. inherit methods
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // repair the constructor pointer

Dog.prototype.bark = function () {
  return "Woof!";
};

const d = new Dog("Rex", "Lab");
d.eat();               // "Rex is eating"
d instanceof Animal;   // true
What’s happening
  1. Dog.prototype = Object.create(Animal.prototype) throws away the default Dog.prototype and replaces it with a new empty object linked to Animal.prototype. That one line creates the chain Dog.prototype → Animal.prototype.
  2. The replacement object has no constructor of its own, so Dog.prototype.constructor would be found on Animal.prototype → Animal. The repair line puts Dog back, so d.constructor === Dog and code like new d.constructor("Max") creates a Dog, not an Animal.
  3. bark is added after the replacement. If you added it before, it would go onto the old default prototype that was just discarded, and d.bark would be undefined.
  4. new Dog("Rex", "Lab"): new creates an object linked to Dog.prototype and runs Dog with this = that object. Animal.call(this, "Rex") runs the Animal body against the same object, adding name: "Rex"; then this.breed = "Lab". d's own properties are { name: "Rex", breed: "Lab" }.
  5. d.eat(): d — no eat. Dog.prototype — only constructor and bark. Animal.prototype — found. Called with this = d, so it returns "Rex is eating".
  6. d instanceof Animal walks d → Dog.prototype → Animal.prototype and finds Animal.prototype, so true. d instanceof Dog is also true.
AdvancedPitfall: sharing the parent prototype
AdvancedRefinements: constructor and setPrototypeOf

Two refinements you'll see in better-written older code:

  • The repaired constructor is enumerable (it was created by plain assignment), so for (const k in d) lists name, breed, constructor, bark, eat. A faithful repair uses Object.defineProperty(Dog.prototype, "constructor", { value: Dog, writable: true, configurable: true }), which defaults to non-enumerable like the original.
  • Object.setPrototypeOf(Dog.prototype, Animal.prototype) re-links the existing default prototype instead of replacing it, so constructor never breaks and the order of adding methods no longer matters.

The class version is the same two steps with the plumbing hidden:

JavaScript
class Animal {
  constructor(name) { this.name = name; }
  eat() { return `${this.name} is eating`; }
}
class Dog extends Animal {          // links Dog.prototype → Animal.prototype
  constructor(name, breed) {
    super(name);                    // runs Animal's constructor on this object
    this.breed = breed;
  }
  bark() { return "Woof!"; }
}
What’s happening
  1. class Dog extends Animal creates Dog.prototype already linked to Animal.prototype — the Object.create step — with a correct, non-enumerable constructor.
  2. super(name) replaces Animal.call(this, name): it runs Animal's constructor so name is set on the new object. In a derived class you must call it before touching this, or you get a ReferenceError.
  3. eat and bark go on the prototypes as non-enumerable methods, so for (const k in new Dog("Rex", "Lab")) lists only name and breed — none of the enumerable-constructor noise from the manual version.
  4. extends also links the constructors themselves (Object.getPrototypeOf(Dog) === Animal), which is what makes static methods inheritable (next topic).

#Static methods on constructor functions

A "static" method lives on the constructor itself, not on instances.

A function is an object, so you can hang properties on it like any other object. That's all a static method is: a property of the constructor function. Instances can't see it because their chain goes through User.prototype, not through User. Statics are for things that belong to the type rather than to one object: factory methods (fromJSON, Array.from), validators (Number.isInteger), and constants.

JavaScript
function User(name) {
  this.name = name;
}
User.fromJSON = function (json) {   // static
  return new User(JSON.parse(json).name);
};
User.prototype.hello = function () { // instance
  return `Hi ${this.name}`;
};

const u = User.fromJSON('{"name":"Rohit"}');
u.hello();     // "Hi Rohit"
u.fromJSON;    // undefined — statics aren't inherited by instances
What’s happening
  1. User.fromJSON = … adds a property to the function object User. User.prototype.hello = … adds a property to the separate prototype object. Two different objects, two different audiences.
  2. User.fromJSON('{"name":"Rohit"}') parses the string to { name: "Rohit" }, reads .name, and calls new User("Rohit"). u is { name: "Rohit" } linked to User.prototype.
  3. u.hello(): not on u, found on User.prototype, called with this = u → "Hi Rohit".
  4. u.fromJSON: u → User.prototype → Object.prototype → null. The function object User is never on that chain, so undefined. (You can still reach it from an instance with u.constructor.fromJSON, because constructor points back to User.)
AdvancedStatics in classes vs constructors

The class equivalent is static fromJSON(json) { … }.

One difference worth knowing: with classes, statics are inherited by subclasses. class Admin extends User also sets Object.getPrototypeOf(Admin) === User, so Admin.fromJSON is found by walking from Admin to User. Inside a static method, this is the constructor it was called on, which is why static create() { return new this(); } called as Admin.create() builds an Admin. With plain constructor functions you only get that if you link them yourself with Object.setPrototypeOf(Admin, User).

#Shadowing prototype properties

Assigning a property on an instance creates an own property that hides (shadows) the prototype's one. The prototype is untouched.

This is the read/write asymmetry of the prototype chain. Reading walks the chain; writing doesn't. obj.x = value doesn't go looking for an existing x up the chain to update — it puts x directly on obj. From then on, reads of obj.x stop at the own property and never reach the prototype's value. It's like writing your own phone number on a sticky note over the company directory: you see yours, everyone else still sees the directory.

JavaScript
function Car() {}
Car.prototype.wheels = 4;

const trike = new Car();
trike.wheels = 3;       // own property — shadows the prototype
const sedan = new Car();

trike.wheels;           // 3
sedan.wheels;           // 4
delete trike.wheels;
trike.wheels;           // 4 again — the prototype shows through
What’s happening
  1. Car.prototype.wheels = 4 puts one wheels property on the shared prototype. Every Car instance can read it.
  2. trike.wheels = 3 is a write, so it creates an own property on trike: trike is now { wheels: 3 }. Car.prototype.wheels is still 4.
  3. trike.wheels → found on trike itself → 3; the lookup stops there. sedan.wheels → sedan has no own wheels → Car.prototype → 4.
  4. delete trike.wheels removes the own property only. delete never reaches up the chain, so the prototype's wheels survives.
  5. trike.wheels now finds nothing on trike, walks to Car.prototype, and reads 4 again.
AdvancedWrites that do consult the chain

There are two exceptions where a write does consult the chain, and both are easy to check:

JavaScript
const proto = {};
Object.defineProperty(proto, "x", { value: 1, writable: false });
const child = Object.create(proto);
child.x = 2;   // silently ignored (TypeError in strict mode)
child.x;       // 1 — no own property was created

const proto2 = {
  set y(v) { console.log("setter ran with", v); },
};
const c2 = Object.create(proto2);
c2.y = 5;                 // logs "setter ran with 5"
Object.hasOwn(c2, "y");   // false
What’s happening
  1. Before creating a new own property, an assignment checks whether the key exists up the chain as a non-writable data property. proto.x is non-writable, so child.x = 2 is refused — silently in sloppy mode, with a TypeError in strict mode (classes and ES modules are strict).
  2. So child never gets an own x, and child.x still reads 1 from proto.
  3. If the key exists up the chain as a setter, the assignment calls that setter (with this = c2) instead of creating a property. That's why c2.y = 5 logs and leaves c2 with no own y.
  4. This setter rule is what makes class accessors work: get/set defined in a class body live on the prototype, and instance.prop = value calls them.
AdvancedPitfall: arrays on the prototype

#Own vs inherited properties: Object.hasOwn vs in

Because reads walk the prototype chain, "does this object have x?" has two meanings: does it have x itself (an own property), or can it reach x somewhere on its chain? Different checks answer different questions, and mixing them up is a common source of bugs — especially with user-supplied keys.

CheckOwn propsInherited props
Object.hasOwn(obj, key)✅❌
obj.hasOwnProperty(key) (older)✅❌
key in obj✅✅
obj[key] !== undefinedUnreliable — the value might be undefined
JavaScript
const obj = { a: 1 };
Object.prototype.c = 3; // inherited (never do this in real code!)

Object.hasOwn(obj, "a"); // true
Object.hasOwn(obj, "c"); // false
"c" in obj;              // true — `in` walks the chain
"toString" in obj;       // true
What’s happening
  1. obj has one own property, a. Adding c to Object.prototype makes c reachable from every ordinary object in the program — that's why it's flagged as "never do this".
  2. Object.hasOwn(obj, "a") only looks at obj's own keys → true.
  3. Object.hasOwn(obj, "c") → obj has no own c → false. It never looks at the prototype.
  4. "c" in obj asks "can obj reach c?" → walks to Object.prototype and finds it → true.
  5. "toString" in obj is true for the same reason. This is why if (key in obj) is wrong for a dictionary: a user key like "toString" or "constructor" appears to exist even when it was never stored.
  6. obj[key] !== undefined fails both ways: it finds inherited values (obj.toString isn't undefined), and it misses own keys whose value is undefined ({ x: undefined }).

#Iterating object properties with for...in

for...in loops over enumerable string keys, including inherited ones. Filter with Object.hasOwn if you only want the object's own keys.

for...in is one of the oldest features in the language and it reflects the prototype model directly: it walks the same chain a property read does and reports every enumerable string key it meets on the way — own keys first, then the prototype's, and so on up. Built-in methods like toString don't show up because they're defined as non-enumerable (see Property descriptors); anything added by plain assignment does.

JavaScript
const obj = { a: 1, b: 2, c: 3 };

for (const key in obj) {
  if (Object.hasOwn(obj, key)) {   // note: Object.hasOwn, not obj.hasOwn
    console.log(`${key}: ${obj[key]}`);
  }
}
What’s happening
  1. for...in produces the keys "a", "b", "c" from obj, then moves to Object.prototype. In a clean program it finds nothing enumerable there, so the loop runs three times.
  2. On each pass, Object.hasOwn(obj, key) confirms the key belongs to obj, and the body logs a: 1, b: 2, c: 3.
  3. The guard matters when something enumerable is inherited. If a library (or the polluting line in the previous topic) had run Object.prototype.c = 3, the unguarded loop would visit a and c for { a: 1 } — and with the guard it skips the inherited c.
  4. obj[key] must use brackets because key is a variable holding a string; obj.key would look for a property literally named "key".
AdvancedInherited keys in for...in

Here's the inherited case directly:

JavaScript
const base = { inherited: true };
const child = Object.create(base);
child.own = 1;

for (const key in child) console.log(key); // "own", then "inherited"
Object.keys(child);                        // ["own"]
What’s happening
  1. child has one own property, own, and is linked to base, which has inherited.
  2. for...in reports child's own key first, then walks up to base and reports inherited. Both are enumerable because both were created by assignment or a literal.
  3. Object.keys stops at the object itself and returns only ["own"]. This is the difference that makes Object.keys the safer default.
AdvancedCorrecting my notes: Object.hasOwn
AdvancedObject.entries instead of for...in

Usually it's simpler to skip for...in and use Object.keys / Object.entries, which only return own enumerable keys:

JavaScript
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
What’s happening
  1. Object.entries(obj) builds an array of [key, value] pairs from the own enumerable string keys: [["a", 1], ["b", 2], ["c", 3]].
  2. for...of iterates that array, and the destructuring [key, value] unpacks each pair, so the body logs a 1, b 2, c 3.
  3. No hasOwn guard is needed, and you get the value without a second lookup. Since you also get a real array, you can map, filter or sort it first.

#Object.keys, values, entries and fromEntries

Objects don't have map or filter — those live on Array.prototype. These four static methods are the bridge: keys, values and entries turn an object's own enumerable string-keyed properties into arrays, you use the array methods you already know, and fromEntries turns the result back into an object. Think of entries and fromEntries as a round trip: object → list of pairs → object.

JavaScript
const prices = { apple: 1, banana: 0.5, cherry: 3 };

Object.keys(prices);    // ["apple", "banana", "cherry"]
Object.values(prices);  // [1, 0.5, 3]
Object.entries(prices); // [["apple", 1], ["banana", 0.5], ["cherry", 3]]

// fromEntries is the reverse — great for transforming objects
const doubled = Object.fromEntries(
  Object.entries(prices).map(([k, v]) => [k, v * 2]),
);
// { apple: 2, banana: 1, cherry: 6 }

const cheap = Object.fromEntries(Object.entries(prices).filter(([, v]) => v < 2));

// Also turns a Map or URLSearchParams into a plain object
Object.fromEntries(new URLSearchParams("a=1&b=2")); // { a: "1", b: "2" }
What’s happening
  1. keys, values and entries all read the same own keys in the same order, so index 0 of each lines up: "apple", 1, ["apple", 1].
  2. doubled: entries gives three pairs; .map(([k, v]) => [k, v * 2]) destructures each pair and returns a new pair — ["apple", 2], ["banana", 1], ["cherry", 6]. fromEntries assembles them into { apple: 2, banana: 1, cherry: 6 }. prices is unchanged.
  3. cheap: .filter(([, v]) => v < 2) skips the key with a hole in the destructuring pattern and keeps pairs whose value is below 2 → ["apple", 1], ["banana", 0.5]. fromEntries gives { apple: 1, banana: 0.5 }.
  4. fromEntries accepts any iterable of pairs, not just arrays. Iterating a URLSearchParams yields ["a", "1"] and ["b", "2"], so you get { a: "1", b: "2" }. The values stay strings — query strings have no types. If a key repeats (a=1&b=2&a=3), later pairs overwrite earlier ones and you get { a: "3", b: "2" }.
  5. A Map is also an iterable of [key, value] pairs, so Object.fromEntries(map) converts it directly (with keys turned into strings), and new Map(Object.entries(obj)) goes the other way.
AdvancedEdge case: integer keys sort first
AdvancedPractical uses of entries and fromEntries

Practical uses: transforming API responses (rename or drop keys), building a lookup from a list (Object.fromEntries(users.map((u) => [u.id, u]))), and counting with Object.entries(counts).sort(([, a], [, b]) => b - a). Remember they all skip inherited, non-enumerable and symbol keys — use Reflect.ownKeys when you need every own key.

#Null-prototype objects

Object.create(null) makes an object with no prototype at all — no toString, no hasOwnProperty, nothing inherited.

A normal {} isn't really empty: through its link to Object.prototype it can reach constructor, toString, valueOf, hasOwnProperty, __proto__ and more. That's harmless when you choose the keys, but when the keys come from outside — a word count over user text, HTTP headers, a cache keyed by user IDs — any of those names can collide with an inherited property. A null-prototype object's chain is just dict → null, so the only keys it has are the ones you put in.

JavaScript
const dict = Object.create(null);
dict.apple = 1;

"toString" in dict;      // false — a truly empty dictionary
dict.__proto__;          // undefined — even this key is safe to use
Object.hasOwn(dict, "apple"); // true (dict.hasOwnProperty would be a TypeError)
What’s happening
  1. Object.create(null) creates an object whose [[Prototype]] is null. dict.apple = 1 adds one own property.
  2. "toString" in dict: in looks at dict (only apple), then at its prototype — which is null — so the search ends → false. With {} it would be true.
  3. dict.__proto__ is undefined because the __proto__ getter lives on Object.prototype, which dict can't reach. On a null-prototype object, __proto__ is an ordinary key: dict.__proto__ = 5 just stores 5 as an own property and doesn't change the prototype.
  4. Object.hasOwn works because it doesn't depend on the object's chain. dict.hasOwnProperty("apple") would throw, since there's no hasOwnProperty to find.
AdvancedWhy use a null-prototype object

Why use one? As a pure lookup table where keys come from users (e.g. "constructor" or "__proto__") and must never collide with inherited properties. It's also one of the defences against prototype pollution. In most modern code a Map is the better choice.

The trade-offs: a null-prototype object can't be printed with string concatenation ("" + dict throws, because there's no toString), and libraries that call obj.hasOwnProperty on it will break. A Map avoids all of this, accepts any key type, and keeps insertion order — reach for Object.create(null) mainly when you need a plain object (for JSON output, or because an API expects one).

#Property descriptors

Added

Every property has hidden attributes. Inspect them with Object.getOwnPropertyDescriptor and set them with Object.defineProperty.

A property is more than a key and a value. Each one also carries flags that say whether the value can change (writable), whether it shows up when you list keys (enumerable), and whether the property itself can be deleted or reconfigured (configurable). Normally you never see these because ordinary assignment sets them all to true. They're how the language hides built-in methods from loops, how Object.freeze works, and how libraries define read-only or hidden properties.

JavaScript
const user = { name: "Rohit" };
Object.getOwnPropertyDescriptor(user, "name");
// { value: "Rohit", writable: true, enumerable: true, configurable: true }

Object.defineProperty(user, "id", {
  value: 42,
  writable: false,     // can't reassign
  enumerable: false,   // hidden from keys / for...in / JSON.stringify
  configurable: false, // can't delete or redefine
});

user.id = 7;           // silently ignored (TypeError in strict mode)
Object.keys(user);     // ["name"]
JSON.stringify(user);  // '{"name":"Rohit"}'
What’s happening
  1. name was created by a literal, so its descriptor has all three flags true, plus its value.
  2. defineProperty adds id with value 42 and all three flags false. (Listing them explicitly is for readability — they'd default to false anyway, as the table below shows.)
  3. user.id = 7: id is not writable, so the assignment fails. In sloppy mode it's silently ignored and user.id is still 42; in strict mode it throws TypeError: Cannot assign to read only property 'id'.
  4. Object.keys(user) returns only ["name"] because id is not enumerable. JSON.stringify, for...in and object spread ({ ...user }) skip it for the same reason — but user.id still reads 42 and Object.hasOwn(user, "id") is true. Hidden isn't the same as absent.
  5. Because id is not configurable, delete user.id returns false (or throws in strict mode), and a second Object.defineProperty(user, "id", { value: 1 }) throws TypeError: Cannot redefine property: id.
AdvancedDescriptor attributes and their defaults
AttributeMeaningDefault with defineProperty
writableValue can be changedfalse
enumerableShows up in loops, Object.keys, spread, JSONfalse
configurableCan be deleted, or its attributes changedfalse
get / setAccessor functions (instead of value/writable)—
AdvancedAssignment vs defineProperty defaults
AdvancedData vs accessor descriptors

A descriptor comes in one of two shapes. A data descriptor has value and writable; an accessor descriptor has get and/or set instead. You can't mix them — passing both value and get throws. For example, Object.getOwnPropertyDescriptor({ get x() { return 1; } }, "x") is { get: [Function: get x], set: undefined, enumerable: true, configurable: true }.

The object-level helpers are built from these flags: Object.freeze(obj) sets every own property to writable: false, configurable: false and stops new properties being added; Object.seal(obj) sets configurable: false but leaves values writable. Both are shallow — nested objects stay mutable (see the copying chapter).

#Getters and setters

Accessors look like properties but run a function when read or written — useful for computed values and validation.

The caller writes account.balance or account.balance = 50 as if it were a plain field, but behind it is a function. That lets you change how a value is stored or computed without changing the code that uses it, add validation at the point of assignment, or expose a derived value that's always up to date. A getter with no setter is effectively read-only.

JavaScript
const account = {
  _balance: 0,
  get balance() {
    return `$${this._balance.toFixed(2)}`;
  },
  set balance(value) {
    if (value < 0) throw new RangeError("Balance can't be negative");
    this._balance = value;
  },
};

account.balance = 50;    // calls the setter
account.balance;         // "$50.00" — calls the getter (no parentheses)
account.balance = -1;    // RangeError

class Temperature {
  #celsius = 0;
  get fahrenheit() {
    return this.#celsius * 1.8 + 32;
  }
  set fahrenheit(f) {
    this.#celsius = (f - 32) / 1.8;
  }
}
What’s happening
  1. account has two own properties: a data property _balance: 0 and an accessor property balance with a get and a set function. There's no stored value called balance at all.
  2. account.balance = 50 calls the setter with value = 50. The check 50 < 0 is false, so it runs this._balance = 50. The data lives in _balance; the setter must write to a different key — writing this.balance = value inside the setter would call the setter again, forever (stack overflow).
  3. account.balance calls the getter, which formats 50 with toFixed(2) → "$50.00". The caller can't tell a function ran.
  4. account.balance = -1 calls the setter, which throws RangeError: Balance can't be negative before assigning, so _balance stays 50.
  5. The underscore is only a convention: account._balance = -999 bypasses the setter entirely, and Object.keys(account) is ["_balance", "balance"]. Temperature fixes that with a #celsius private field that outside code can't touch at all.
  6. new Temperature().fahrenheit computes 0 * 1.8 + 32 → 32. Setting fahrenheit = 212 stores (212 - 32) / 1.8 → 100 in #celsius; reading it again gives 212. Only Celsius is stored, so the two scales can never disagree.
AdvancedWhere getters and setters show up

Where accessors show up: derived values (get fullName()), validation on assignment, lazy computation, and backwards-compatible APIs (turning a field into a computed property without breaking callers). Two things to know: class accessors live on the prototype, so Object.keys(new Temperature()) is [] and JSON.stringify gives "{}"; while accessors defined in an object literal are own properties, so JSON.stringify(account) calls the getter and includes "balance":"$50.00". And keep getters cheap and side-effect free — callers assume reading a property is fast and harmless.

#Symbols

A Symbol is a primitive that's guaranteed unique. Two symbols with the same description are still different.

Symbols were added in ES2015 to solve a naming problem: how do you add a property to an object without any chance of colliding with keys someone else chose, now or in future? A string key like "id" might already be in use. A symbol is a key that nobody else can create by accident, because the only way to get it is to have a reference to it. The description you pass ("id") is just a label for debugging; it has no effect on identity.

JavaScript
const id1 = Symbol("id");
const id2 = Symbol("id");
id1 === id2; // false

const mySymbol = Symbol("privateProperty");
const obj = { name: "John", [mySymbol]: 42 };

Object.keys(obj);                     // ["name"] — symbols are skipped
JSON.stringify(obj);                  // '{"name":"John"}'
obj[mySymbol];                        // 42
Object.getOwnPropertySymbols(obj);    // [Symbol(privateProperty)]
Reflect.ownKeys(obj);                 // ["name", Symbol(privateProperty)]
What’s happening
  1. Each Symbol("id") call creates a new unique value. Same description, different symbols → false. id1.description is "id", and that's all the description is for.
  2. [mySymbol]: 42 uses a computed key to store a property keyed by the symbol itself — not by the string "privateProperty". obj.privateProperty would be undefined.
  3. Object.keys and JSON.stringify only deal with string keys, so they skip the symbol and see just name. for...in skips it too.
  4. obj[mySymbol] works because you hold the symbol. Code without a reference to mySymbol can't spell this key.
  5. But it can still discover it: Object.getOwnPropertySymbols(obj) returns the symbol keys, and Reflect.ownKeys(obj) returns every own key, strings and symbols together.
AdvancedHidden symbol keys and global symbols

Symbol properties are skipped by for...in, Object.keys and JSON, which makes them good for metadata and "internal" state that won't collide with normal keys. They are not truly private, though — getOwnPropertySymbols reveals them. For real privacy use #private fields.

Correcting my notes: they said symbol properties are "not enumerable". They're created enumerable like any other property; those APIs skip them because they only list string keys. The difference shows up with copying: object spread and Object.assign copy enumerable own symbol properties, so { ...obj } keeps [mySymbol]: 42.

Global symbols: Symbol.for("app.id") returns the same symbol every time, across files and even iframes. Symbol.keyFor(sym) gives back the key.

JavaScript
Symbol.for("app.id") === Symbol.for("app.id"); // true
What’s happening
  1. The first Symbol.for("app.id") looks up "app.id" in a runtime-wide global symbol registry, finds nothing, creates a symbol and stores it under that key.
  2. The second call finds the stored symbol and returns the same one, so === is true.
  3. Symbol.keyFor(Symbol.for("app.id")) returns "app.id"; for a normal Symbol("id") it returns undefined, because that symbol was never registered.
  4. Use the registry when separately loaded code (two bundles, a library and a plugin) must agree on the same key. Namespace the string, because anyone who knows it can get the symbol — which makes registered symbols less collision-proof than plain ones.

#Well-known (built-in) symbols

Advanced

JavaScript uses special symbols as hooks that let your objects plug into language features.

When the engine needs to do something generic with your object — loop over it, convert it to a primitive, decide what instanceof means — it looks up a property keyed by one of these predefined symbols. If the property exists, the engine calls it; otherwise it uses the default behaviour. They're symbols rather than strings precisely so they can never collide with ordinary property names in existing code: adding Symbol.iterator to the language couldn't break anyone who already had an iterator key.

SymbolLets you customise
Symbol.iteratorfor...of, spread, destructuring
Symbol.asyncIteratorfor await...of
Symbol.toPrimitiveConversion to number/string (+obj, template literals)
Symbol.toStringTagThe [object X] in Object.prototype.toString
Symbol.hasInstanceinstanceof
JavaScript
const money = {
  amount: 42,
  currency: "CAD",
  [Symbol.toPrimitive](hint) {
    return hint === "number" ? this.amount : `${this.amount} ${this.currency}`;
  },
  get [Symbol.toStringTag]() {
    return "Money";
  },
};

+money;                               // 42
`${money}`;                           // "42 CAD"
Object.prototype.toString.call(money); // "[object Money]"
What’s happening
  1. +money needs a number, so the engine looks for money[Symbol.toPrimitive], finds it, and calls it with hint = "number". The method returns this.amount → 42. The same happens for money * 2 → 84.
  2. A template literal such as ${money} needs a string, so the hint is "string". That isn't "number", so the method returns "42 CAD". String(money) behaves the same.
  3. There's a third hint, "default", used when the operator could go either way — money + "" and money == "42 CAD". Here it falls into the string branch, so money + "" is "42 CAD".
  4. Without Symbol.toPrimitive, the engine would fall back to valueOf and toString — and the inherited toString gives "[object Object]".
  5. Object.prototype.toString.call(money) builds "[object " + tag + "]", reading the tag from money[Symbol.toStringTag]. The getter returns "Money", so the result is "[object Money]". That's how built-ins report "[object Map]" or "[object Promise]", and why this call is a common type-check trick.

Symbol.iterator is covered in detail in Iterators and the iteration protocol.

In real code, Symbol.iterator is the one you'll write most often (custom collections, generators); Symbol.toPrimitive is useful for value types like money, dates or vectors; and Symbol.toStringTag is mostly for libraries that want readable debug output. Symbol.hasInstance exists, but overriding instanceof tends to confuse readers — use it rarely.

#Proxy and Reflect

A Proxy wraps an object and lets you intercept operations on it — reads, writes, deletes, in, function calls — through traps. Reflect has a matching method for every trap, which you use to perform the default behaviour.

Getters and setters let you intercept one named property you know in advance. A Proxy intercepts the operation itself, for any key, including keys that don't exist yet. You create new Proxy(target, handler); the proxy is a separate object that stands in front of target. Every time code reads, writes, deletes or checks a key on the proxy, the engine looks in handler for a trap with the matching name (get, set, deleteProperty, has, …). If there's a trap, it runs; if not, the operation passes straight through to target. Think of it as a receptionist: callers talk to the receptionist, who can answer, refuse, log, or pass the request to the real person.

Reflect exists so a trap can say "now do whatever would normally happen" without reimplementing it. Reflect.get(obj, prop, receiver) is exactly the default get behaviour, including getters and the prototype chain.

JavaScript
const target = { name: "Rohit" };

const logged = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log(`read ${String(prop)}`);
    return Reflect.get(obj, prop, receiver);
  },
});

logged.name; // logs "read name", returns "Rohit"
What’s happening
  1. new Proxy(target, handler) returns a new object logged. It is not target (logged === target is false), but it has no data of its own — everything is forwarded to target.
  2. logged.name is a property read, so the engine calls the get trap with obj = target, prop = "name" and receiver = logged (the object the read was performed on).
  3. The trap logs read name. String(prop) is there because prop can be a symbol (for example when something checks logged[Symbol.iterator]), and putting a symbol directly into a template literal throws a TypeError.
  4. Reflect.get(obj, prop, receiver) performs the normal read on target → "Rohit", which the trap returns as the result of logged.name.
  5. Passing receiver matters when target has a getter: the getter then runs with this = logged, so property reads inside the getter also go through the proxy and get logged. Writing obj[prop] instead would run the getter with this = target and bypass the proxy.
AdvancedA validating Proxy set trap

Validation with a Proxy

JavaScript
function withValidation(obj) {
  return new Proxy(obj, {
    set(target, prop, value) {
      if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
        throw new TypeError("age must be a non-negative integer");
      }
      return Reflect.set(target, prop, value); // must return true for success
    },
  });
}

const person = withValidation({});
person.age = 30;    // ok
person.age = -5;    // TypeError
person.age = "30";  // TypeError
What’s happening
  1. withValidation({}) wraps an empty object and returns the proxy. All writes to person now go through the set trap first.
  2. person.age = 30: the trap gets prop = "age", value = 30. Number.isInteger(30) is true and 30 < 0 is false, so the check passes. Reflect.set stores age: 30 on the target and returns true.
  3. person.age = -5: it's an integer but negative, so the trap throws and Reflect.set never runs. The target still has age: 30.
  4. person.age = "30": Number.isInteger("30") is false — it doesn't convert strings — so the trap throws. This catches the classic "form value is a string" bug at the point of assignment, not later when some arithmetic gives "301".
  5. Any other key (person.name = "Rohit") skips the if and goes straight to Reflect.set, so the proxy validates only what you asked it to.
  6. The trap must return true to report success. If it returned false, the write would be ignored in sloppy mode and throw TypeError: 'set' on proxy: trap returned falsish in strict mode. Returning Reflect.set(...) passes the real result through.

Other uses: default values for missing keys, negative array indexes (arr[-1]), read-only views, change tracking. Vue 3's reactivity system and libraries like Immer and MobX are built on Proxies.

The change-tracking use is the important one to be able to explain: Vue wraps your state in a proxy whose get trap records "this component read count" and whose set trap says "count changed, re-render whoever read it". You write plain state.count++, and the proxy turns it into a notification. Immer does the reverse trick: it gives your function a proxy draft, records the writes you make, and builds a new immutable object from them.

#Avoiding prototype pollution

Prototype pollution is a security bug where attacker-controlled keys like __proto__, constructor or prototype end up modifying Object.prototype — which then affects every object in the app.

It follows directly from the rest of this chapter. Almost every object links to the one shared Object.prototype. Writing obj.__proto__.x = … (or obj.constructor.prototype.x = …) doesn't write to obj — it reaches through the link and writes to that shared object. Normally no code does that on purpose. The bug happens when generic code — a deep merge, a "set by path" helper, a query-string parser — takes keys from user input and uses them as property names, so the attacker chooses the path. Once Object.prototype.isAdmin is true, every if (user.isAdmin) check in the process passes for objects that never had the key.

JavaScript
// A naive deep merge
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object" && source[key] !== null) {
      target[key] ??= {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);

({}).isAdmin; // true 😱 — every object is now "admin"
What’s happening
  1. JSON.parse treats "__proto__" as an ordinary key, so payload gets an own property literally named "__proto__" whose value is { isAdmin: true }. (An object literal written in code would set the prototype instead — the JSON route is what makes this attack possible.)
  2. merge({}, payload) loops over payload's keys and gets key = "__proto__". source["__proto__"] reads payload's own property → { isAdmin: true }, an object, so it takes the recursive branch.
  3. target["__proto__"] ??= {} reads target.__proto__ — and target is a normal {}, so this runs the inherited getter and returns Object.prototype. That isn't nullish, so nothing is assigned.
  4. merge(target["__proto__"], …) therefore recurses with target = Object.prototype. Inside, key = "isAdmin", the value is true (not an object), so it runs Object.prototype.isAdmin = true.
  5. Now ({}).isAdmin looks on the empty object, doesn't find isAdmin, walks to Object.prototype and finds true. Every ordinary object in the process inherits it — sessions, config objects, request bodies.
  6. {"constructor": {"prototype": {"isAdmin": true}}} works too: target.constructor is inherited Object, so the merge recurses into Object, then into Object.prototype, and writes isAdmin there. Blocking only __proto__ isn't enough.
AdvancedA safe merge that blocks pollution

A safe version skips the dangerous keys, only iterates own keys, and only recurses into objects the target really owns:

JavaScript
const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (BLOCKED.has(key)) continue;
    const value = source[key];
    if (typeof value === "object" && value !== null) {
      if (!Object.hasOwn(target, key)) target[key] = {};
      safeMerge(target[key], value);
    } else {
      target[key] = value;
    }
  }
  return target;
}

const input = JSON.parse('{"__proto__": {"isAdmin": true}, "theme": {"dark": true}}');
safeMerge({}, input); // { theme: { dark: true } }
({}).isAdmin;         // undefined
What’s happening
  1. Object.keys(source) gives ["__proto__", "theme"] — own keys only, so nothing inherited is ever merged.
  2. "__proto__" is in BLOCKED, so continue skips it before any read or write happens.
  3. "theme": the value is an object. Object.hasOwn(target, "theme") is false, so a fresh {} is created as an own property, then the recursion copies dark: true into it.
  4. The Object.hasOwn check (instead of ??=) means the merge never recurses into something the target only inherits. Even a key the blocklist missed couldn't lead the merge up the prototype chain.
  5. Result: { theme: { dark: true } }, and Object.prototype is untouched, so ({}).isAdmin is undefined.
AdvancedDefences against prototype pollution

How to defend:

  • Skip dangerous keys when merging or setting by path: if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
  • Use Map for user-keyed data, or Object.create(null) objects.
  • Check with Object.hasOwn rather than in or a truthy check.
  • Validate input with a schema (zod, Joi, JSON Schema) before merging it.
  • As a last resort, Object.freeze(Object.prototype) at startup (can break old libraries).

Why each of these works: a Map stores keys as data, so "__proto__" is just a string entry and never touches a prototype. A null-prototype object has no __proto__ getter, so writing that key creates an ordinary own property. Object.hasOwn(user, "isAdmin") is still false after pollution, whereas user.isAdmin and "isAdmin" in user are true — so own-property checks keep authorisation logic correct even if pollution happens somewhere else. A schema rejects unexpected keys before they reach any merge. Freezing Object.prototype makes the final write fail (silently, or with a TypeError in strict mode). In Node you can also start the process with --disable-proto=delete, which removes the __proto__ accessor entirely. Old versions of lodash's merge/set and jQuery's $.extend(true, …) had exactly this bug, so keep dependencies patched.

Built from Rohit’s “Javascript/HTML interview” study doc. Examples target modern browsers and Node 20+.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.