Skip to main content

Objects & Prototypes

JavaScript is an object-oriented language, but it uses prototypal inheritance instead of classical inheritance (like Java or C++). Understanding objects and prototypes is essential for mastering JavaScript. In most languages you learn, objects are created from classes — you define a blueprint, then stamp out instances. JavaScript flips this on its head. Here, objects inherit directly from other objects. Think of it like learning a skill from a mentor rather than reading a textbook: there is no abstract blueprint, just a real existing object that a new object can look to for guidance. When the new object cannot find a property on itself, it asks its mentor (prototype). If the mentor does not have it either, the mentor asks its mentor, all the way up the chain. This is prototypal inheritance, and once you internalize it, the entire language makes more sense.

1. Objects

An object is a collection of key-value pairs. Keys are strings (or Symbols), values can be anything.

Creating Objects

Computed Property Names (ES6)

Shorthand Properties (ES6)


2. The this Keyword

this is one of the most confusing parts of JavaScript. Its value depends on how a function is called, not where it is defined. Here is the mental model that makes this predictable: look at the call site, not the function definition. When you see obj.fn(), the dot before fn tells you this is obj. When you see fn() with no dot, there is no object, so this falls back to undefined (in strict mode) or window (in sloppy mode). Every time you are confused about this, find the call site and ask: “What is to the left of the dot?”

Rules for this (in priority order)

Examples

Arrow Functions and this

Arrow functions do not have their own this. They capture this from the enclosing scope at the time they are defined, not called. This is what makes them ideal for callbacks inside methods.
Do not use arrow functions as object methods. Since arrow functions capture this from the enclosing scope, this inside an arrow method points to whatever this was outside the object literal — usually window or undefined. Use regular method shorthand (greet() {}) for object methods, and arrow functions for callbacks inside those methods.

Binding this

When you need to control this explicitly, JavaScript gives you three tools. Think of them as three ways to hand a function its “context badge” — telling it who it should report to.

call vs apply vs bind — Complete Comparison

this Decision Guide

When you are confused about what this is, check these rules in order (first match wins):
  1. new keyword?this is the newly created object.
  2. call/apply/bind?this is whatever you passed as the first argument.
  3. Method call (obj.fn())?this is the object to the left of the dot.
  4. Arrow function?this is whatever this was in the enclosing scope when the arrow was defined.
  5. Plain function call (fn())?this is undefined (strict mode) or window/globalThis (sloppy mode).

3. Prototypes

Every JavaScript object has a hidden property called [[Prototype]] (accessible via __proto__ or Object.getPrototypeOf()). When you access a property, JavaScript looks up the prototype chain. Think of the prototype chain like a family tree of knowledge. When you (the object) need to answer a question (access a property), you first check if you know the answer yourself. If not, you ask your parent. If they do not know either, they ask their parent, all the way up to the root ancestor (Object.prototype). If nobody in the chain knows, the answer is undefined.

The Prototype Chain

When accessing rabbit.toString():
  1. Check rabbit — Not found
  2. Check animal — Not found
  3. Check Object.prototype — Found! (this is why every object has toString())

Object.create()

The proper way to create an object with a specific prototype.

4. Constructor Functions

Before ES6 classes, constructor functions were the standard way to create objects with shared behavior.

The new Keyword

When you call new Person(), JavaScript does five things behind the scenes:
  1. Creates a new empty object {}
  2. Sets the new object’s [[Prototype]] to Person.prototype
  3. Binds this inside the constructor to the new object
  4. Executes the constructor function body
  5. Returns the new object (unless the constructor explicitly returns a different object)
Forgetting new is a silent bug. If you call Person('Alice', 25) without new, this will be undefined (strict mode) or window (sloppy mode), and the constructor will either throw an error or silently attach properties to the global object. ES6 classes fix this by throwing a TypeError if you call them without new.

5. ES6 Classes

Classes are syntactic sugar over constructor functions and prototypes. They do not introduce a new OOP model — under the hood, a class declaration creates a constructor function with methods on its .prototype, exactly like the manual approach above. The benefit is cleaner syntax and built-in guardrails (like requiring new).

Inheritance with extends

Private Fields (ES2022)

Object Creation Patterns — When to Use Which

Decision guide:
  • Simple data objects (API responses, config): Object literals. No ceremony needed.
  • Shared behavior via inheritance: ES6 classes. Cleanest syntax, enforces new, supports #private.
  • Composition over inheritance: Factory functions. Avoid class hierarchies; compose behavior from small functions.
  • Delegation without constructors: Object.create(). Useful for dictionary-like objects (Object.create(null) creates an object with no prototype — no toString, no hasOwnProperty, nothing inherited).

6. Object Methods

Object.keys/values/entries

Object.assign and Spread

Both Object.assign and spread create SHALLOW copies. Nested objects are shared by reference, not cloned. If you modify a nested object in the copy, the original changes too. For deep copies, use structuredClone(obj) (available in modern browsers and Node 17+) or a library.

