π¦ JavaScript Deep Dive β Issue #26
JavaScript Mental Models That Make Everything Click
Stop memorizing JavaScript behavior. Start understanding why it behaves that way.
Thereβs a point in learning JavaScript where knowing more syntax stops helping.
You know functions.
You know objects.
You know Promises.
You know async/await.
You know closures.
But then JavaScript does something unexpected:
console.log([] == false); // true
Or:
const a = { count: 1 };
const b = a;
b.count++;
console.log(a.count); // 2
Or:
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
What gets printed?
The problem isn’t missing syntax.
It’s the mental model you’re using to predict what JavaScript will do.
Senior developers don’t memorize thousands of JavaScript edge cases.
They build mental models that let them reason about the language.
Let’s look at some of the most useful ones.
π§ 1. Variables Don’t Contain Objects
This is one of the most important mental shifts in JavaScript.
Consider:
const user = {
name: "Alex"
};
It’s tempting to imagine user as a box containing the object.
A more useful mental model is:
user βββββββΊ { name: "Alex" }
user holds a reference to the object.
Now:
const admin = user;
admin.name = "Sam";
console.log(user.name);
Output:
Sam
Why?
Because you didn’t create another object.
You created another reference:
user βββ
ββββΊ { name: "Sam" }
admin βββ
This mental model immediately explains a huge number of JavaScript bugs involving:
- objects
- arrays
- function arguments
- state
- mutation
π¨ 2. Assignment Doesn’t Mean Copy
Consider:
const original = {
settings: {
theme: "dark"
}
};
const copy = { ...original };
Many developers think:
“I copied the object.”
You did β but only one level.
The mental model looks more like:
original βββΊ object A βββΊ settings object
copy βββΊ object B βββΊ settings object
Both objects still reference the same nested settings object.
So:
copy.settings.theme = "light";
console.log(original.settings.theme);
Outputs:
light
This is why understanding references matters more than memorizing the spread operator.
π¨ 3. Functions Carry Their Surrounding Environment
Closures become much easier when you stop thinking of functions as only blocks of code.
Think of a function as:
CODE + ENVIRONMENT
Example:
function createCounter() {
let count = 0;
return function () {
return ++count;
};
}
const counter = createCounter();
Even though createCounter() has finished running, the returned function still has access to:
count
Why?
Because the function carries access to the lexical environment where it was created.
That’s a closure.
This mental model explains:
- private state
- callbacks
- event handlers
- factories
- memoization
- React hooks
Closures aren’t magic.
They’re functions remembering where they came from.
π¨ 4. this Is About How a Function Is Called
One of the biggest JavaScript misconceptions is:
“
thisrefers to the object containing the function.”
Not necessarily.
For regular functions, this depends heavily on how the function is invoked.
Consider:
const user = {
name: "Alex",
greet() {
console.log(this.name);
}
};
user.greet();
Here:
this β user
But:
const greet = user.greet;
greet();
You removed the calling context.
The useful question isn’t:
“Where was this function written?”
Ask:
“How is this function being called?”
That question solves many this problems.
π¨ 5. Arrow Functions Don’t Create Their Own this
Arrow functions behave differently.
const obj = {
value: 10,
method() {
const inner = () => {
console.log(this.value);
};
inner();
}
};
The arrow function uses this from its surrounding lexical scope.
Think:
regular function β invocation determines this
arrow function β surrounding scope supplies this
That small model is much easier to remember than dozens of special cases.
π¨ 6. JavaScript Doesn’t “Wait” for Async Work
Consider:
console.log("Start");
setTimeout(() => {
console.log("Timer");
}, 1000);
console.log("End");
JavaScript doesn’t stop at setTimeout().
Instead, think:
Run current code
β
schedule future work
β
continue current code
β
process scheduled work later
Output:
Start
End
Timer
This becomes especially important with network requests, events, timers, and Promises.
π¨ 7. The Event Loop Is About Queues
Now consider:
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
});
console.log("D");
Many developers expect:
A
B
C
D
Actual result:
A
D
C
B
A useful simplified model is:
Call Stack
β
Microtasks
β
Next task
Promise callbacks enter the microtask queue.
Timer callbacks are scheduled as tasks.
So after the current synchronous code finishes:
A
D
JavaScript processes the Promise microtask:
C
Then the timer:
B
Once this model clicks, async JavaScript becomes dramatically easier to predict.
π¨ 8. async/await Doesn’t Make JavaScript Synchronous
This:
const data = await fetch(url);
looks synchronous.
But it isn’t blocking JavaScript’s entire execution environment while the network responds.
A better mental model is:
Start async operation
β
pause this async function
β
allow other work to continue
β
resume when Promise settles
await changes how you write asynchronous code.
It doesn’t turn asynchronous operations into synchronous ones.
π¨ 9. Promises Represent Future Outcomes
Don’t think of a Promise as “the result.”
Think:
Promise = placeholder for a future outcome
A Promise can be:
pending
β
fulfilled
or
pending
β
rejected
That explains why this doesn’t work:
const data = fetch(url);
console.log(data);
data isn’t the response body.
It’s a Promise representing work that hasn’t necessarily completed yet.
π¨ 10. JavaScript Objects Are More Like Dictionaries Than Classes
Developers coming from class-heavy languages sometimes imagine JavaScript objects primarily as instances of classes.
A simpler model is:
object = collection of property keys β values
For example:
const user = {
name: "Alex",
age: 32
};
Conceptually:
"name" β "Alex"
"age" β 32
And:
user.name
is property access.
So is:
user["name"]
This model makes dynamic property access much easier to understand:
const property = "age";
console.log(user[property]);
π¨ 11. Prototypes Are Delegation, Not Copying
Prototype inheritance becomes easier when you stop imagining objects copying methods from one another.
Instead:
object
β
prototype
β
prototype
β
null
When JavaScript can’t find a property on an object, it looks further up the prototype chain.
Example:
const user = {
name: "Alex"
};
console.log(user.toString);
You didn’t define toString.
JavaScript finds it through the prototype chain.
Think:
“If I don’t have it, ask my prototype.”
That’s prototype delegation.
π¨ 12. Equality Is Easier When You Understand Conversion
This is infamous:
0 == false
Result:
true
Instead of memorizing dozens of strange equality examples, understand that == can perform type coercion before comparison.
By contrast:
0 === false
returns:
false
because strict equality doesn’t perform the same coercion.
Practical rule:
===
should generally be your default.
But the deeper lesson is:
Understand conversion instead of memorizing weird outputs.
π¨ 13. Mutation Is Shared Change
Mutation isn’t inherently evil.
The danger is shared mutation.
Consider:
const settings = {
theme: "dark"
};
function updateSettings(config) {
config.theme = "light";
}
updateSettings(settings);
The function changed the same object the caller owns.
That’s why mutation can create surprising behavior.
A safer pattern might be:
function updateSettings(config) {
return {
...config,
theme: "light"
};
}
Now the original remains unchanged.
π¨ 14. Garbage Collection Is About Reachability
You don’t manually free ordinary JavaScript objects.
Instead, the runtime asks something closer to:
“Can this object still be reached?”
Consider:
let user = {
name: "Alex"
};
user = null;
If nothing else references that object, it becomes eligible for garbage collection.
This also explains many memory leaks.
An object might no longer be useful β but if something still references it, such as:
- an event listener
- timer
- closure
- cache
it may remain reachable.
So a useful mental model is:
reachable β potentially needed
unreachable β collectible
π₯ Putting the Models Together
Consider:
function createTracker() {
const history = [];
return {
add(value) {
history.push(value);
},
getHistory() {
return [...history];
}
};
}
const tracker = createTracker();
tracker.add("A");
tracker.add("B");
console.log(tracker.getHistory());
Several JavaScript concepts are working together.
Closure
The methods retain access to:
history
References
history points to an array object.
Mutation
history.push(value);
mutates that array.
Encapsulation
Outside code cannot directly access history.
Copying
return [...history];
returns a new array instead of exposing the internal array.
That’s the power of mental models.
Instead of seeing unrelated JavaScript features, you start seeing one coherent system.
π§© Mental Model Challenge
Predict the output before running the code.
Challenge #1
const a = { value: 10 };
const b = a;
b.value = 20;
console.log(a.value);
Challenge #2
console.log("1");
Promise.resolve().then(() => {
console.log("2");
});
console.log("3");
Challenge #3
const original = {
user: {
name: "Alex"
}
};
const copy = { ...original };
copy.user.name = "Sam";
console.log(original.user.name);
Challenge #4
function outer() {
let value = 10;
return () => ++value;
}
const fn = outer();
console.log(fn());
console.log(fn());
If you can explain why each answer occurs rather than simply predicting the result, your JavaScript mental model is getting stronger.
π¦ JavaScript Mental Model Cheat Sheet
Objects
variable βββΊ object
Closures
function = code + lexical environment
Regular this
how function is called β this
Arrow this
surrounding lexical scope β this
Promises
future outcome β fulfilled / rejected
Event loop
synchronous code
β
microtasks
β
next task
Prototype chain
object β prototype β prototype β null
Garbage collection
reachability determines eligibility
π Final Thoughts
The biggest jump in JavaScript skill doesn’t happen when you learn another framework.
It happens when you can look at unfamiliar JavaScript and predict what it will do before running it.
That’s when you stop memorizing JavaScript.
You start reasoning about JavaScript.
And once the mental models are right, concepts that once seemed unrelated β closures, Promises, mutation, prototypes, this, async code, and memory β begin fitting together.
That’s when JavaScript really starts to click.
Coming next
π Issue #27 β JavaScript Memory Leaks: The Bugs That Get Worse the Longer Your App Runs
We’ll explore event listeners, closures, timers, detached DOM nodes, caches, garbage collection, WeakMap, WeakSet, and how to actually track down memory that refuses to disappear.