webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
JavaScript · Lesson 4 of 17

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)       // ✗ ReferenceError

Why 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 = 2

TDZ — 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 → bind or () => 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/const throw an error (TDZ).
  • this depends on how you call the function; arrow functions have no this of their own.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.