- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
How JavaScript Stores Variables: Why Objects Behave Differently From Numbers
In this lab, you'll discover why JavaScript objects and arrays behave differently from numbers when you assign and copy them. You'll experiment with value and reference semantics, model employee pay records as objects containing a nested array of bonus history, and calculate each employee's expected pay when a performance bonus is on the line. By the end, you'll know how to tell shallow copies from deep copies, why a shallow copy can still leak mutations through nested data, and how to copy objects safely to avoid one of the most common sources of bugs in JavaScript: unintended mutation.
Lab Info
Table of Contents
-
Challenge
Step 1: Introduction
Welcome to the How JavaScript Stores Variables: Why Objects behave differently from Numbers Code Lab.
JavaScript treats numbers and objects differently when you assign or copy them, and that difference is one of the most common sources of subtle bugs in real applications. Understanding when a variable holds its own value versus when it merely points to shared data lets you predict exactly how your code behaves instead of guessing after something breaks.
In this lab, you'll build a small payroll calculator that models employee pay records as objects, each holding a nested array of past bonus percentages, and computes every employee's expected pay from a formula that factors in a performance bonus. Along the way, you'll copy records the wrong way and watch the payroll team's exact bug reappear in your own code, then fix it for good with a proper deep copy. To begin, you'll set that bug aside for a moment and confirm the simplest case of all: how a plain number behaves when you copy it.
The
applicationdirectory already contains this lab's Node.js project, with files you'll edit at paths likesrc/semantics/numberCopy.js.warning> If VS Code shows a Do you trust the authors of the files in this folder? prompt, click Yes, I trust the authors. Trusting the workspace is required for VS Code to run and debug the code in this lab.
info> Stuck? Check the matching
solution/stepN/folder for a working implementation, but try it yourself first.In addition, you can
-
open up the Terminal: In VS Code, click
Control+`. -
In the Terminal, from in the
workspacedirectory, you can run./runTest.sh task1through./runTest.sh task10.
(This isn't required, as you can get similar information by clicking the Validate buttons.)
-
-
Challenge
Step 2: Explore how numbers are copied
To begin, you'll experiment with a plain number variable to see exactly how JavaScript copies it when you assign it to a second variable. This gives you a concrete baseline for how primitive values behave, one you'll contrast against objects and arrays in the next step, where the payroll bug starts to take shape.
Copying primitive values
JavaScript variables that hold primitives, such as numbers, strings, or booleans, store the value itself rather than a pointer to it. When you assign one variable to another, the second variable gets its own independent copy of that value, so changing it afterward has no effect on the first. This is the baseline behavior every other comparison in this lab measures against.
let a = 5; let b = a; b = 10; // a is still 5 -
Challenge
Step 3: Discover object and array references
With primitive copies confirmed to behave independently, you're ready to see how objects behave differently. Here, you'll copy a sample employee record, and separately its nested bonus history array, into new variables and mutate each copy to see whether the original data changes. This step reveals why an assignment that looks identical to a number's assignment produces a completely different outcome for objects and arrays, setting up the payroll bug you'll fix later in the lab. You'll define the full shape of an employee record yourself in the next step; here, the sample record is just a vehicle for observing reference behavior.
Objects and arrays share references
Objects and arrays behave differently from primitives. When you assign an existing object or array to a new variable, that variable doesn't get its own copy: it points to the exact same object in memory as the original. Changing a property on the object, or an element in the array, through either variable changes what both variables see, because there's only one object being shared.
const record = { total: 1 }; const alias = record; alias.total = 99; // record.total is now 99 too -
Challenge
Step 4: Compute expected pay
You can now distinguish how a variable's assignment behaves depending on whether it holds a primitive or a reference. Here, you'll put that understanding to work by modeling employee pay records as objects with a base pay amount, a bonus probability, a bonus multiplier, and a bonus history array, then implementing a function that calculates each employee's expected pay from those fields. By the end of this step, your calculator produces a correct expected pay value for any employee record you give it.
Modeling a record with an object literal
A record like an employee's pay information can be represented as a plain object literal, with each piece of data stored as a named property. Grouping related values into one object, rather than passing them around as separate variables, keeps the data together and makes it easy to pass a single employee to other functions later in this lab. When a parameter name matches the property name you want, you can use property shorthand instead of repeating the name twice.
function createItem(label, quantity) { return { label, quantity }; } ``` ### Understand the expected pay formula An employee's expected pay follows a weighted formula: `Expected Pay = Base Pay * (1 - Chance of Bonus) + Base Pay * Chance of Bonus * Bonus Multiplier`. -
Challenge
Step 5: Attempt a shallow copy
With a working expected pay calculation in place, you're ready to reuse it across multiple employees, which is exactly when the payroll team's original bug appeared. Here, you'll copy an employee record with a shallow copy technique such as spread syntax, then mutate both a top-level field and the nested bonus history array on the copy to see which changes leak back into the original. This step lets you observe firsthand why a shallow copy is not enough to protect nested data, the exact failure the payroll team ran into.
Shallow copies
A shallow copy creates a new top-level object, copying each property's value onto it. You can create one using spread syntax (
{ ...original }) orObject.assign({}, original). For properties holding primitives, this gives you full independence from the original object.const copy = { ...original }; ``` ### What a shallow copy protects Because a shallow copy creates its own top-level object, reassigning one of its primitive properties, such as a number or string, only touches that copy. The original object's corresponding property is untouched, since primitives are never shared between the two objects, only the object wrapper itself was copied. ### Where a shallow copy falls short A shallow copy only duplicates an object's own top-level properties. If one of those properties is itself an object or array, such as a bonus history list, the copy still points to that exact same nested array as the original. Modifying the nested array through the copy, for example by pushing a new value onto it, changes the same array the original object sees. -
Challenge
Step 6: Clone employee records safely with a deep copy
Having seen exactly where a shallow copy falls short, you're ready to fix it. Here, you'll implement a deep copy strategy that clones an employee record along with its nested bonus history array, then repeat the same mutation checks from the previous step to confirm the original record no longer changes. Every check so far has worked with one employee record at a time; you'll close out the lab by extending that same deep copy strategy to a full array of employee records, computing payroll for the entire team at once and confirming that every employee's record stays fully independent.
Deep copies
A deep copy clones an object and every object or array nested inside it, so the copy shares no references with the original at any depth. In modern JavaScript,
structuredClone(value)does this automatically for plain data. You can also write a manual recursive copy that clones each nested object or array in turn, if you ever need behaviorstructuredClonedoesn't support.const copy = structuredClone(original); ``` ### Confirming deep independence Because a deep copy gives its nested arrays and objects their own separate identity, changes made to a nested array on the copy, such as pushing a new value, no longer reach the original array. The two objects no longer share any part of their structure, which is exactly what a shallow copy couldn't guarantee. ### Bringing it together across a list of employees The payroll calculator needs to process every employee's record without letting a change to one employee's data affect another's. Mapping over an array with `Array.prototype.map` lets you transform each employee record into a result in a new array, and combining that with a deep copy of each record before calculating its pay keeps every employee's data fully independent from the others. ```js function processAll(items) { return items.map((item) => transform(item)); } ``` Nice work. You built a small payroll calculator that models employee pay records, calculates each employee's expected pay from a performance-bonus formula, and computes payroll across an entire team of employees without any of them affecting each other's data. If you've completed every task, confirm you're in the `workspace` folder, then run: ```bash ./runTest.sh runExampleto see your calculator compute a real employee's expected pay.
Along the way, you saw firsthand how JavaScript treats primitives and reference types differently: a copied number is always independent, while a copied object or array can still point back to the same data underneath. You used that distinction to explain exactly why a shallow copy protected one field but leaked another, and you fixed the problem for good with a deep copy.
That same pattern, watching for shared references and reaching for a deep copy when nested data needs to stay independent, will keep showing up any time you copy objects or arrays in your own JavaScript code.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.