Skip to content
beginner Phase 4 · JavaScript Fundamentals

Objects & Prototypes

Understand object creation, prototype chain, Object methods, and the `this` keyword.

1h 15m
0 problems
Topic Progress 0%

Object Creation Patterns

Object Creation Patterns

JavaScript objects are collections of key-value pairs that can hold data and functions. There are several ways to create objects, each with specific use cases.

Object Literal

The simplest way to create an object is using literal syntax:

const person = {
  name: 'Alice',
  age: 30,
  greet() {
    return `Hello, my name is ${this.name}`;
  }
};

console.log(person.greet()); // 'Hello, my name is Alice'

Object literals are ideal when you need a one-off object. They are concise and easy to read. Each property can be a primitive value, another object, or a method.

Constructor Functions

Constructor functions allow you to create multiple objects with the same structure:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
  this.getAge = function() {
    return new Date().getFullYear() - this.year;
  };
}

const myCar = new Car('Toyota', 'Camry', 2020);
console.log(myCar.getAge()); // 6

When you use the new keyword, JavaScript creates a new empty object, sets the prototype, executes the constructor with this bound to the new object, and returns the object.

Object.create

Object.create creates a new object with a specified prototype:

const animalProto = {
  speak() {
    return `${this.name} makes a sound.`;
  }
};

const dog = Object.create(animalProto);
dog.name = 'Rex';
dog.speak = function() {
  return `${this.name} barks!`;
};

console.log(dog.speak()); // 'Rex barks!'

This method gives you explicit control over the object's prototype, which is fundamental to understanding inheritance in JavaScript.

Property Descriptors and Attributes

Property Descriptors and Attributes

Every property in a JavaScript object has attributes that control how the property behaves. These are called property descriptors.

Property Descriptor Attributes

Each property descriptor can have the following attributes:

  • value: The value of the property (defaults to undefined)
  • writable: Whether the property can be changed (defaults to false in strict mode)
  • enumerable: Whether the property shows up in for...in loops (defaults to false)
  • configurable: Whether the property can be deleted or reconfigured (defaults to false)

Object.defineProperty

You can define or modify properties with specific descriptors:

const user = {};

Object.defineProperty(user, 'name', {
  value: 'Alice',
  writable: false,
  enumerable: true,
  configurable: false
});

console.log(user.name); // 'Alice'
user.name = 'Bob'; // Silently fails in non-strict mode, throws in strict
console.log(user.name); // 'Alice' (unchanged)

// Try to delete
delete user.name; // false (cannot delete non-configurable property)

Getters and Setters

You can define computed properties using getter and setter methods:

const temperature = {
  _celsius: 0,
  get fahrenheit() {
    return (this._celsius * 9/5) + 32;
  },
  set fahrenheit(f) {
    this._celsius = (f - 32) * 5/9;
  }
};

temperature.fahrenheit = 212;
console.log(temperature._celsius); // 100
console.log(temperature.fahrenheit); // 212

Object.getOwnPropertyDescriptor

You can retrieve the descriptor of any property:

const descriptor = Object.getOwnPropertyDescriptor(user, 'name');
console.log(descriptor);
// { value: 'Alice', writable: false, enumerable: true, configurable: false }

Understanding property descriptors is crucial for writing robust code, especially when you need to protect certain properties from modification or create computed properties.

Prototype Chain and Inheritance

Prototype Chain and Inheritance

JavaScript uses prototypal inheritance, where objects can inherit properties and methods from other objects through the prototype chain.

How the Prototype Chain Works

When you access a property on an object, JavaScript first checks the object itself. If not found, it walks up the prototype chain until the property is found or null is reached.

const animal = {
  eats: true,
  walk() {
    return 'Walking...';
  }
};

const rabbit = Object.create(animal);
rabbit.jumps = true;

console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
console.log(rabbit.walk()); // 'Walking...' (inherited method)
console.log(rabbit); // { jumps: true }

hasOwnProperty vs in Operator

The hasOwnProperty method only checks the object's own properties, while the in operator checks the entire prototype chain:

console.log(rabbit.hasOwnProperty('jumps')); // true
console.log(rabbit.hasOwnProperty('eats')); // false (inherited)
console.log('jumps' in rabbit); // true
console.log('eats' in rabbit); // true (found in prototype chain)

