Chapter 8: Objects and Prototypes
Objects are the foundation of JavaScript. Almost everything in the language is built on objects, from arrays and functions to dates and regular expressions. An object is a collection of key–value pairs, where each key is a string (or symbol) and each value can be any data type, including another object or function.
JavaScript’s object system is unique among major languages because it is prototype-based rather than class-based at its core. Although modern syntax allows classes for convenience, under the surface everything still works through prototypes and delegation along a chain of linked objects.
This chapter explores how objects are created, extended, and connected through prototypes. You will learn how to use object literals, control property behavior with descriptors, and understand how prototype delegation defines inheritance in JavaScript. You will also examine how this works within object methods, how to create new objects from existing ones, and how JSON represents data structures in a portable text format.
By mastering objects and prototypes, you gain a deeper understanding of how JavaScript actually works under the hood, allowing you to design data structures, manage inheritance, and build maintainable, extensible programs with confidence.
Object literals and property access
Objects in JavaScript are usually created using object literals—a simple, readable syntax that defines key–value pairs inside curly braces. Each key is called a property name, and each value can be of any type, including another object, array, or function.
Creating objects with literals
An object literal is written as a list of property definitions inside braces:
const user = {
name: 'Robin',
age: 55,
isAuthor: true
};
You can create an empty object with {} or use new Object(), though the literal form is preferred for its clarity and conciseness.
const empty = {};
const another = new Object();
Accessing properties
JavaScript provides two main ways to access object properties: dot notation and bracket notation. Dot notation is the most common and is used when the property name is a valid identifier:
console.log(user.name); // Robin
console.log(user.age); // 55
Bracket notation is required when the property name is dynamic (stored in a variable) or contains spaces or other special characters:
const key = 'isAuthor';
console.log(user[key]); // true
const data = { 'user name': 'Robin' };
console.log(data['user name']); // Robin
Adding, changing, and deleting properties
Objects are mutable, so you can freely add, modify, or remove properties after creation:
user.country = 'UK';
user.age = 56;
delete user.isAuthor;
console.log(user);
// { name: 'Robin', age: 56, country: 'UK' }
null or undefined if you only need to mark it as unused.
Nested objects and property chains
Objects can contain other objects, creating nested structures for representing more complex data:
const book = {
title: 'This is JavaScript',
author: {
first: 'Robin',
last: 'Nixon'
}
};
console.log(book.author.first); // Robin
Attempting to access a property on undefined or null will cause an error. The optional chaining operator (?.) can safely check nested properties without throwing:
console.log(book.publisher?.name); // undefined, no error
?.) and nullish coalescing (??) make working with nested objects safer and more expressive. Together they help avoid long, repetitive checks for undefined or null.
JavaScript Descriptors
Every property in a JavaScript object has an associated set of attributes known as a property descriptor. These descriptors control how the property behaves, such as whether it can be changed, listed in loops, or deleted. Understanding them gives you precise control over object behavior and immutability.
Viewing property descriptors
You can inspect the descriptor of a property using Object.getOwnPropertyDescriptor():
const user = { name: 'Robin' };
const descriptor = Object.getOwnPropertyDescriptor(user, 'name');
console.log(descriptor);
This outputs an object describing the property:
{
value: 'Robin',
writable: true,
enumerable: true,
configurable: true
}
These three Boolean attributes (writable, enumerable, and configurable) define the property’s mutability, visibility, and flexibility.
writable
The writable flag determines whether the property’s value can be changed. If it is set to false, any attempt to modify the value will silently fail in non-strict mode or throw an error in strict mode:
const user = {};
Object.defineProperty(user, 'id', {
value: 1001,
writable: false
});
user.id = 2002; // silently ignored (or error in strict mode)
console.log(user.id); // 1001
writable: false only affects the property’s value. It does not prevent deleting or reconfiguring the property unless configurable is also false.
enumerable
The enumerable flag determines whether a property appears in loops such as for...in and in methods like Object.keys() or JSON.stringify(). Non-enumerable properties remain accessible but are hidden from iteration:
const user = {};
Object.defineProperty(user, 'secret', {
value: 'classified',
enumerable: false
});
user.visible = 'shown';
for (const key in user) {
console.log(key); // only "visible" is shown
}
console.log(Object.keys(user)); // ['visible']
console.log(user.secret); // accessible directly
toString()) are non-enumerable. This is why looping through an object does not list them.
configurable
The configurable flag determines whether a property can be deleted or redefined. Once configurable is set to false, it cannot be changed back, and the property cannot be removed or reconfigured:
const user = {};
Object.defineProperty(user, 'role', {
value: 'admin',
configurable: false
});
delete user.role; // has no effect
Object.defineProperty(user, 'role', { value: 'editor' }); // throws error
configurable is false, even writable and enumerable cannot be altered. Use it only when you are certain a property should remain fixed.
Defining multiple properties at once
You can define several properties in one operation using Object.defineProperties(). This is useful when setting detailed descriptors during object initialization:
const book = {};
Object.defineProperties(book, {
title: {
value: 'This is JavaScript',
writable: false
},
author: {
value: 'Robin Nixon',
enumerable: true
}
});
console.log(Object.getOwnPropertyDescriptors(book));
Descriptors allow you to design objects with carefully controlled interfaces, making them ideal for secure APIs, library design, or immutable configurations.
Prototype chain and delegation
Every JavaScript object has an internal link to another object known as its prototype. When you try to access a property that does not exist on an object, JavaScript automatically looks for it up the prototype chain. This mechanism is called delegation and it forms the basis of JavaScript’s inheritance model.
The prototype link
You can view or access an object’s prototype using Object.getPrototypeOf(), and set it explicitly using Object.setPrototypeOf() (though doing so dynamically can affect performance):
const parent = { kind: 'parent' };
const child = { name: 'Robin' };
Object.setPrototypeOf(child, parent);
console.log(child.kind); // inherited from parent
console.log(Object.getPrototypeOf(child) === parent); // true
In this example, the child object delegates missing property lookups to its prototype (parent), forming a simple two-link chain.
__proto__. It is a legacy accessor for the prototype, equivalent to Object.getPrototypeOf(), but should not be used in production code.
Prototype delegation in action
When JavaScript encounters a property access like child.kind, it checks the object itself first. If the property is not found, it follows the prototype link and continues searching up the chain until it either finds the property or reaches the top, the Object.prototype.
const grandparent = { species: 'human' };
const parent = Object.create(grandparent);
parent.role = 'developer';
const child = Object.create(parent);
child.name = 'Robin';
console.log(child.species); // found on grandparent
console.log(child.role); // found on parent
console.log(child.name); // found on child
The lookup sequence continues up the chain until no prototype remains, at which point JavaScript returns undefined.
The end of the chain
All standard objects in JavaScript ultimately inherit from Object.prototype, which provides fundamental methods like toString(), valueOf(), and hasOwnProperty():
console.log(Object.getPrototypeOf({}) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
This means that every object has access to those shared methods unless they are shadowed (redefined) further down the chain.
toString or constructor. Overriding them can cause unexpected results or break built-in behavior.
Creating custom prototypes
The most common way to create objects with a specific prototype is using Object.create(). It allows you to define a prototype and optionally provide property descriptors at creation time:
const animal = {
speak() {
console.log('Generic sound');
}
};
const dog = Object.create(animal);
dog.speak(); // Generic sound
dog.speak = function() {
console.log('Woof!');
};
dog.speak(); // Woof!
In this case, dog delegates to animal for any missing properties, but can override them locally when needed.
How delegation differs from class inheritance
Unlike class-based inheritance found in languages such as Java or C++, JavaScript’s prototype system uses delegation rather than copying. No properties or methods are duplicated since objects simply link to one another dynamically.
const base = { greet() { console.log('Hello'); } };
const derived = Object.create(base);
derived.greet(); // Hello
base.greet = function() { console.log('Hi'); };
derived.greet(); // Hi (delegation reflects the change)
Because delegation is dynamic, changes to a prototype are immediately visible to all objects that inherit from it. This flexibility makes prototypes both powerful and potentially hazardous if modified carelessly at runtime.
class and extends) is built on top of this same prototype mechanism. Classes simply provide a cleaner, more structured way to define prototype chains.
this binding rules
In JavaScript, the value of this is determined by how a function is called, not where it is defined. Understanding these binding rules is crucial when working with object methods, event handlers, and constructors, because this can change depending on the calling context.
Default binding
When a function is called in the global scope or as a plain function (not attached to an object), this refers to the global object (window in browsers, global in Node.js). In strict mode, however, this becomes undefined instead.
function showThis() {
console.log(this);
}
showThis(); // window (or undefined in strict mode)
this to the global object.
Implicit binding (object methods)
When a function is called as a property of an object, this is automatically set to the object before the dot:
const user = {
name: 'Robin',
greet() {
console.log('Hello, ' + this.name);
}
};
user.greet(); // Hello, Robin
However, if the method is extracted and called separately, the binding is lost:
const greetFn = user.greet;
greetFn(); // undefined or window, depending on mode
Explicit binding with call, apply, and bind
You can manually control what this refers to by using Function.prototype.call(), apply(), or bind(). These methods allow precise context management:
function greet() {
console.log('Hello, ' + this.name);
}
const user = { name: 'Robin' };
const guest = { name: 'Alex' };
greet.call(user); // Hello, Robin
greet.apply(guest); // Hello, Alex
const boundGreet = greet.bind(user);
boundGreet(); // Hello, Robin
call() and apply() invoke the function immediately, while bind() returns a new function with this permanently set to the specified value.
Constructor binding
When a function is used with the new keyword, it acts as a constructor. A new object is automatically created and assigned to this inside the function:
function Person(name) {
this.name = name;
}
const user = new Person('Robin');
console.log(user.name); // Robin
In this case, this refers to the newly created instance. If the constructor explicitly returns an object, that object replaces the default this.
Lexical binding in arrow functions
Arrow functions behave differently from regular functions because they do not have their own this. Instead, they inherit this from the surrounding scope (lexical binding):
const counter = {
value: 0,
start() {
setInterval(() => {
this.value++;
console.log(this.value);
}, 1000);
}
};
counter.start();
Here, the arrow function inside setInterval uses the same this as the enclosing start() method, so it correctly refers to counter.
this value cannot be changed using call(), apply(), or bind().
Binding precedence summary
When multiple binding rules could apply, JavaScript follows a clear order of precedence:
- new binding (highest priority)
- explicit binding (call, apply, bind)
- implicit binding (object method)
- default binding (global or undefined)
Arrow functions bypass this order because they always use lexical binding from their defining scope.
this always refers to what you expect.
Creating and assigning objects
JavaScript provides several built-in methods for creating and combining objects. Two of the most important are Object.create() and Object.assign(). The first lets you create an object with a specific prototype, while the second copies properties from one or more source objects into a target object.
Creating objects with Object.create()
Object.create() creates a new object whose internal prototype is set to a given object. This allows precise control over inheritance without using constructor functions or classes:
const animal = {
speak() {
console.log('Generic sound');
}
};
const dog = Object.create(animal);
dog.bark = function() {
console.log('Woof!');
};
dog.speak(); // Generic sound (from prototype)
dog.bark(); // Woof!
Here, dog inherits from animal. Any properties not found on dog are delegated up the prototype chain to animal.
Object.create() containing property descriptors, allowing you to define properties with full control from the start.
const cat = Object.create(animal, {
sound: {
value: 'Meow',
writable: false
}
});
console.log(cat.sound); // Meow
cat.sound = 'Purr'; // Ignored (not writable)
Copying properties with Object.assign()
Object.assign() copies all enumerable own properties from one or more source objects into a target object. It returns the target object after merging:
const defaults = { theme: 'light', fontSize: 14 };
const settings = { theme: 'dark' };
const finalConfig = Object.assign({}, defaults, settings);
console.log(finalConfig); // { theme: 'dark', fontSize: 14 }
If multiple sources contain the same key, later ones overwrite earlier values. Non-enumerable properties are ignored, and property descriptors are not preserved (only values are copied).
Object.assign() performs a shallow copy. If a property holds a nested object, only the reference is copied, not a deep clone of its contents.
const original = { info: { name: 'Robin' } };
const copy = Object.assign({}, original);
copy.info.name = 'Alex';
console.log(original.info.name); // Alex (same reference)
Combining patterns
Object.create() and Object.assign() can be used together to build objects with inherited behavior and custom overrides:
const baseUser = {
role: 'user',
describe() {
console.log(this.name + ' (' + this.role + ')');
}
};
const admin = Object.create(baseUser);
Object.assign(admin, { name: 'Robin', role: 'admin' });
admin.describe(); // Robin (admin)
This pattern is lightweight and flexible. It avoids the need for classes or constructors while still providing inheritance and customization through simple composition.
Object.create() and Object.assign() form the foundation of many modern design patterns, such as mixins and prototype-based factories. They offer a clean, low-level way to model relationships and behavior in JavaScript.
JSON as a data format
JSON (JavaScript Object Notation) is a lightweight, text-based format for storing and transmitting data. It is based on JavaScript object syntax, but it is used independently of the language. JSON has become a universal standard for data exchange between applications, APIs, and databases.
Structure of JSON
JSON represents data as key–value pairs enclosed in curly braces, much like a JavaScript object literal. Keys must always be strings wrapped in double quotes, and values can be strings, numbers, booleans, arrays, objects, or null:
{
"name": "Robin",
"age": 55,
"isAuthor": true,
"books": ["This is Python", "This is JavaScript"],
"details": { "country": "UK" }
}
Although JSON looks like JavaScript, it is purely data and it cannot include functions, comments, or special values such as undefined.
Converting objects to JSON
To convert a JavaScript object into a JSON string, use JSON.stringify(). This is commonly done when sending data over a network or saving it to a file:
const user = {
name: 'Robin',
age: 55,
active: true
};
const json = JSON.stringify(user);
console.log(json); // {"name":"Robin","age":55,"active":true}
You can also control how objects are stringified by providing a replacer function or an array of property names, as well as a number of spaces for readable formatting:
console.log(JSON.stringify(user, null, 2));
JSON.stringify(object, null, 2) for a neatly formatted output.
Parsing JSON back into objects
To convert a JSON string back into a JavaScript object, use JSON.parse():
const text = '{"name":"Robin","age":55}';
const obj = JSON.parse(text);
console.log(obj.name); // Robin
Parsing invalid JSON will throw a SyntaxError, so it is good practice to wrap JSON.parse() in a try...catch block when handling data from unreliable sources.
try {
const data = JSON.parse(invalidText);
} catch (e) {
console.error('Invalid JSON data');
}
JSON and data interchange
Because JSON is language-independent, it is supported by almost every programming environment (and is even a useful way to convey complex instructions to generative AI). APIs and web services typically send and receive data in JSON format, which can easily be parsed or generated by both client and server applications.
// Example of sending JSON data with fetch
fetch('/api/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Robin', active: true })
});
JSON’s simplicity and portability have made it the de facto standard for configuration files, REST APIs, and structured data storage in both browsers and servers.
JSON bridges the gap between JavaScript objects and universal data exchange formats. Using JSON.stringify() and JSON.parse(), you can serialize and deserialize data with ease, enabling communication across systems, services, and platforms in a consistent, language-neutral way.