JavaScript Essentials and the DOM
Use modern JavaScript to make pages interactive and manipulate the DOM safely and efficiently.
Content
Variables and scoping rules
Versions:
Watch & Learn
AI-discovered learning video
JavaScript Variables and Scoping Rules — the backstage pass to your app's memory
Imagine you stored a user's session token in a global variable. Then some other script (or a sneaky library) overwrote it. Congratulations — you just created a security bug and a very unhappy user.
(You read the previous CS50 section on handling user state securely and authentication. Good — this is where those ideas meet the language-level rules that make or break safe state management.)
What this is and why it matters
Variables and scope determine where your data lives, who can see it, and for how long. In web apps this directly affects:
- Security: sensitive values that leak into global scope are easier to tamper with.
- Correctness: bugs like unexpected overwrites and surprising values come from misunderstanding scope and hoisting.
- Memory & lifecycle: closures can keep things alive longer than you expect.
So if you want robust authentication, safe session handling, and fewer hair-pulling bugs — learn scoping.
The three players: var, let, const
Micro explanation
- var — old-school, function-scoped, hoisted. Can cause surprise leaks.
- let — block-scoped, not accessible before declaration (temporal dead zone).
- const — block-scoped, must be initialized, binding cannot be reassigned (but objects it points to can be mutated).
Always prefer const by default, let when you must reassign, and avoid var unless you have a very specific reason.
// var example (hoisting):
console.log(x); // undefined (not ReferenceError)
var x = 5;
// let/const example (temporal dead zone):
console.log(y); // ReferenceError
let y = 10;
Hoisting — the illusion that declarations move
JavaScript hoists declarations (not initializations). With var, the declaration is lifted to the top of its function scope, but the assignment stays where it was.
Example that bites you:
function getUser() {
console.log(token); // undefined
var token = window.localStorage.getItem('token');
}
You might think token is unavailable before the line that sets it. But var makes it exist (as undefined) from the start — which can hide bugs.
With let/const you get a ReferenceError instead, which often helps catch problems earlier.
Function scope vs block scope
- Function scope (var): variables declared with var are visible anywhere inside the function.
- Block scope (let/const): variables declared with let or const are only visible inside the nearest
{ ... }block (if, for, while, etc.).
The infamous for-loop closure bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 10); // prints 3, 3, 3
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 10); // prints 0, 1, 2
}
Why? var i is shared across iterations (function scope), so by the time callbacks run i is 3. let creates a fresh binding per iteration. This is a practical reason to prefer let in loops.
Closures: private variables and long-lived references
A closure lets a function access variables from an outer scope even after that outer function has finished running. This is powerful and subtle.
Use case: module pattern (privacy)
const Auth = (function() {
let token = null; // private
return {
setToken(t) { token = t; },
getToken() { return token; }
};
})();
Auth.setToken('abc');
console.log(Auth.getToken()); // 'abc'
This pattern keeps token out of global scope — a win for security. But closures also keep token alive in memory until the closure itself is collectible.
Temporal Dead Zone (TDZ) — why some access raises errors
Accessing a let/const binding before its declaration throws a ReferenceError. This is the TDZ and prevents subtle hoisting bugs.
function weird() {
console.log(a); // ReferenceError
let a = 2;
}
TDZ helps you catch mistakes early — another reason let/const are preferable.
Global scope: the land of accidents
Anything attached to the global object (window in browsers) is accessible everywhere. That makes globals great for convenience and terrible for security and maintenance.
Bad idea:
window.currentUserId = 'sensitive-id';
Any script can read or overwrite that. Instead: keep sensitive info on the server, or in scoped closures/modules and only expose controlled APIs to the rest of your code.
Practical rules for CS50 web projects (and real life)
- Default to const. Use let only for variables that change. Avoid var.
- Never store sensitive session/auth data in globals. Store tokens in secure HTTP-only cookies or server-side session stores. If you must store anything client-side, minimize and protect it.
- Encapsulate module state using closures, ES modules, or classes. Expose minimal API methods that validate input.
- Beware third-party scripts. They can access globals. Don't attach secrets to window.
- Use strict mode (automatically in ES modules): catches silent errors and forbids accidental globals.
- Mind closures: they are great for privacy but can keep data alive longer than intended.
Quick reference: behavior summary
- var: function-scoped, hoisted (initialized to undefined), can be redeclared
- let: block-scoped, hoisted but uninitialized (TDZ), cannot redeclare in same scope
- const: block-scoped, must initialize, binding immutable (value may be mutable if object)
A real-world security example
Imagine you tried to cache a token client-side during debugging:
var token = 'temporary-auth';
// some other script accidentally reassigns token = null
// your app loses auth state or accepts malicious value
If instead token were kept inside a module closure and only accessible through controlled getters/setters, an accidental overwrite would be far less likely.
Key takeaways
- Scope is your first line of defense against accidental leaks and overwrites.
- Prefer const and let over var for predictable behavior and fewer surprises.
- Avoid global variables for auth or sensitive state; keep secrets server-side or in HTTP-only cookies.
- Use closures and modules to create private state and controlled APIs.
"Treat variables like people in a party: keep strangers out of your private conversations, and don't let everyone shout into the room." — your slightly dramatic CS50 TA
If you master scoping, hoisting, and closures, you'll write code that's easier to reason about, harder to exploit, and—bonus—less likely to cause 2 AM debugging spirals.
Further reading: MDN on var/let/const, articles on temporal dead zone, and patterns for secure client-side state. Try refactoring one of your project files to remove var and globals — it's a great exercise.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!