this keyword;Object.keys, Object.values, Object.entries, plus copying and merging;Learn JS Series):As always, we start with the worked solutions to last episode's three exercises. Do not just read them -- type them out, run them, and compare against your own attempt. That comparison is where the learning actually happens.
Exercise 1 - queue operations:
const queue = ["a", "b", "c"];
queue.push("d"); // mutates: ["a","b","c","d"]
const removed = queue.shift(); // mutates: removes "a"
console.log(queue, removed); // ["b","c","d"] "a"
The insight: both push and shift mutate the array in place; shift additionally returns the element it removed, which is why we can capture it in removed.
Exercise 2 - filter then map:
const temps = [15, 22, 8, 30, 19];
const warm = temps.filter((t) => t > 18).map((t) => `${t} C`);
console.log(warm); // ["22 C", "30 C", "19 C"]
The insight: because filter returns a new array, you can chain map straight onto it, a tiny pipeline that reads top to bottom -- keep the warm ones, then label each one.
Exercise 3 - max via reduce:
const nums = [4, 9, 2, 15, 7];
const max = nums.reduce((best, n) => (n > best ? n : best), -Infinity);
console.log(max); // 15
The insight: the accumulator best carries the largest value seen so far, and each step keeps whichever is bigger. Starting at -Infinity guarantees the very first real number wins the comparison.
Right, on to today. Arrays, which we covered last time, are for ordered lists -- "give me the thing at position 2". But when you want to describe a single thing with named attributes -- a user, a car, a blog post -- you reach for an object. Objects are the other half of JavaScript's data world, and honestly you will use them just as much as arrays.
An object is a collection of key-value pairs, written with curly braces. The keys (also called properties) are strings; the values can be anything at all, including numbers, strings, arrays, other objects, and even functions:
const user = {
name: "scipio",
level: 7,
isActive: true,
};
console.log(user.name); // "scipio"
console.log(user.level); // 7
An object models a real-world entity nicely: a user has a name, has a level, has an active flag. Where an array asks "what is at index 2?", an object asks "what is the value of name?". The data is addressed by meaningful labels rather than by numeric position, and that is exactly what you want when the pieces are not naturally ordered.
A quick note on those keys. Even when you write them without quotes (like name above), they are stored as strings under the hood. You can quote them, and you must quote them when the key contains something unusual like a space or a hyphen:
const config = {
host: "localhost",
"max-retries": 3, // hyphen -> must be quoted
"user agent": "test", // space -> must be quoted
};
console.log(config.host); // "localhost"
console.log(config["max-retries"]); // 3
That already hints at the next topic: there are two different ways to reach into an object, and one of them is not optional.
There are two ways to read and write a property. Dot notation is the common, readable one you saw above:
const car = { brand: "generic", speed: 120 };
console.log(car.brand); // "generic"
car.speed = 130; // change an existing property
car.color = "blue"; // add a NEW property just by assigning to it
console.log(car); // { brand: 'generic', speed: 130, color: 'blue' }
Notice that adding a property is nothing special -- you assign to a key that does not exist yet, and it springs into being. There is no "declare the shape first" step like you would find in a stricter language. Objects in JavaScript are open for business at all times.
Bracket notation uses a string inside [ ]. It looks clunkier, but it can do two things dot notation simply cannot: use a key that is computed at runtime, and use a key that is not a valid identifier:
const obj = { "first name": "scipio", level: 7 };
console.log(obj["first name"]); // brackets required: the key has a space
console.log(obj.level); // dot works fine for a normal key
const key = "level"; // a key decided at runtime
console.log(obj[key]); // 7 - the variable's VALUE is used as the key
// console.log(obj.key); // ERROR (runtime): undefined - looks for a literal "key"
That last pair is the important bit. obj[key] looks up the value of the variable key (which is the string "level") and uses that as the property name. Writing obj.key instead would look for a property literally named "key", which does not exist, so you would get undefined. Getting these two confused is a classic beginner trap.
The rule of thumb: use dot notation by default, and switch to brackets when the key lives in a variable, is computed on the fly, or contains characters like spaces or hyphens. That obj[someVariable] trick -- choosing which property to read based on a value you only know at runtime -- is everywhere in real code, especially when data arrives from an API or a form.
Remove a property with the delete operator, and test whether a property exists with the in operator:
const account = { user: "scipio", token: "abc123" };
delete account.token;
console.log(account); // { user: 'scipio' }
console.log("user" in account); // true
console.log("token" in account); // false
console.log(account.token); // undefined
Now, why prefer "key" in obj over checking obj.key === undefined? Because a property could genuinely be set to the value undefined, and in that case the two checks disagree with each other:
const settings = { theme: undefined };
console.log(settings.theme === undefined); // true - but the key DOES exist!
console.log("theme" in settings); // true - the honest answer
console.log("missing" in settings); // false
The in operator asks the one question that matters: does this key exist at all, regardless of what value it holds? That distinction saves you from real bugs when undefined is a legitimate value in your data.
There is also Object.hasOwn(obj, key), a newer and safer cousin of in. The difference is that in also finds inherited properties (from the object's prototype chain, which we will unpack in a later phase), whereas Object.hasOwn only checks the object's own properties:
const point = { x: 1, y: 2 };
console.log(Object.hasOwn(point, "x")); // true - its own property
console.log(Object.hasOwn(point, "toString")); // false - that is inherited
console.log("toString" in point); // true - in sees inherited too
For most day-to-day checks either one is fine, but when you are looping over data you did not create, Object.hasOwn is the more precise tool.
Modern JavaScript gives you two conveniences you will use daily. First, when you already have variables whose names match the property names you want, shorthand lets you skip the repetition:
const name = "scipio";
const level = 7;
const user = { name, level }; // same as { name: name, level: level }
console.log(user); // { name: 'scipio', level: 7 }
This comes up constantly, because so often you have a handful of local variables and want to bundle them into one object to return or pass along. Writing { name: name } felt silly, so the language let us drop the redundant half.
Second, computed property names let you build a key from an expression right inside the object literal, using the same brackets as before:
const field = "score";
const record = {
id: 1,
[field]: 95, // the key becomes "score"
[`${field}_max`]: 100, // the key becomes "score_max"
};
console.log(record); // { id: 1, score: 95, score_max: 100 }
Before this feature existed, you had to create the object first and then add the dynamic key on a separate line with bracket assignment. Being able to do it inline keeps related code together and reads more clearly.
Here is where objects earn their keep. Objects and arrays combine freely, and that is how you represent real, structured data. An object can hold arrays, which hold objects, as deep as you like:
const blogPost = {
title: "Learn JS",
author: { name: "scipio", verified: true },
tags: ["javascript", "programming"],
stats: { views: 1200, votes: 45 },
comments: [
{ user: "ada", text: "great post" },
{ user: "linus", text: "clear examples" },
],
};
console.log(blogPost.author.name); // "scipio"
console.log(blogPost.tags[0]); // "javascript"
console.log(blogPost.stats.views); // 1200
console.log(blogPost.comments[1].user); // "linus"
You navigate by chaining accessors: blogPost.comments[1].user walks into the comments array, grabs element 1, then reads its user property. This nested shape is exactly what JSON (which we cover a bit later) represents, and it is how almost all real data arrives in your programs -- from web APIs, config files, databases, you name it.
One caution while nesting: if you try to read through a property that does not exist, you get an error, not a friendly undefined:
const data = { user: { name: "scipio" } };
console.log(data.user.name); // "scipio"
// console.log(data.account.id); // ERROR (runtime): cannot read 'id' of undefined
console.log(data.account?.id); // undefined - optional chaining, safe
We met the optional chaining operator ?. back in episode 4; here it shows its real value. data.account?.id says "if account is missing, just give me undefined instead of crashing". When you walk into deeply nested data whose shape you are not 100% sure of, ?. is your seatbelt.
A property whose value is a function is called a method. This lets an object bundle behaviour right alongside its data:
const counter = {
count: 0,
increment() { // method shorthand
this.count += 1; // 'this' refers to the object
return this.count;
},
reset() {
this.count = 0;
},
};
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
counter.reset();
console.log(counter.count); // 0
Inside a method, the keyword this refers to the object the method was called on, so this.count reaches that object's own count. Notice the increment() shorthand too -- it is the modern way to write increment: function () { ... }, and it reads much cleaner.
Now, this has real subtleties (it gets a whole dedicated episode in the next phase, because it trips up even experienced developers), so for now just hold on to the simple pattern: a method is a function stored on an object, and it reaches its own object through this. That is enough to be productive today; the sharp edges can wait.
Objects are not directly iterable with for...of (that loop is for arrays and other list-like things), so to walk an object you first turn it into arrays with three standard helpers:
const scores = { math: 90, science: 85, art: 78 };
console.log(Object.keys(scores)); // ["math", "science", "art"]
console.log(Object.values(scores)); // [90, 85, 78]
console.log(Object.entries(scores)); // [["math",90],["science",85],["art",78]]
for (const [subject, score] of Object.entries(scores)) {
console.log(`${subject}: ${score}`);
}
// math: 90
// science: 85
// art: 78
Object.keys gives you the property names, Object.values gives the values, and Object.entries gives [key, value] pairs -- perfect for a for...of loop with array destructuring (which we saw in episode 11). These three are your bread-and-butter tools for looping over an object's contents, and because they hand you real arrays, you can pipe them straight into map, filter, and reduce from last episode:
const scores = { math: 90, science: 85, art: 78 };
const passing = Object.entries(scores)
.filter(([, score]) => score >= 80) // keep 80 and up
.map(([subject]) => subject); // keep only the names
console.log(passing); // ["math", "science"]
const average =
Object.values(scores).reduce((sum, n) => sum + n, 0) / Object.values(scores).length;
console.log(average); // 84.333...
See how the two data structures work together? An object turns into an array of entries, you run your array pipeline, and you get an answer. That interplay is a huge part of everyday JavaScript.
One thing that surprises newcomers: objects are handled by reference. Assigning an object to a new variable does not copy it -- both names point at the very same object, so a change through one is visible through the other:
const original = { a: 1, b: 2 };
const alias = original; // NOT a copy - same object, two names
alias.a = 99;
console.log(original.a); // 99 - the "original" changed too!
To get an actual separate copy, spread the object into a fresh literal with ... (the same spread we used for arrays). This also gives us a clean way to merge objects and to override selected fields:
const base = { a: 1, b: 2 };
const copy = { ...base }; // a real shallow copy
copy.a = 99;
console.log(base.a); // 1 - the original is safe now
const defaults = { theme: "light", fontSize: 14 };
const userPrefs = { theme: "dark" };
const merged = { ...defaults, ...userPrefs }; // later keys win
console.log(merged); // { theme: 'dark', fontSize: 14 }
That merge pattern -- start with defaults, spread the user's choices on top so their values override -- is one of the most common idioms in the whole language. The rule is "last one wins": when the same key appears twice, the rightmost spread takes precedence. Object.assign(target, ...sources) does the same job and you will still see it in older code, but the spread form is what most people reach for today.
One honest warning, the same one I gave for arrays: { ...base } is a shallow copy. Top-level properties are copied, but if a value is itself an object, both copies still share that inner object. We will tackle deep copying properly in a later episode -- for now, just know that spread copies one level deep, which is exactly right most of the time and quietly wrong when your data is nested.
Quite some of you arrived here from the Learn Python Series, and a few from Rust, so a glance sideways is worth it. It shows which of JavaScript's choices are universal and which are its own flavour.
Python has two things that together cover what a JS object does. A dictionary is the direct match for key-value data -- ordered (since 3.7), mutable, addressed by key. The main difference is syntax and access style: Python uses d["key"] almost everywhere (there is no dot-access for dict keys), whereas JS prefers the dot:
# Python: a dict is the closest cousin to a JS object
user = {"name": "scipio", "level": 7}
print(user["name"]) # "scipio" - bracket access is the norm
user["active"] = True # add a key by assignment
print("name" in user) # True - same membership test as JS 'in'
print(list(user.items())) # like Object.entries
Python's .keys(), .values(), and .items() line up almost one-to-one with Object.keys, Object.values, and Object.entries. The place they part ways: when Python wants methods bundled with data, it uses a class, not a dict, whereas JS is happy to drop a function straight into an object literal.
Rust is stricter, and splits the idea in two on purpose. When the set of fields is known ahead of time, you use a struct -- a fixed, named, type-checked shape. When the keys are dynamic and only known at runtime, you use a HashMap. JavaScript blurs this line: one object type serves both roles, which is convenient but means the compiler cannot catch a typo in a property name for you:
use std::collections::HashMap;
fn main() {
// struct: fixed, known fields, checked at compile time
struct User { name: String, level: u32 }
let u = User { name: String::from("scipio"), level: 7 };
println!("{} {}", u.name, u.level);
// HashMap: dynamic keys, decided at runtime
let mut scores = HashMap::new();
scores.insert("math", 90);
println!("{:?}", scores.get("math")); // Some(90)
}
Notice scores.get("math") returns Some(90) rather than a bare value -- Rust forces you to handle the "key not present" case explicitly, where JavaScript just hands you undefined and trusts you to remember. Different philosophies: Rust wants the missing-key case impossible to ignore, JS wants to stay out of your way.
Go takes yet another split. It has a map for dynamic key-value data and a struct for fixed shapes, much like Rust, and both are statically typed. A Go map access can even tell you whether the key was present via a second return value:
func main() {
scores := map[string]int{"math": 90, "science": 85}
value, ok := scores["math"]
fmt.Println(value, ok) // 90 true
_, missing := scores["art"]
fmt.Println(missing) // false - key was not there
}
The throughline: every serious language gives you some form of key-value map, but they differ on how much they hand you for free versus how much they force you to declare and check up front. JavaScript sits at the loose, flexible end -- one object type does everything, keys are dynamic, missing keys read as undefined instead of raising an error. That flexibility is why objects feel so effortless in JS, and also why a little discipline (consistent shapes, in/Object.hasOwn checks, ?. when walking uncertain data) pays off as programs grow.
book with properties title, pages, and author (itself an object with a name). Read and print the author's name via chained dot access, then add a new property published: 2026 by assignment and print the whole object.const settings = { theme: "dark", "font size": 14 }, read and print both properties. In a comment, explain why one of them forces you to use bracket notation while the other does not.const inventory = { apples: 3, pears: 0, plums: 7 }, use Object.entries together with a loop (or filter and map) to print only the items whose count is greater than zero, one per line, formatted as "apples: 3".delete, and test existence with "key" in obj (safer than === undefined) or Object.hasOwn (own properties only).{ name }) and computed keys ({ [field]: value }) cut repetition.?. to walk uncertain shapes without crashing.this refers to that object.Object.keys, Object.values, and Object.entries, then feed the results into your array pipeline.{ ...obj } and merge with { ...defaults, ...overrides } (last key wins) -- but remember it is a shallow copy.That wraps up the data structures of Phase 1: you now know how JavaScript stores single values, how it stores ordered lists, and how it stores named key-value data. Next episode we close out these foundations by confronting the thing that surprises newcomers most -- truthiness, and the exact rules behind == versus === -- so you never get bitten by a comparison again. ;-)