JavaScript Mental Models That Make Everything Click JavaScript Deep Dive

🟦 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:

this refers 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.