Objects and Prototypes
Objects are the backbone of JavaScript. Almost everything in the language is an object or behaves like one. In this lesson, you will go beyond the basics of key-value pairs and learn how to build objects with methods, understand the this keyword, and unlock the power of prototypal inheritance, the mechanism that JavaScript uses to share behavior between objects.
What You'll Learn
- How to create objects with literals and constructors
- Dot notation vs bracket notation for property access
- How to define methods and use the
thiskeyword - The prototype chain and how inheritance works in JavaScript
- How to create objects with
Object.create
Object Literals Revisited
You already know how to create simple objects. Let's expand on that with computed property names and shorthand syntax:
// Shorthand property names
const name = "Ada Lovelace";
const year = 1843;
const inventor = { name, year };
console.log(inventor);
// Computed property names
const field = "specialty";
const scientist = {
name: "Marie Curie",
[field]: "Radioactivity",
["birth" + "Year"]: 1867
};
console.log(scientist);
console.log(scientist.specialty);
console.log(scientist.birthYear);
Shorthand syntax removes repetition when the variable name matches the property name. Computed property names let you build keys dynamically using expressions inside square brackets.
Property Access and Manipulation
There are two ways to read and write properties. Dot notation is cleaner for known keys while bracket notation lets you use variables and strings that aren't valid identifiers:
const car = {
make: "Toyota",
model: "Camry",
year: 2024,
"fuel type": "hybrid"
};
// Dot notation
console.log(car.make);
// Bracket notation (required for keys with spaces)
console.log(car["fuel type"]);
// Dynamic access
const key = "model";
console.log(car[key]);
// Check if a property exists
console.log("year" in car);
console.log("color" in car);
// Delete a property
delete car.year;
console.log("After delete:", car);
// Object.keys, Object.values, Object.entries
console.log("Keys:", Object.keys(car));
console.log("Values:", Object.values(car));
console.log("Entries:", Object.entries(car));
Methods and the this Keyword
When a function lives inside an object, it is called a method. Inside a method, the keyword this refers to the object the method was called on. This gives the method access to the object's own properties:
const player = {
name: "Elena",
score: 0,
// Method shorthand syntax
addPoints(points) {
this.score += points;
console.log(this.name + " now has " + this.score + " points");
},
reset() {
this.score = 0;
console.log(this.name + "'s score has been reset");
},
getInfo() {
return this.name + ": " + this.score + " points";
}
};
player.addPoints(10);
player.addPoints(25);
console.log(player.getInfo());
player.reset();
console.log(player.getInfo());
Predict
Predict both lines before running. The method is called two ways: once ON the object, and once after being pulled off into a bare variable. What does each log? (This runs as a plain script — the same way the playground runs it.)
const counter = {
count: 5,
getCount() {
return this.count;
},
};
const getCount = counter.getCount; // pulled off the object
console.log(counter.getCount()); // called on the object
console.log(getCount()); // called on its ownThe value of this depends on how the function is called, not where it is defined. Calling counter.getCount() sets this to counter; pulling the method into a bare variable and calling getCount() loses that binding. This is why passing a method as a callback (setTimeout(obj.method, 100)) so often "forgets" its object — and why bind, arrow functions, or wrapping in () => obj.method() exist to pin this down.
Constructor Functions
Before ES6 classes, constructor functions were the standard way to create multiple objects with the same shape. By convention, constructor names start with a capital letter and are invoked with the new keyword:
function Book(title, author, pages) {
this.title = title;
this.author = author;
this.pages = pages;
this.read = false;
}
Book.prototype.markAsRead = function() {
this.read = true;
console.log('"' + this.title + '" marked as read');
};
Book.prototype.getSummary = function() {
const status = this.read ? "read" : "unread";
return this.title + " by " + this.author + " (" + this.pages + " pages, " + status + ")";
};
const book1 = new Book("Dune", "Frank Herbert", 412);
const book2 = new Book("Neuromancer", "William Gibson", 271);
console.log(book1.getSummary());
book1.markAsRead();
console.log(book1.getSummary());
console.log(book2.getSummary());
// Both objects share the same prototype methods
console.log(book1.getSummary === book2.getSummary);
Methods placed on the prototype are shared across all instances rather than duplicated, which saves memory.
The Prototype Chain
Every JavaScript object has a hidden link to another object called its prototype. When you access a property that doesn't exist on the object itself, JavaScript walks up this chain until it finds the property or reaches null:
const animal = {
alive: true,
breathe() {
return this.name + " is breathing";
}
};
const dog = Object.create(animal);
dog.name = "Rex";
dog.bark = function() {
return this.name + " says Woof!";
};
console.log(dog.bark());
console.log(dog.breathe());
console.log(dog.alive);
// Check the chain
console.log(dog.hasOwnProperty("name"));
console.log(dog.hasOwnProperty("alive"));
// Walk the chain manually
console.log(Object.getPrototypeOf(dog) === animal);
console.log(Object.getPrototypeOf(animal) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype) === null);
Object.create builds a new object whose prototype is the object you pass in. The new object inherits all properties and methods from its prototype, while still being free to add its own.
Debug
child should print 'Woof! Hello, I am Rex' — its own greeting wrapped around the inherited one. The lookup itself doesn't throw; it quietly returns the wrong greeting, with no 'Woof!'. The assert catches it: it fails with 'got Hello, I am Rex', so nothing is ever printed. Predict what child.greet() returns, then fix it so the log line is reached.
const assert = require('assert');
const base = {
greet() {
return "Hello, I am " + this.name;
},
};
const child = Object.create(base);
child.name = "Rex";
// Intent: give child its OWN greet that adds "Woof! " in front of the
// inherited greeting.
child.gret = function () {
return "Woof! " + base.greet.call(this);
};
const greeting = child.greet();
assert.strictEqual(greeting, "Woof! Hello, I am Rex", 'got ' + greeting);
console.log(greeting);Expected output: Woof! Hello, I am Rex
Building an Inheritance Hierarchy
You can chain prototypes to model real-world relationships. Here is a small example that shows how a hierarchy works using Object.create:
const vehicle = {
init(make, year) {
this.make = make;
this.year = year;
return this;
},
describe() {
return this.year + " " + this.make;
}
};
const electricVehicle = Object.create(vehicle);
electricVehicle.setBattery = function(kwh) {
this.batteryCapacity = kwh;
return this;
};
electricVehicle.range = function() {
return this.make + " has " + this.batteryCapacity + " kWh battery";
};
const myTesla = Object.create(electricVehicle);
myTesla.init("Tesla Model 3", 2024);
myTesla.setBattery(75);
console.log(myTesla.describe());
console.log(myTesla.range());
// The chain: myTesla -> electricVehicle -> vehicle -> Object.prototype
console.log(myTesla.hasOwnProperty("make"));
console.log(myTesla.hasOwnProperty("describe"));
Object Utility Methods
JavaScript provides built-in methods for working with objects that are essential in everyday programming:
// Object.assign - copy properties
const defaults = { theme: "light", lang: "en", debug: false };
const userPrefs = { theme: "dark", fontSize: 16 };
const settings = Object.assign({}, defaults, userPrefs);
console.log("Merged settings:", settings);
// Spread operator (modern alternative)
const settingsSpread = { ...defaults, ...userPrefs };
console.log("Spread settings:", settingsSpread);
// Object.freeze - make object immutable
const config = Object.freeze({ apiUrl: "https://api.example.com", version: 2 });
config.version = 3; // Silently fails (no error in non-strict mode)
console.log("Frozen config:", config);
// Object.keys / Object.values / Object.entries for iteration
const scores = { alice: 95, bob: 82, charlie: 91 };
Object.entries(scores).forEach(function(entry) {
console.log(entry[0] + " scored " + entry[1]);
});
Recall
Without scrolling up: you have an object { alice: 95, bob: 82, charlie: 91 } and you want the names of everyone who scored above 90, as an array. Which combination gets you there — and why does it work?
Try It Yourself
Reading about constructors and prototypes is not the same as building with them. This is a build task: a small program that reports its own pass/fail. You are given a list of track lengths and a Playlist type to finish — one constructor and two prototype methods. Run it as-is and it fails immediately, telling you which piece is still missing. Implement each one until every check passes and it prints All checks passed.
The three pieces reuse exactly what this lesson taught: a constructor that sets each instance's own data with this.property = value, and methods placed on Playlist.prototype so every instance shares one copy. The final check even proves that sharing (playlist.add === other.add). The starter already has the data, the stubs, and the checks — you write only the logic inside each one. Nothing above spells out all three answers, so you will have to assemble them yourself.
Build
Finish the build. One constructor and two prototype methods are stubbed out, and the checks below them fail until each returns the right value. Run it as-is to see which check fails first, decide what that piece is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.
const assert = require('assert');
// The data: a stack of song lengths in seconds. Do NOT change this array.
const trackLengths = [210, 185, 240, 200];
// TODO 1: constructor. Set this.name to name, this.tracks to a NEW empty array,
// and this.totalSeconds to 0. Called with new Playlist("Focus").
// new Playlist("Focus").name -> "Focus"; new Playlist("Focus").tracks -> []
function Playlist(name) {
// your code here
}
// TODO 2: prototype method. Push the length onto this.tracks and add it to
// this.totalSeconds. Return this so calls can chain.
// p.add(210).add(185); p.tracks -> [210, 185]; p.totalSeconds -> 395
Playlist.prototype.add = function (seconds) {
// your code here
};
// TODO 3: prototype method. Return the average track length as a number.
// Assume at least one track. adding all of [210, 185, 240, 200] -> 208.75
Playlist.prototype.averageLength = function () {
// your code here
};
// --- Build checks: these must all pass. Do not edit below this line. ---
const playlist = new Playlist("Focus");
assert.strictEqual(
playlist.name,
"Focus",
"new Playlist(name) should store name as the instance's own property",
);
assert.deepStrictEqual(
playlist.tracks,
[],
"new Playlist(name) should give each instance its own empty tracks array",
);
for (const length of trackLengths) {
playlist.add(length);
}
assert.deepStrictEqual(
playlist.tracks,
[210, 185, 240, 200],
"add(seconds) should push each length onto this.tracks in order",
);
assert.strictEqual(
playlist.totalSeconds,
835,
"add(seconds) should accumulate this.totalSeconds as tracks are added",
);
assert.strictEqual(
playlist.averageLength(),
208.75,
"averageLength() should return the mean track length (208.75)",
);
const other = new Playlist("Chill");
assert.strictEqual(
playlist.add === other.add,
true,
"add should be one shared prototype method, not copied onto each instance",
);
const chainCheck = new Playlist("chain");
assert.strictEqual(
chainCheck.add(1),
chainCheck,
"add(seconds) should return this so calls can chain",
);
console.log("All checks passed.");
console.log("Playlist:", playlist.name);
console.log("Tracks:", playlist.tracks);
console.log("Total seconds:", playlist.totalSeconds);
console.log("Average length:", playlist.averageLength());Expected output: All checks passed.
Playlist: Focus
Tracks: [ 210, 185, 240, 200 ]
Total seconds: 835
Average length: 208.75
Once it passes, try two variations and predict each before running:
- Drop the running total. In
add, keep thethis.tracks.push(seconds)but delete thethis.totalSeconds += secondsline, then predict which check breaks first. ThetotalSecondsassert fires —0 !== 835— because the instance's own running total never accumulates, even though the tracks array still fills correctly. An instructive assert failure that isolates own accumulated data from own array data. - One more track before the logs. After the checks pass, add
playlist.add(300)just above theconsole.loglines and predict the three echoed numbers. Tracks becomes[ 210, 185, 240, 200, 300 ], total climbs to1135, and the average drops to227— the constructor's own mutable data updated by one moreadd. This changes the echoed output, not any check.
Capstone milestone
Milestone — the Task model. The capstone's Task is a constructor with methods on its prototype (complete/reopen) and its data as own properties (title, status). The Playlist you just built is the same shape. Confirm you can build a constructor-plus-prototype model.
Hint: You don't need the full capstone yet — this confirms the constructor-plus-prototype skill the Task model is built on. In the capstone, new Task(title) sets own data and Task.prototype.complete() is a shared method, exactly like the Playlist build above.
- Wrote a constructor function that sets each instance's own data with this.property = value
- Put shared methods on the constructor's .prototype (not inside the constructor body)
- Created instances with new and called their prototype methods
- Confirmed instances SHARE prototype methods (playlist.add === other.add is true)
Key Takeaways
- Object literals, shorthand syntax, and computed property names make object creation flexible
- The
thiskeyword inside a method refers to the object the method was called on - Constructor functions with
newcreate multiple objects of the same shape - Every object has a prototype, and JavaScript follows the prototype chain to look up properties
Object.createlets you set the prototype explicitly to build inheritance hierarchiesObject.assign, the spread operator, andObject.freezeare everyday tools for working with objects
Next Steps
Now that you understand how objects and prototypes work, the next lesson covers closures and scope, two fundamental concepts that control where variables are accessible in your code and enable powerful patterns like data privacy.
Pro Tip: Modern JavaScript's
classsyntax is a cleaner way to work with prototypes — it uses the same prototype system underneath, though it also adds a few things plain functions can't do (like truly private#fields). Understanding prototypal inheritance first makes classes much easier to grasp because you'll know what's happening behind the scenes.
Next lesson
Closures and Scope
Understand JavaScript closures and scope. Learn lexical scoping, the module pattern, IIFEs, and practical closure patterns for data privacy.
24 min