Constructor and prototype Property

Every function has a prototype property that becomes the prototype of objects created with that constructor:

function Shape(color) {
  this.color = color;
}

Shape.prototype.describe = function() {
  return `This is a ${this.color} shape`;
};

function Circle(color, radius) {
  Shape.call(this, color);
  this.radius = radius;
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.area = function() {
  return Math.PI * this.radius ** 2;
};

const redCircle = new Circle('red', 5);
console.log(redCircle.describe()); // 'This is a red shape'
console.log(redCircle.area()); // 78.54

Checking Prototype Relationships

You can check relationships between objects using instanceof and Object.getPrototypeOf:

console.log(redCircle instanceof Circle); // true
console.log(redCircle instanceof Shape); // true
console.log(Object.getPrototypeOf(redCircle) === Circle.prototype); // true

Understanding the prototype chain is essential for debugging, performance optimization, and designing inheritance hierarchies in JavaScript.

The this Keyword

The this Keyword

The this keyword in JavaScript refers to the object that is currently executing the code. Its value depends on how a function is called, not where it is defined.

Four Rules for this

1. Default Binding (Global Context)

When a function is called standalone, this refers to the global object (window in browsers, global in Node.js):

function showThis() {
  console.log(this);
}

showThis(); // window (in browser), global (in Node.js)

2. Implicit Binding (Object Method)

When a function is called as a method of an object, this refers to that object:

const calculator = {
  value: 0,
  add(n) {
    this.value += n;
    return this;
  },
  subtract(n) {
    this.value -= n;
    return this;
  }
};

calculator.add(5).subtract(2);
console.log(calculator.value); // 3

3. Explicit Binding (call, apply, bind)

You can explicitly set this using call, apply, or bind:

function greet(greeting) {
  return `${greeting}, I am ${this.name}`;
}

const user = { name: 'Alice' };

// call: arguments passed individually
console.log(greet.call(user, 'Hello')); // 'Hello, I am Alice'

// apply: arguments passed as array
console.log(greet.apply(user, ['Hi'])); // 'Hi, I am Alice'

// bind: returns new function with fixed this
const boundGreet = greet.bind(user);
console.log(boundGreet('Hey')); // 'Hey, I am Alice'

4. new Binding (Constructor)

When using new to create an object, this refers to the newly created instance:

function Person(name) {
  this.name = name;
}

const bob = new Person('Bob');
console.log(bob.name); // 'Bob'

Arrow Functions and this

Arrow functions do not have their own this. They inherit this from the enclosing lexical scope:

const team = {
  name: 'Engineering',
  members: ['Alice', 'Bob'],
  showMembers() {
    this.members.forEach((member) => {
      // Arrow function inherits 'this' from showMembers
      console.log(`${member} is in ${this.name}`);
    });
  }
};

team.showMembers();
// 'Alice is in Engineering'
// 'Bob is in Engineering'

Common Pitfalls

Losing implicit binding when passing a method as a callback:

const user = {
  name: 'Alice',
  greet() {
    console.log(`Hello, ${this.name}`);
  }
};

const fn = user.greet;
fn(); // 'Hello, undefined' (this lost)

Understanding this is critical for writing correct JavaScript, especially when working with callbacks, event handlers, and class-based code.

Built-in Object Methods

Built-in Object Methods

JavaScript provides a rich set of methods on the Object constructor for manipulating and inspecting objects.

Object.assign

Object.assign copies properties from one or more source objects to a target object:

const defaults = { color: 'blue', size: 'medium', price: 100 };
const userPrefs = { color: 'red', price: 150 };

const config = Object.assign({}, defaults, userPrefs);
console.log(config);
// { color: 'red', size: 'medium', price: 150 }

Note that Object.assign performs a shallow copy. Nested objects are copied by reference:

const source = { nested: { a: 1, b: 2 } };
const target = Object.assign({}, source);

target.nested.a = 99;
console.log(source.nested.a); // 99 (both reference the same object)

For deep cloning, use structured clone or spread with recursion.

Object.keys, Object.values, Object.entries

These methods return arrays of an object's keys, values, or key-value pairs:

const product = {
  name: 'Laptop',
  price: 999,
  stock: 45
};

console.log(Object.keys(product)); // ['name', 'price', 'stock']
console.log(Object.values(product)); // ['Laptop', 999, 45]
console.log(Object.entries(product));
// [['name', 'Laptop'], ['price', 999], ['stock', 45]]

// Useful for iteration
Object.entries(product).forEach(([key, value]) => {
  console.log(`${key}: ${value}`);
});

Object.fromEntries

The reverse of Object.entries - creates an object from an array of key-value pairs:

const entries = [['name', 'Laptop'], ['price', 999]];
const obj = Object.fromEntries(entries);
console.log(obj); // { name: 'Laptop', price: 999 }

// Practical use: converting Map to object
const map = new Map([['a', 1], ['b', 2]]);
const objFromMap = Object.fromEntries(map);
console.log(objFromMap); // { a: 1, b: 2 }

Object.freeze and Object.seal

Object.freeze makes an object immutable. Object.seal prevents adding/removing properties but allows modifying existing values:

const frozen = Object.freeze({ x: 1, y: 2 });
frozen.x = 99; // Silently fails (throws in strict mode)
console.log(frozen.x); // 1

const sealed = Object.seal({ a: 1, b: 2 });
sealed.a = 99; // Works
sealed.c = 3; // Silently fails (cannot add new properties)

Object.keys Length Check

Use Object.keys().length to check if an object has properties:

const empty = {};
const filled = { a: 1 };

console.log(Object.keys(empty).length === 0); // true
console.log(Object.keys(filled).length === 0); // false

These built-in methods are essential tools for working with objects efficiently and writing clean, maintainable JavaScript code.

Quiz

1. What does Object.create do in JavaScript?

Question 1 options

2. What is the output of the following code? ```javascript const obj = { a: 1, b: 2, c: 3 }; console.log(Object.keys(obj).length); ```

Question 2 options

3. Why does the following code lose its `this` context? ```javascript const user = { name: 'Alice', greet() { console.log(this.name); } }; const fn = user.greet; fn(); ```

Question 3 options

Flashcards

Question

What is the difference between Object.assign and the spread operator for shallow copying?

Answer

Both create shallow copies, but Object.assign targets a specific object while spread creates a new one. Object.assign({}, source) and { ...source } produce the same result for shallow copies. Both copy only one level deep—nested objects are shared by reference.

Question

How does the prototype chain work when accessing a property?

Answer

When you access obj.prop, JavaScript checks: 1) Does obj have an own property 'prop'? If yes, use it. 2) Check obj's prototype ([[Prototype]]). 3) Continue up the chain until found or null is reached. This is why methods defined on prototypes are available to all instances.