Copying Objects — When to Use Which Method

Decision guide:
  • Flat object, no nesting: Use spread { ...obj }. Simplest and fastest.
  • Nested objects, no functions: Use structuredClone(). Correct deep copy with circular reference support.
  • Nested objects with functions: No built-in solution. Use a library like Lodash _.cloneDeep(), or write a custom recursive clone.
  • Never use JSON.parse(JSON.stringify()) in production: It silently drops functions, converts Dates to strings, loses undefined properties, and throws on circular references. It is only acceptable as a quick hack in debugging.

Object.freeze and Object.seal


Summary

  • Objects: Key-value pairs. Use dot or bracket notation.
  • this: Depends on how a function is called. Arrow functions inherit this.
  • Prototypes: Objects inherit from other objects via the prototype chain.
  • Classes: Syntactic sugar over prototypes. Use extends for inheritance.
  • Private Fields: Use # prefix for true encapsulation (ES2022).
Next, we’ll dive into Async JavaScript, understanding callbacks, Promises, and async/await.

Interview Deep-Dive

Strong Answer:
  • In classical inheritance (Java, C++), you define a class (a blueprint), and objects are instances stamped from that blueprint. The class hierarchy is static — defined at compile time. A Dog extends Animal relationship is baked into the type system.
  • In prototypal inheritance, there are no classes at the fundamental level (ES6 class is syntactic sugar). Objects inherit directly from other objects. Every object has an internal [[Prototype]] link pointing to another object. When you access a property on an object and it is not found, the engine walks up the prototype chain — checking the prototype, then the prototype’s prototype — until it finds the property or reaches null.
  • The key practical difference: prototypal inheritance is dynamic. You can modify a prototype at runtime and all objects that inherit from it immediately see the change. In classical inheritance, adding a method to a base class after compilation requires recompilation. In JavaScript, Animal.prototype.breathe = function() {} instantly gives every existing animal instance the breathe method. This is powerful but dangerous — monkey-patching built-in prototypes (like Array.prototype) is how libraries historically caused conflicts.
  • Another difference: prototypal inheritance naturally supports “mixins” and composition. You can copy methods from multiple source objects onto a single target using Object.assign(target.prototype, mixin1, mixin2). Classical inheritance typically supports single inheritance, requiring interfaces or abstract classes for multiple behavior contracts.
  • Real-world implication: when you write class Dog extends Animal in JavaScript, behind the scenes, Dog.prototype.__proto__ === Animal.prototype is true. The extends keyword sets up the prototype chain. The super keyword navigates it. Understanding this means you can debug inheritance bugs by inspecting the actual prototype chain with Object.getPrototypeOf() instead of guessing at class hierarchies.
Follow-up: If I modify Object.prototype, what happens to every object in the application?Every object in JavaScript (except those created with Object.create(null)) has Object.prototype at the top of its prototype chain. If you add Object.prototype.hack = true, then {}.hack, [].hack, new Date().hack, and even (function(){}).hack all return true. This is why modifying Object.prototype is considered one of the most dangerous things you can do in JavaScript. It also breaks for...in loops (the new property becomes enumerable and shows up in every loop) unless you use Object.defineProperty with enumerable: false. Libraries like Prototype.js (early 2000s) did this aggressively and caused conflicts with every other library. Modern best practice: never modify built-in prototypes except for polyfills in specific, controlled environments.
Strong Answer:
  • this in JavaScript is not determined by where a function is written, but by how it is called (the call site). The four rules, in priority order: (1) new binding — this is the newly created object. (2) Explicit binding — call, apply, bind set this directly. (3) Implicit binding — obj.fn() sets this to obj. (4) Default binding — standalone fn() gets this = undefined in strict mode, or window/globalThis in sloppy mode. Arrow functions are the exception: they have no this of their own and inherit it lexically from the enclosing scope.
  • Senior engineer trap: destructuring a method from an object. Consider: const { greet } = person; greet();. This looks clean and is common in modern JavaScript. But it detaches greet from person, so this inside greet is no longer person. It is undefined (strict mode). This happens constantly in React class components: onClick={this.handleClick} passes the method as a bare function reference, losing this. The fix is .bind(this) in the constructor or using arrow function class fields.
  • Another senior trap: passing a method to setTimeout or setInterval. setTimeout(person.greet, 1000) does not call person.greet() — it stores the function reference and calls it later as a bare function. Same this loss. Same fix: .bind(person) or wrap in an arrow function () => person.greet().
  • The deepest gotcha: this in a nested function inside a method. Even if the outer method has the correct this, a regular function defined inside it gets its own this (default binding). This is why the const self = this pattern existed before arrow functions, and why arrow functions are now the standard for callbacks inside methods.
