Skip to content

Commit 2190115

Browse files
authored
feat: expand content and examples across some JS quiz content (#50)
1 parent cd6d7a7 commit 2190115

6 files changed

Lines changed: 1077 additions & 185 deletions

File tree

  • questions
    • explain-hoisting
    • explain-the-difference-between-shallow-copy-and-deep-copy
    • explain-what-a-single-page-app-is-and-how-to-make-one-seo-friendly
    • what-is-a-closure-and-how-why-would-you-use-one
    • what-is-event-loop-what-is-the-difference-between-call-stack-and-task-queue
    • what-is-the-difference-between-double-equal-and-triple-equal

questions/explain-hoisting/en-US.mdx

Lines changed: 156 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ The following behavior summarizes the result of accessing the variables before t
2929

3030
## Hoisting
3131

32-
Hoisting is a term used to explain the behavior of variable declarations in JavaScript code.
32+
Hoisting is a term used to explain the behavior of declarations in JavaScript code.
3333

34-
Variables declared or initialized with the `var` keyword will have their declaration "moved" up to the top of their containing scope during compilation, which we refer to as hoisting.
34+
Variables declared with the `var` keyword have their declaration "moved" up to the top of their containing scope during compilation, which we refer to as hoisting.
3535

3636
Only the declaration is hoisted; the initialization/assignment (if there is one) will stay where it is. Note that the declaration is not actually moved – the JavaScript engine parses the declarations during compilation and becomes aware of variables and their scopes, but it is easier to understand this behavior by visualizing the declarations as being "hoisted" to the top of their scope.
3737

3838
Let's explain with a few code samples. Note that the code for these examples should be executed within a module scope instead of being entered line by line into a REPL like the browser console.
3939

4040
### Hoisting of variables declared using `var`
4141

42-
Hoisting is seen in action here as even though `foo` is declared and initialized after the first `console.log()`, the first `console.log()` prints the value of `foo` as `undefined`.
42+
Hoisting is visible here: even though `foo` is declared and initialized after the first `console.log()`, the first `console.log()` prints `undefined`.
4343

4444
```js live
4545
console.log(foo); // undefined
@@ -80,7 +80,7 @@ class Foo {
8080

8181
### Hoisting of function expressions
8282

83-
Function expressions are functions written in the form of variable declarations. Since they are also declared using `var`, only the variable declaration is hoisted.
83+
A function expression is a function assigned to a variable binding. When the binding uses `var`, only the variable declaration is hoisted — the function body is not.
8484

8585
```js live
8686
console.log(bar); // undefined
@@ -91,6 +91,14 @@ var bar = function () {
9191
};
9292
```
9393

94+
Arrow functions are function expressions too, so the same rule applies — only the binding is hoisted, and its TDZ behavior follows the declaration keyword (`var` initializes to `undefined`, `let` and `const` remain in the TDZ until their declaration runs).
95+
96+
```js live
97+
console.log(baz); // undefined
98+
var baz = () => 'arrow';
99+
console.log(baz()); // 'arrow'
100+
```
101+
94102
### Hoisting of function declarations
95103

96104
Function declarations use the `function` keyword. Unlike function expressions, function declarations have both the declaration and definition hoisted, thus they can be called even before they are declared.
@@ -104,7 +112,7 @@ function foo() {
104112
}
105113
```
106114

107-
The same applies to generators (`function*`), async functions (`async function`), and async function generators (`async function*`).
115+
The same applies to generator functions (`function*`), async functions (`async function`), and async generator functions (`async function*`).
108116

109117
### Hoisting of `import` statements
110118

@@ -128,16 +136,157 @@ However, this statement is [a little different for the `var` keyword](https://tc
128136

129137
> Var variables are created when their containing Environment Record is instantiated and are initialized to `undefined` when created.
130138
139+
MDN groups hoisting into four observable behaviors, which map to the declaration kinds covered above:
140+
141+
1. **Value hoisting** — the value is usable before the declaration. Applies to function declarations.
142+
2. **Declaration hoisting** — the binding is usable before the declaration but reads `undefined`. Applies to `var`.
143+
3. **Scope tainting** — the binding exists from the top of the scope but any access throws (the TDZ). Applies to `let`, `const`, and `class`.
144+
4. **Side effects** — the declaration's side effects run before the rest of the module evaluates. Applies to `import`.
145+
131146
## Modern practices
132147

133148
In practice, modern codebases avoid using `var` and use `let` and `const` exclusively. It is recommended to declare and initialize your variables and import statements at the top of the containing scope/module to eliminate the mental overhead of tracking when a variable can be used.
134149

135150
ESLint is a static code analyzer that can find violations of such cases with the following rules:
136151

137-
- [`no-use-before-define`](https://eslint.org/docs/latest/rules/no-use-before-define): This rule will warn when it encounters a reference to an identifier that has not yet been declared.
138-
- [`no-undef`](https://eslint.org/docs/latest/rules/no-undef): This rule will warn when it encounters a reference to an identifier that has not yet been declared.
152+
- [`no-use-before-define`](https://eslint.org/docs/latest/rules/no-use-before-define): Warns when an identifier is referenced before its declaration appears in source.
153+
- [`no-undef`](https://eslint.org/docs/latest/rules/no-undef): Warns when an identifier is referenced without being declared anywhere in scope.
154+
155+
## Additional examples
156+
157+
The examples below cover hoisting behaviors that are less obvious from the summary table and that commonly cause confusion.
158+
159+
### Function declaration compared with function expression
160+
161+
```js live
162+
console.log(declared());
163+
console.log(expressed());
164+
165+
function declared() {
166+
return 'function declaration';
167+
}
168+
169+
var expressed = function () {
170+
return 'function expression';
171+
};
172+
```
173+
174+
- `declared()` returns `'function declaration'`. Function declarations are fully hoisted — both the identifier binding and the function body are available from the top of the scope.
175+
- `expressed()` throws `TypeError: expressed is not a function`. The `var expressed` binding is hoisted and initialized to `undefined`, but the assignment of the function expression happens at its source location. Calling `undefined()` produces the `TypeError`.
176+
177+
### `var` in a `for` loop with `setTimeout`
178+
179+
```js live
180+
for (var i = 0; i < 3; i++) {
181+
setTimeout(() => console.log(i), 0);
182+
}
183+
// Output: 3, 3, 3
184+
```
185+
186+
`var i` is function-scoped rather than block-scoped, so all three callbacks close over the same binding. The loop increments `i` to `3` before any `setTimeout` callback runs, because macrotasks run after the current synchronous code completes. Each callback then reads the current value of the shared `i`, which is `3`.
187+
188+
Two fixes:
189+
190+
- Replace `var` with `let`. `let` is block-scoped, so each iteration creates a fresh binding that the callback closes over.
191+
- Wrap the body in an IIFE that captures the current value as a parameter: `(i => setTimeout(() => console.log(i), 0))(i)`. This was the pre-ES6 workaround.
192+
193+
### `var` escapes block scope
194+
195+
```js live
196+
if (true) {
197+
var a = 1;
198+
let b = 2;
199+
}
200+
console.log(a); // 1
201+
console.log(b); // ReferenceError: b is not defined
202+
```
203+
204+
`var` is scoped to the nearest function or script, not to the enclosing block. The declaration is hoisted past `if`, `for`, `while`, and plain block statements to the containing function or module scope, which is why `a` is still visible after the `if`. `let` and `const` are block-scoped, so `b` only exists inside the block.
205+
206+
### Redeclaration
207+
208+
```js
209+
var x = 1;
210+
var x = 2; // OK — x is now 2
211+
212+
let y = 1;
213+
let y = 2; // SyntaxError: Identifier 'y' has already been declared
214+
```
215+
216+
`var` allows the same name to be redeclared in the same scope; the second declaration is a no-op and only the assignment runs. `let`, `const`, and `class` throw `SyntaxError` if the same name is declared twice in the same scope. Like hoisting, this is resolved statically before execution — duplicate lexical declarations are an _early error_ detected during parsing, so no code runs at all.
217+
218+
### Class declarations
219+
220+
```js live
221+
console.log(typeof Foo);
222+
223+
class Foo {}
224+
```
225+
226+
This throws `ReferenceError: Cannot access 'Foo' before initialization`.
227+
228+
Class declarations are hoisted — the binding is created at the top of the enclosing block — but they remain in the Temporal Dead Zone until the `class` declaration is evaluated. Any access before that point throws, including `typeof`.
229+
230+
This behavior can be confused with "classes are not hoisted". The two statements are observably different:
231+
232+
- If `Foo` were not hoisted, `typeof Foo` would return `'undefined'` (the behavior for truly undeclared identifiers).
233+
- Because `Foo` is hoisted but uninitialized, `typeof Foo` throws.
234+
235+
The distinction also matters for `extends` clauses, which are evaluated at class declaration time. `class A extends B {}` throws if `B` is hoisted but still in the TDZ at that point.
236+
237+
### `typeof` and the Temporal Dead Zone
238+
239+
```js live
240+
console.log(typeof undeclaredVariable); // 'undefined'
241+
console.log(typeof someLet); // ReferenceError
242+
let someLet = 1;
243+
```
244+
245+
`typeof` does not throw when applied to an identifier that has no declaration anywhere in scope — it returns the string `'undefined'`. However, `typeof` does throw when applied to an identifier that is declared but still in the Temporal Dead Zone. The binding exists, and reading it (which `typeof` must do) triggers the TDZ error.
246+
247+
This distinguishes "undeclared" (no binding in any enclosing scope) from "declared but uninitialized" (binding exists, initialization has not yet occurred).
248+
249+
### Shared names across `var` and function declarations
250+
251+
```js live
252+
function outer() {
253+
console.log(inner);
254+
inner();
255+
256+
function inner() {
257+
console.log('inner called');
258+
}
259+
260+
var inner = 'overwritten';
261+
}
262+
263+
outer();
264+
// Output:
265+
// [Function: inner]
266+
// inner called
267+
```
268+
269+
Two behaviors combine here:
270+
271+
1. Both `var inner` and the `function inner` declaration are hoisted to the top of `outer`.
272+
2. When a `var` declaration and a function declaration share a name in the same scope, the function declaration takes precedence during initialization. `inner` is initialized with the function object rather than `undefined`.
273+
274+
The `var inner = 'overwritten'` assignment takes effect only after the two `console.log` calls, so those calls observe the function. A `console.log(inner)` after the assignment would print `'overwritten'`.
275+
276+
A `let` or `const` declaration in the same scope as a `var` of the same name produces a `SyntaxError` at parse time, before any code runs.
277+
278+
## Common misconceptions
279+
280+
The following statements appear frequently in explanations of hoisting, including in material generated by large language models, but are incorrect or imprecise:
281+
282+
1. **Classes are not hoisted.** Class declarations are hoisted. They remain in the Temporal Dead Zone until the declaration is evaluated, which is observably different from not being hoisted at all — most notably, `typeof` throws on a class in the TDZ but returns `'undefined'` for a truly undeclared identifier.
283+
2. **`var` is hoisted; `let` and `const` are not.** All three are hoisted. They differ in initialization: `var` is initialized to `undefined` at hoist time, while `let` and `const` remain uninitialized in the TDZ until their declaration is evaluated.
284+
3. **`typeof` never throws on undeclared variables.** `typeof` is safe for identifiers that have no declaration anywhere in scope, but it throws in the TDZ. The `typeof x === 'undefined'` guard is only safe if `x` is not declared anywhere in the enclosing scope.
285+
4. **Function declarations are hoisted; function expressions are not.** Both are hoisted, but only the binding. In `var fn = function () {}`, the `var fn` declaration is hoisted and initialized to `undefined`. In `let fn = function () {}`, the binding is hoisted but remains in the TDZ. The function body is never hoisted for expressions.
139286

140287
## Further reading
141288

142289
- [Hoisting | MDN](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting)
290+
- [ECMA-262 §14.2.6 Block Runtime Semantics: Evaluation](https://tc39.es/ecma262/#sec-block-runtime-semantics-evaluation)
291+
- [Temporal Dead Zone | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz)
143292
- [What is Hoisting in JavaScript?](https://www.freecodecamp.org/news/what-is-hoisting-in-javascript)

0 commit comments

Comments
 (0)