Scope, hoisting and this
Where variables live and why a method "loses" its this.
Updated
What scope is
Scope = the part of the code where a variable exists and can be used. Outside of it, the variable doesn't exist.
function order() {
const total = 100 // exists only inside order()
}
console.log(total) // ✗ ReferenceErrorWhy it exists: without scope, every variable would be global and they'd trip over each other (two functions with an i each).
Kinds of scope
| Kind | Created by | Contains |
|---|---|---|
| global | the file / page | everything declared outside functions (in ES modules — global only for that module) |
| function | every function | its parameters and variables (including var) |
| block | any { }: if, for, while, a plain {} |
let, const, class |
const app = 'learn' // global (to the module)
function render() {
const title = 'Functions' // function scope
if (title) {
const upper = title.toUpperCase() // block scope
}
upper // ✗ no longer exists
}Lexical scope and the scope chain
When you use a variable, JS looks for it in the current scope, then in the parent scope, then in its parent… up to the global one. This chain is called the scope chain.
Important: the parent is decided by where the function is written in the code (lexically), not by where it's called from.
const x = 'global'
function outer() {
const x = 'outer'
return function inner() { return x } // inner is written inside outer → sees outer's x
}
outer()() // 'outer'This is where the closure comes from — see Functions.
What hoisting is
Hoisting ("lifting") = before running the code, JS walks through the scope and registers all the declarations. The effect: some things can be used before the line where they're written.
Each kind of declaration is hoisted differently:
| Declaration | Hoisted? | What you get if you use it before its line |
|---|---|---|
function f() {} |
yes, body included | the function works normally |
var x = 1 |
yes, but without the value | undefined |
let / const |
yes, but locked | ReferenceError (TDZ) |
class A {} |
yes, but locked | ReferenceError |
const f = () => {} |
like any const |
ReferenceError |
sayHi() // ✓ 'Hi' — the function declaration is fully hoisted
function sayHi() { return 'Hi' }
console.log(a) // undefined — var is hoisted, the value only arrives at its line
var a = 1
console.log(b) // ✗ ReferenceError
const b = 2TDZ — the temporal dead zone
Between the start of the scope and the line where a let/const is declared, the variable is in a "dead zone" (TDZ): JS knows it exists, but refuses to let you use it. That's a good thing — it catches bugs that var used to hide behind undefined.
What it means in practice: you can organize a file with the main function at the top and the helpers (function declarations) at the bottom — it works thanks to hoisting.
What this is
this is a special variable, available inside functions, that points to the object in whose context the function runs. Unlike other variables, the value of this does not depend on where the function is written, but on how it's called.
The rules, in order of priority
| How you call it | this is |
Example |
|---|---|---|
new Fn() |
the newly created object | new User('Ana') |
fn.call(obj), fn.apply(obj), fn.bind(obj) |
obj, set explicitly |
greet.call(user) |
obj.method() |
the object before the dot | user.greet() |
a plain fn() |
undefined (in modules / strict mode) |
const g = user.greet; g() |
| arrow function | the this of the place where it was written — it has no this of its own |
() => this.name |
const user = {
name: 'Ana',
greet() { return `Hello, ${this.name}` },
}
user.greet() // 'Hello, Ana' — this = user
const greet = user.greet
greet() // ✗ this is undefined — the method got "detached" from the object
const bound = user.greet.bind(user)
bound() // ✓ 'Hello, Ana'Why arrow functions "fix" callbacks
const timer = {
seconds: 0,
start() {
setInterval(function () { this.seconds++ }, 1000) // ✗ this isn't timer
setInterval(() => { this.seconds++ }, 1000) // ✓ the arrow takes this from start()
},
}When you care about this in practice
- In modern React (functions + hooks), almost never.
- In classes (custom errors, SDKs), in classic event listeners and in older code.
- The rule: methods → normal functions; callbacks inside them → arrows; passing a method as a callback →
bindor() => obj.method().
Summary
- Scope = where a variable lives: global, function, block. The lookup climbs the scope chain, based on where the code is written.
- Hoisting = declarations are registered before running; functions work before their definition,
let/constthrow an error (TDZ). thisdepends on how you call the function; arrow functions have nothisof their own.