JavaScript Intermediate
A Complete 7-Chapter Course
Table of Contents
- Object.keys/values/entries and the Spread/Rest Operators
- Closures
- ES6 Classes
- this In Depth
- Error Handling
- Array/Object Copying
- Local Storage and JSON
Object.keys/values/entries and the Spread/Rest Operators
Fundamentals Chapter 7 flagged that Object.keys() turns an object's property names into a real array β meaning Chapter 6's map/filter/reduce can be used on object data too. This chapter covers that properly, plus the spread (...) and rest (...) operators, which look identical but do opposite jobs depending on where they appear.
Object.keys, Object.values, Object.entries
Each of these returns a real array, unlocking everything from Chapter 6. entries() is the most useful for loops β each item is a [key, value] pair, which destructures neatly (covered below).
Combining Object.values with reduce
This is the cleaner version of the Fundamentals Chapter 7 inventory challenge, which used a manual for...in loop with a separate running-total variable β here, reduce does both jobs in one expression.
Array Destructuring
Destructuring unpacks array elements straight into named variables in one line. for...of (a cousin of the for...in from Fundamentals Chapter 4/7) pairs naturally with entries(), since each entry is itself a two-element array ready to destructure.
Object Destructuring
Object destructuring pulls named properties straight out into variables of the same name β far shorter than writing person.name and person.city on separate lines, especially useful for function parameters (see below).
The Spread Operator (...) β Expanding a Collection
Spread "unpacks" an array or object's contents into a new one. { ...original, age: 36 } copies every property from original, then overwrites age β a clean way to update one field without mutating the original object, the same non-mutating spirit as map/filter from Chapter 6.
The Rest Parameter (...) β Collecting the Leftovers
The exact same ... syntax, in a function's parameter list, does the opposite job: it gathers any number of arguments into a single real array (numbers), rather than expanding something. This is what lets sum() accept any number of arguments instead of a fixed count.
... in an array/object literal or function call is spread (expanding). ... in a function's parameter definition is rest (collecting). There's no separate keyword β only where it appears tells you which one it is.
| Tool | Input | Output |
|---|---|---|
| Object.keys(obj) | Object | Array of property names |
| Object.values(obj) | Object | Array of values |
| Object.entries(obj) | Object | Array of [key, value] pairs |
| { ...obj, key: val } | Object | New object, copied + overridden |
| function f(...args) | Any number of arguments | A single real array, args |
Coding Challenges
Given const stock = { apples: 10, bananas: 5, oranges: 8 }, use Object.entries and a for...of loop with destructuring to log each item as "item: quantity", then use Object.values combined with reduce to log the total quantity.
π View solutionGiven const settings = { theme: "dark", fontSize: 14, notifications: true }, use the spread operator to create a new object updatedSettings with fontSize changed to 16, without modifying the original settings object. Log both objects to confirm settings is unchanged.
π View solutionWrite a function describeTeam(captain, ...players) that logs the captain's name on its own line, then logs every remaining player using a rest parameter and forEach. Call it with at least 4 names total.
π View solutionChapter 1 Quick Reference (Intermediate)
- Object.keys/values/entries β convert an object into a real array of names/values/pairs
- Array/object destructuring β unpack values directly into named variables
- for...of + Object.entries() β idiomatic way to loop over an object with both key and value
- Spread (...) in a literal/call β expands a collection's contents (copying, merging, overriding)
- Rest (...) in a parameter list β collects multiple arguments into one real array
- Spread never mutates the original β same non-mutating principle as map/filter
- Next chapter: closures β how a function can "remember" variables from where it was created
Closures
Fundamentals Chapter 5 showed a nested function reading a variable from its enclosing function, and flagged that this "remembering" behaviour is called a closure, promising full coverage later β this is that chapter. A closure is simply a function that keeps access to variables from the scope it was created in, even after that outer scope has technically finished running.
The Basic Closure
makeGreeter finishes running and returns the inner function β but that inner function still remembers whatever greeting was at the moment it was created. sayHello and sayHi are two completely separate closures, each with its own private copy of greeting, even though both were built by the exact same makeGreeter function.
This Is Exactly Fundamentals Chapter 6's makeMultiplier Challenge
That earlier challenge asked why the inner function could still access factor after makeMultiplier had already finished β the answer is exactly this chapter's topic. The returned function "closes over" factor, carrying its own private reference to it for as long as the returned function itself exists.
A Closure Holds a Reference, Not a Snapshot
Each call to the returned function mutates the SAME count variable β it isn't reset, and it isn't a frozen copy taken at creation time. The closure remembers the actual variable, live, the same way two object references (Fundamentals Chapter 7) point at the same underlying object rather than separate copies.
const counterA = makeCounter(); const counterB = makeCounter(); creates two separate count variables β one per call to makeCounter() β so calling counterA() never affects counterB()'s count. Each invocation of the outer function creates a brand-new scope to close over.
The Classic Loop + Closure Pitfall (and Why let Fixes It)
This is the single most famous closure-related bug in JavaScript. With var (Fundamentals Chapter 2's "avoid it" keyword), there's only ever one shared i across the entire loop β by the time any of the three setTimeout callbacks actually runs, the loop has already finished and i is 3. let creates a genuinely new i binding for each iteration, so each callback's closure captures its own separate value.
Practical Use: Private State with a Module-Style Pattern
balance is never a property on the returned object β there is no account.balance to read or overwrite directly. The only way to affect it is through the three methods that closed over it, giving genuine privacy with nothing extra needed: no special keyword, no class syntax (Intermediate Chapter 3) β just an ordinary closure.
addEventListener callback from Fundamentals Chapter 8 is itself a closure β it routinely reaches back to variables declared outside the handler (a counter, a piece of state) and keeps working correctly across repeated clicks, for exactly the same underlying reason as makeCounter above.
Coding Challenges
Write a function makePowerOf(exponent) that returns a function taking a base number and returning base raised to that exponent (use ** for exponentiation). Create square = makePowerOf(2) and cube = makePowerOf(3), then test both with the same input.
π View solutionWrite a function makeIdGenerator() that returns a function with no parameters, returning a new sequential ID each time it's called (starting at 1). Create two SEPARATE generators and call each one a few times to prove their counts don't interfere with each other.
π View solutionWrite a function createInventory() returning an object with addItem(name, qty), removeItem(name), and getCount(name) methods, all closing over a private object that the caller can never access directly. Add a few items, remove one, and confirm getCount reflects the changes.
π View solutionChapter 2 Quick Reference
- Closure β a function that retains access to variables from its creation scope, even after that scope has "finished"
- Each call to the outer function creates a fresh, independent set of variables to close over
- A closure references the real variable, not a frozen snapshot β mutations are visible across calls
- var in a loop + closures = classic bug; let gives each iteration its own binding, fixing it
- Private state pattern: variables declared inside an outer function, exposed only via returned methods
- Event listeners are closures β they routinely reach back to outer variables across repeated events
- Next chapter: ES6 classes β constructors, methods, and inheritance with extends
ES6 Classes
Fundamentals Chapter 7 built objects by hand, one at a time, each with its own copy of methods like greet() defined inline. class syntax (added in the same 2015 update as let/const) provides a blueprint for creating many similarly-shaped objects, plus a built-in way to extend one type's behaviour from another β JavaScript's equivalent of inheritance.
Defining a Class
constructor(...) runs automatically whenever new Person(...) is called β its job is to set up the new object's starting properties via this. Every other method (greet here) is defined directly inside the class body, with no function keyword and no commas between methods β a more compact form than Fundamentals Chapter 7's object-literal method syntax, but functionally similar underneath.
class is mostly a cleaner syntax over JavaScript's existing object/prototype system β typeof Person actually returns "function". The new keywords (class, constructor, extends) exist purely for readability; they don't introduce a fundamentally different kind of object.
Multiple Instances, Each With Their Own Data
Each new Person(...) call produces a completely independent object with its own name/age β but all instances share the exact same greet method definition, stored once rather than duplicated per object, which is more memory-efficient than the object-literal approach from Chapter 7 at scale.
Inheritance with extends
class Student extends Person makes Student a more specific version of Person β it automatically gets every method Person has (greet), plus whatever new methods it adds itself (study). super(name, age) must be called before this can be used at all inside a subclass constructor β it runs the parent class's constructor logic first, so this.name/this.age get set up correctly before Student adds its own school property.
constructor, it MUST call super(...) before referencing this anywhere β JavaScript throws a ReferenceError immediately otherwise. If a subclass needs no extra setup at all, its constructor can simply be omitted entirely; the parent's constructor runs automatically in that case.
Overriding a Method
Defining greet again inside Student replaces (overrides) the inherited version for any Student instance. super.greet() inside the override still reaches back to Person's original method β useful when the subclass wants to extend, not completely replace, the parent's behaviour.
Getters β Computed Properties That Look Like Plain Fields
get area() defines a method that's accessed WITHOUT parentheses β rect.area, not rect.area() β recalculated fresh every time it's read. Useful for values that are always derivable from existing properties (width Γ height) rather than stored separately, avoiding the risk of them drifting out of sync.
| Concept | Fundamentals Ch 7 (object literal) | This chapter (class) |
|---|---|---|
| Creating an instance | { name: ..., greet() {...} } | new Person(name) |
| Method sharing | Duplicated per object | Shared once across all instances |
| Extending behaviour | No built-in mechanism | class Sub extends Base |
| Computed property | A method called with () | get propertyName() β read without () |
Coding Challenges
Define a class Animal with a constructor taking name, and a method speak() logging "{name} makes a sound." Create two Animal instances with different names and call speak() on each.
π View solutionDefine a class Dog that extends Animal from Challenge 1, overriding speak() to log "{name} barks." instead, while still calling the parent's speak() first using super.speak(). Create a Dog instance and call speak() on it.
π View solutionDefine a class Circle with a constructor taking radius, and a getter area returning the circle's area (Ο Γ radiusΒ², using 3.14159 for Ο) and a getter circumference returning 2 Γ Ο Γ radius. Create an instance and log both computed properties.
π View solutionChapter 3 Quick Reference
- class Name { constructor(...) {...} method() {...} } β basic class shape
- new ClassName(...) β creates an instance, running the constructor
- class Sub extends Base β inherits all of Base's methods automatically
- super(...) β calls the parent constructor; required before using this in a subclass constructor
- super.method() β calls the parent's version of an overridden method
- get propertyName() {...} β a method read without parentheses, like a plain property
- class is mostly syntax over JavaScript's existing object/prototype system
- Next chapter: this in depth β call/apply/bind, and arrow vs regular function context
this In Depth
Fundamentals Chapter 5 and 7 both flagged this behaviour without fully resolving it: regular functions get their own this determined by how they're called; arrow functions inherit it from their surrounding scope. This chapter makes that precise, covers the situations where this goes wrong, and introduces three methods β call, apply, bind β that control it directly.
The Real Rule: this Depends on HOW a Function Is Called
The exact same function, called two different ways, gets a different this. person.greet() sets this to person, because it's called through person. Once that same function is assigned to greetFn and called on its own, there's no object to its left at the call site β this falls back to undefined (in strict mode/modules) rather than person.
button.addEventListener("click", person.greet) (Fundamentals Chapter 8) hands the function over to be called plain later β by the time the browser actually calls it, this is no longer person. This is the single most common real-world "this" bug.
call() and apply() β Explicitly Setting this for One Call
call and apply run a function immediately, with the first argument forced into the role of this for that one call β regardless of how the function was originally defined or attached. They're functionally identical except for how the remaining arguments are passed: call lists them one by one, apply takes them as a single array (similar in spirit to the spread operator from Intermediate Chapter 1).
bind() β Permanently Locking this for Later
Unlike call/apply, bind doesn't run the function immediately β it returns a brand-new function with this permanently locked to whatever was passed in, no matter how that new function later gets called. This is the standard fix for the callback problem above: bind the method to its object before handing it off as a callback.
Arrow Functions: No Own this, Inherited Instead
Had the setInterval callback been a regular function instead, this inside it would be undefined (plain-call rule from earlier), breaking this.seconds++ entirely. The arrow function instead has no this of its own β it transparently uses whatever this was already in scope at the point it was written, which is timer, since start() itself was called as timer.start().
function for object methods that need their own this (set by however they're called). Use an arrow function for callbacks nested inside a method, specifically so they inherit the surrounding this instead of losing it.
Why Arrow Functions Can't Be Fixed with bind/call/apply
Since an arrow function never has its own this to begin with, call/apply/bind have nothing to override β they're silently ignored for the this argument specifically. This is the one situation where those three methods simply don't apply.
| Method | Runs immediately? | Sets this for |
|---|---|---|
| fn.call(obj, a, b) | Yes | That one call, args listed individually |
| fn.apply(obj, [a, b]) | Yes | That one call, args as an array |
| fn.bind(obj) | No β returns a new function | Every future call of the returned function |
Coding Challenges
Write a plain function introduce(role) that logs `${this.name} works as a ${role}`. Create two different objects, each with a name property, and use call() to invoke introduce with each object as this, passing a different role string each time.
π View solutionCreate an object counter with a count property (0) and a method increment() that increases count by 1 and logs it. Extract increment as a standalone function, use bind() to lock it to counter, then call the bound version three times via setTimeout to prove this stays correct even when called asynchronously.
π View solutionCreate an object stopwatch with elapsed: 0 and a method start() that uses setInterval with an ARROW function to increment elapsed and log it every second, for 3 seconds (then stop with clearInterval). Confirm elapsed actually increases by logging it.
π View solutionChapter 4 Quick Reference
- this depends on the call site β obj.method() sets this to obj; a plain call doesn't
- fn.call(obj, a, b) β runs fn immediately with this = obj, args listed individually
- fn.apply(obj, [a, b]) β same as call, but args passed as an array
- fn.bind(obj) β returns a NEW function with this permanently locked to obj
- Regular functions get their own this; arrow functions inherit this from their surrounding scope
- call/apply/bind have no effect on an arrow function's this β there's nothing to override
- Passing a method as a bare callback loses its this β bind it first, or use an arrow wrapper
- Next chapter: error handling β try/catch/finally and custom Error classes
Error Handling
Fundamentals Chapter 10 wrapped await fetch(...) in try/catch without explaining the mechanism fully. This chapter covers throw, the complete try/catch/finally structure, and β using Chapter 3's class/extends syntax β how to build custom error types carrying more information than a plain message string.
throw β Raising an Error Deliberately
throw new Error("message") immediately stops the current function (and everything that called it) from continuing normally β unlike Go's error return values (a deliberate contrast worth knowing if the Go course is ever revisited), JavaScript errors propagate upward automatically until something explicitly catches them.
try/catch β Catching a Thrown Error
Code inside try { } runs normally until something throws β at that exact point, execution jumps straight into catch (error) { }, skipping anything remaining in the try block. error.message holds the text passed to new Error(...); every error also has a name property ("Error" by default).
finally β Runs No Matter What
finally { } runs after try/catch complete, regardless of whether an error occurred β useful for cleanup work (hiding a loading spinner, closing a connection) that needs to happen either way. This is conceptually similar to Go's defer from the Go Intermediate course, just scoped specifically to error handling rather than every function exit.
Custom Error Classes
class ValidationError extends Error (Chapter 3's inheritance, applied to the built-in Error type) creates a genuinely new error type carrying extra structured data β here, field β alongside the standard message. super(message) must run first, exactly as with any other subclass constructor, to set up Error's own internal behaviour (including the stack trace).
Distinguishing Error Types in a catch Block
error instanceof ValidationError checks whether the caught error is specifically that custom type (or a subclass of it) β this is JavaScript's rough equivalent of Go's errors.As from the Go Advanced course, letting a catch block react differently depending on exactly what went wrong, rather than treating every error identically.
instanceof, a single catch block treats a genuine validation problem the same as an unrelated bug (a typo causing a TypeError, for instance) β both get silently swallowed and handled identically. Checking the specific error type before deciding how to respond avoids masking real bugs as if they were expected validation failures.
| Piece | Purpose |
|---|---|
| throw new Error("msg") | Deliberately raise an error, halting normal execution |
| try { } catch (e) { } | Run code; if it throws, jump to catch instead of crashing |
| finally { } | Always runs after try/catch, error or not β for cleanup |
| class X extends Error | Define a custom error type with extra data fields |
| error instanceof X | Check which specific error type was actually caught |
Coding Challenges
Write a function parsePositiveNumber(value) that throws a plain Error if value is negative or not a number (use isNaN), otherwise returns it. Call it inside a try/catch with a value that triggers the error, logging the caught message.
π View solutionDefine a custom error class InsufficientFundsError extending Error, with a constructor taking (message, shortfall) and storing shortfall. Write a function withdraw(balance, amount) that throws it if amount > balance, including the shortfall (amount - balance). Catch it and log a message using error.shortfall.
π View solutionWrite a function riskyOperation(shouldFail) that throws a RangeError if shouldFail is true, otherwise returns "Success". Wrap a call in try/catch/finally: log the result or the error's name+message in catch, and always log "Operation complete" in finally, regardless of outcome. Call it twice, once with each boolean.
π View solutionChapter 5 Quick Reference
- throw new Error("message") β deliberately raises an error, halting normal flow
- try { } catch (error) { } β catches a thrown error instead of letting it crash the program
- finally { } β always runs after try/catch, error or not
- error.message β the text passed when the error was created; error.name β its type name
- class X extends Error β custom error type; must call super(message) first
- error instanceof X β checks the specific error type caught, for differentiated handling
- Built-in error types: Error, TypeError, RangeError, SyntaxError, and more
- Next chapter: array/object copying β shallow vs deep, and common mutation pitfalls
Array/Object Copying
Intermediate Chapter 1 introduced the spread operator for copying arrays and objects, describing it as non-mutating, "the same spirit as map/filter." This chapter shows exactly where that breaks down: spread only copies one level deep, and anything nested still points at the original.
Reference Types: The Root of the Problem
Arrays and objects are reference types β a variable holding one doesn't hold the actual data, it holds a reference (an address) to where that data lives. const b = a copies the reference, not the array β both variables end up pointing at the exact same underlying array, so mutating through either one is visible through both.
Shallow Copying with Spread
{ ...original } genuinely does create a separate object β for top-level, primitive values (strings, numbers, booleans) this is a complete, independent copy. This is "shallow" because it only copies one level deep; the moment a property's value is itself an object or array, the problem returns.
Where Shallow Copying Breaks Down
Spread copied the address property, but that property's value is itself an object β spread copies the reference to that nested object, not the nested object's contents. copy.address and original.address still point at the exact same inner object, so mutating one mutates both. This is the most common real-world "spread didn't work" bug.
const copy = [...original] creates a new array, but if original contains objects, each element in copy still points at the SAME objects as original β changing a property on copy[0] changes original[0] too. Spreading an array is shallow in exactly the same way as spreading an object.
Deep Copying with structuredClone
structuredClone() is a built-in browser/Node function that performs a true deep copy β it recursively copies every nested object and array, so the result is completely independent of the original at every level, no matter how deeply nested.
JSON.parse(JSON.stringify(obj)), which silently loses things like undefined values and dates.
map() Avoids This Problem Entirely
Combining map (Fundamentals Chapter 6) with object spread inside the callback creates a genuinely new object for each element β since name and age are both primitives here, this shallow approach is entirely sufficient; deep copying is only needed when the nested values are themselves objects/arrays that also need protecting from mutation.
| Operation | Top-level copy? | Nested objects/arrays copied? |
|---|---|---|
| const b = a | No β same reference | No |
| { ...obj } / [...arr] | Yes (shallow) | No β nested values still shared |
| structuredClone(obj) | Yes | Yes β fully independent at every level |
Coding Challenges
Create const original = [1, 2, 3]. Create const sameRef = original (no copy) and const shallowCopy = [...original]. Push 4 onto sameRef and 5 onto shallowCopy, then log original, sameRef, and shallowCopy to show the different effects.
π View solutionCreate const settings = { theme: "dark", display: { brightness: 80 } }. Create a shallow copy with spread, change the copy's display.brightness to 50, then log the original's display.brightness to demonstrate the shallow-copy bug from this chapter.
π View solutionRepeat Challenge 2, but use structuredClone instead of spread to create the copy. Change the copy's display.brightness to 50, then log the original's display.brightness to confirm it's now unaffected.
π View solutionChapter 6 Quick Reference
- const b = a (arrays/objects) β copies the reference only; both point at the same data
- { ...obj } / [...arr] β shallow copy; top level is independent, nested values are NOT
- structuredClone(obj) β true deep copy; every level is fully independent
- structuredClone cannot clone functions or DOM elements β ordinary data only
- map() + spread per item β a common, sufficient pattern when elements only hold primitives
- Mutating a nested shared reference is the most common real-world "spread didn't work" bug
- Next chapter: local storage and JSON β persisting data in the browser between page loads
Local Storage and JSON
Every variable in every chapter so far has vanished the instant the page reloads. localStorage is a browser-provided key/value store that survives refreshes, browser restarts, even the computer rebooting β and combined with JSON.stringify/JSON.parse (the same JSON mechanics behind Fundamentals Chapter 10's fetch), it can store more than just plain text.
Storing and Reading a String
setItem(key, value) saves a value under a string key; getItem(key) reads it back. Reload the page and run getItem("username") again β the value is still there, unlike every variable from earlier chapters, which would be reset to nothing on reload.
localStorage.setItem("age", 35) silently converts 35 to the string "35" β reading it back with getItem gives a string, not a number. Trying to store an object directly (setItem("user", { name: "Philip" })) doesn't work either: it gets converted to the unhelpful string "[object Object]", losing all the actual data.
JSON.stringify β Turning Data Into a Storable String
JSON.stringify converts an object or array into a JSON-formatted string β text that fully represents the original data's structure and values, safe to store as a single localStorage string. This is the same conversion that happens automatically inside a fetch request body when sending JSON data to a server.
JSON.parse β Turning It Back Into Real Data
JSON.parse does the reverse: a JSON string back into a real, usable JavaScript object β with correct types preserved (age comes back as an actual number, not the string "35"). This stringify/parse pair is exactly how Fundamentals Chapter 10's response.json() works internally.
The Complete Save/Load Pattern
localStorage.getItem returns null if the key was never set β checking for that with stored ? ... : null (Fundamentals Chapter 3's ternary) avoids calling JSON.parse(null), which would throw rather than returning something sensible.
Removing and Clearing Storage
removeItem deletes a single key, the direct equivalent of delete obj.key from Fundamentals Chapter 7; clear() wipes every key the current site has stored in localStorage entirely.
localStorage persists indefinitely, across browser restarts, until explicitly cleared. sessionStorage shares the exact same API (setItem/getItem/removeItem/clear) but is wiped automatically when the browser tab closes β useful for data that should only last for the current visit.
| Tool | Purpose |
|---|---|
| localStorage.setItem(key, value) | Save a string value, persisting across reloads |
| localStorage.getItem(key) | Read a stored value; returns null if never set |
| JSON.stringify(value) | Convert an object/array into a storable JSON string |
| JSON.parse(jsonString) | Convert a JSON string back into a real object/array |
| localStorage.removeItem(key) / .clear() | Delete one key / delete everything |
Coding Challenges
Save the string "dark" under the key "theme" using localStorage.setItem. Read it back with getItem and log it. Then use removeItem to delete it, and log getItem("theme") again to confirm it now returns null.
π View solutionCreate an object task = { title: "Buy milk", done: false }. Save it to localStorage under "task1" using JSON.stringify. Read it back, parse it with JSON.parse, and log task.title and typeof task.done to confirm the boolean type survived the round trip.
π View solutionWrite functions saveTodos(todos) and loadTodos() using the save/load pattern from this chapter, where todos is an array of strings. saveTodos should stringify and store the array; loadTodos should return the parsed array, or an empty array [] (not null) if nothing has been saved yet. Save 3 todos, then load and log them.
π View solutionChapter 7 Quick Reference
- localStorage.setItem(key, value) β saves a STRING, persisting across reloads/restarts
- localStorage.getItem(key) β reads it back; returns null if the key was never set
- JSON.stringify(value) β converts an object/array into a JSON string for storage
- JSON.parse(jsonString) β converts a JSON string back into a real object/array, types intact
- localStorage.removeItem(key) β deletes one key; clear() β deletes everything for this site
- sessionStorage β same API, but wiped when the browser tab closes
- Always guard JSON.parse against a null/missing value before calling it
- This completes JavaScript Intermediate. Advanced begins with modules β import/export and bundlers conceptually.