Question

What are the four rules for determining the value of `this`?

Answer

1) Default: standalone function call → global object. 2) Implicit: method call on object → that object. 3) Explicit: call/apply/bind → the specified object. 4) New: constructor call → newly created instance. Arrow functions always inherit this from enclosing scope.

Revision Notes

Key Takeaways

  • 1. Objects are created via literals, constructors, or Object.create—choose based on whether you need a single instance or multiple instances with shared methods
  • 2. The prototype chain is how JavaScript achieves inheritance: properties are looked up through [[Prototype]] links until found or null
  • 3. this depends on how a function is called, not where it's defined—use call/apply/bind for explicit binding, arrow functions for lexical this
  • 4. Object.keys/values/entries convert objects to arrays for iteration; Object.fromEntries reverses this process
  • 5. Object.freeze makes objects immutable; Object.seal prevents adding/removing properties but allows value changes

Interview Tips

  • Explain the difference between prototype-based inheritance and class-based inheritance—JavaScript uses prototypes even with ES6 classes
  • Be prepared to explain why this behaves differently in arrow functions versus regular functions
  • Know when to use Object.create versus constructor functions versus ES6 classes
  • Understand the performance implications of prototype chain depth—too many levels can slow property lookups
  • Be ready to demonstrate shallow vs deep copying and why Object.assign only copies one level

Cheat Sheet

Object Creation: {} (literal), new Constructor(), Object.create(proto). Prototype: obj.proto or Object.getPrototypeOf(obj). Property Descriptors: writable, enumerable, configurable. this Rules: default (global), implicit (object), explicit (call/apply/bind), new (instance). Object Methods: keys(), values(), entries(), fromEntries(), assign(), freeze(), seal(), defineProperty(). Common Pattern: const obj = Object.create(proto); obj.prop = value;