Follow-up: What happens to this when you bind a function twice? Does the second bind override the first?No. bind creates a new function with a permanently fixed this. Calling .bind() again on an already-bound function wraps it in another layer but does NOT override the original binding. const bound1 = fn.bind(objA); const bound2 = bound1.bind(objB); bound2()this inside fn is still objA, not objB. The second bind creates a wrapper that calls bound1 with objB as this, but bound1 ignores that because it is already hard-bound to objA. This is specified in the ECMAScript spec: a bound function’s [[BoundThis]] cannot be overridden by another bind. The only exception: new can override a bindnew bound1() creates a new object for this, ignoring the bound objA.
Strong Answer:
  • Object.create(null) creates an object with absolutely no prototype. Its [[Prototype]] is null, not Object.prototype. This means it has no inherited properties at all: no toString, no hasOwnProperty, no constructor, no __proto__ getter/setter. It is a truly blank dictionary.
  • The primary use case is creating a “clean” hash map. When you use a plain object {} as a dictionary, keys like toString, constructor, __proto__, and hasOwnProperty are already “occupied” by inherited properties. If a user-provided key happens to be "constructor" or "__proto__", you get subtle bugs or even security vulnerabilities (__proto__ pollution attacks). Object.create(null) eliminates this entire class of bugs.
  • Real-world usage: Express.js and many routing libraries internally use Object.create(null) for route lookup tables. V8 also optimizes Object.create(null) objects as “dictionary mode” objects, which can be faster for highly dynamic key access patterns (frequent additions/deletions) compared to regular objects which V8 tries to optimize with hidden classes.
  • The trade-off: you lose all Object.prototype methods. obj.hasOwnProperty("key") throws because hasOwnProperty does not exist on the object. You must use Object.hasOwn(obj, "key") (ES2022) or Object.prototype.hasOwnProperty.call(obj, "key"). You also cannot use obj.toString() — you must handle serialization explicitly.
  • Since ES6, Map is generally the better choice for dynamic key-value lookups because it handles any key type, has no prototype pollution risk, and provides .size, iteration, and better performance for frequent add/delete operations. Object.create(null) is still useful when you need an object (for JSON serialization, for example) but want prototype safety.
Follow-up: What is prototype pollution, and how does it become a security vulnerability?Prototype pollution is when an attacker injects properties into Object.prototype (or another prototype) through user-controlled input. The classic vector: a deep merge function that recursively copies properties from untrusted JSON into an object. If the attacker sends {"__proto__": {"isAdmin": true}}, a naive merge copies isAdmin onto Object.prototype. Now every object in the application has isAdmin === true, potentially bypassing authorization checks. Real CVEs have been filed against lodash’s _.merge, jQuery’s $.extend, and many other libraries for exactly this vulnerability. Mitigations: (1) never allow __proto__, constructor, or prototype as keys in user input, (2) use Object.create(null) for lookup tables, (3) use Map instead of plain objects for user-controlled keys, (4) freeze prototypes in security-critical code, (5) validate and sanitize all deeply-merged input.
Strong Answer:
  • When you write class Person { constructor(name) { this.name = name; } greet() { return this.name; } }, the engine creates: (1) a constructor function Person whose body is the constructor method, (2) Person.prototype.greet = function() { return this.name; } — methods are placed on the prototype, not on each instance. (3) Person.prototype.constructor = Person — the circular reference that constructors have by convention.
  • You can verify: typeof Person is "function". Person.prototype.greet exists. new Person("Alice").__proto__ === Person.prototype is true. All of this is identical to the pre-class constructor function pattern.
  • The differences (not just sugar): (1) Classes enforce new — calling Person("Alice") without new throws TypeError. Constructor functions silently bind this to window/undefined. (2) Class methods are non-enumerable by default (Object.keys(Person.prototype) returns []). Constructor function prototype methods are enumerable. (3) Class bodies are always in strict mode, even without "use strict". (4) Classes are not hoisted in the same way — they have a TDZ like let/const. (5) extends properly sets up the prototype chain including the constructor’s own prototype (Dog.__proto__ === Animal), enabling static method inheritance.
  • The extends keyword does two things: Dog.prototype.__proto__ = Animal.prototype (instance method inheritance) and Dog.__proto__ = Animal (static method inheritance). The second one is unique to classes — pre-ES6, static method inheritance required manual setup.
  • Private fields (#field) are genuinely new — they are not sugar over anything. They use a WeakMap-like internal mechanism that provides hard privacy, not the convention-based _underscore pattern.
Follow-up: Can you mix class syntax with manual prototype manipulation? What happens?Yes, they are fully interoperable. After defining class Person {}, you can do Person.prototype.legacyMethod = function() { return "works"; } and new Person().legacyMethod() returns "works". You can also do Object.getPrototypeOf(new Person()) === Person.prototype to inspect the chain. In practice, this interop is how polyfills and legacy code coexist with modern classes. The only caveat: if you overwrite Person.prototype entirely (not just add to it), you break the prototype chain for existing instances and lose the constructor reference. This is true for both classes and constructor functions.