JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
109: Pure Functions and Immutability
I want to show you something that tripped me up for years early in my career. It's a subtle bug that doesn't throw an error—the code runs perfectly—but your data ends up in a state you never intended. Let's look at a small piece of logic for a user profile system.
The Trap of the Shared Object
Imagine we have a user object and a function to update their email. At first glance, this looks like the most straightforward way to do it:
const user = {
name: 'Alex',
email: 'alex@example.com',
role: 'admin'
};
function updateEmail(user, newEmail) {
user.email = newEmail;
return user;
}
const updatedUser = updateEmail(user, 'alex.new@work.com');
console.log(updatedUser.email); // 'alex.new@work.com'
Everything seems fine, right? We called the function, it updated the email, and it returned the user. But here is where things get weird. If I check the original user object now, I'll see it has also changed.
console.log(user.email); // 'alex.new@work.com'
Wait. I thought I was creating an updatedUser. I didn't realize I was actually modifying the original source of truth. In a small script, this is a nuisance. In a massive application with a state management system (like Redux or React), this is a nightmare because the system can't tell that the data changed—the reference to the object is still the same, even though the insides are different.
Tracking Down the Ghost Change
This happens because objects in JavaScript are passed by reference. When I passed user into updateEmail, I wasn't passing a copy; I was passing a pointer to the memory location where that object lives. By doing user.email = newEmail, I performed a mutation.
Now, imagine this function was part of a larger "Undo" feature. If I mutate the original object, I've just erased the previous state forever. There's no going back because the original user object is gone, replaced by the updated one. This is why we talk about Immutability—the idea that once data is created, it should not be changed.
Stopping the Bleeding with the Spread Operator
So, how do we fix this? I need to stop changing the existing object and instead return a brand new one. The cleanest way to do this in modern JS is using the spread operator (...).
function updateEmailPure(user, newEmail) {
return {
...user,
email: newEmail
};
}
const userOriginal = {
name: 'Alex',
email: 'alex@example.com',
role: 'admin'
};
const userUpdated = updateEmailPure(userOriginal, 'alex.new@work.com');
console.log(userOriginal.email); // 'alex@example.com' (Still the same!)
console.log(userUpdated.email); // 'alex.new@work.com' (New object!)
Look at what happened here. I created a new object literal {}, copied all the properties of the old user into it, and then specifically overrode the email property. The original userOriginal remains untouched. This is the core of immutability.
The Peace of Mind in Predictability
By doing this, we've turned updateEmail into a Pure Function. A function is "pure" if it meets two criteria:
- It always returns the same output for the same input.
- It has no side effects (it doesn't change anything outside of itself, like modifying a global variable or mutating an input object).
I love pure functions because they are boring. And in software engineering, boring is a compliment. I don't have to wonder if calling updateEmail is going to accidentally break a different part of my app. I know exactly what goes in, and I know exactly what comes out, and I know the rest of my system is safe from unexpected changes.
📋 Practical Task
Fixing the Inventory Mutator
You are working on a warehouse management app. There is a function called updateStock that is currently mutating the inventory array, which is causing the "Previous Stock" logs to be incorrect. Your task is to rewrite this function to be a pure function.
Requirements:
- Do not use
.push()or direct index assignment (e.g.,arr[i] = value), as these mutate the array. - Use
.map()or the spread operator to return a new array. - The original
inventoryarray must remain unchanged.
const inventory = [
{ item: 'Laptop', quantity: 5 },
{ item: 'Mouse', quantity: 12 },
{ item: 'Keyboard', quantity: 8 }
];
// FIX THIS FUNCTION
function updateStock(items, itemName, newQuantity) {
// Your code here: return a new array with the updated quantity
}
const updatedInventory = updateStock(inventory, 'Mouse', 10);
console.log('Original should be 12:', inventory[1].quantity);
console.log('Updated should be 10:', updatedInventory[1].quantity);
There are no comments for now.