👩‍💻 chrismanbrown.gitlab.io

scope

how scope works in javascript

2024-02-25

CONTENTS

  1. INTRODUCTION
  2. FUNCTION AND GLOBAL SCOPE
  3. HOISTING
  4. LEXICAL SCOPE WITH LET AND CONST
  5. THIS
  6. ARROW FUNCTIONS

INTRODUCTION

Hey, I heard you’re interested in how scope works!

Scope in javascript is wonderfully complicated. I’m going to tell you all that I know about it in case you find it interesting or relevant to your learning.

FUNCTION AND GLOBAL SCOPE

So in the beginning, javascript only had function scope and global scope.

Because of function scope, variables declared inside a function are not accessible outside that function.

// function scope
function adder(x, y) {
  var result = x + y
}

console.log(result); // doesn't work

But global variables are accessible inside functions.

// global scope
var result = 0;

function adder(x, y) {
  result = x + y
  return result;
}

console.log(result); // works!

So when looking for variable definitions, the compiler will first look inside the current function, and will then look inside the enclosing function, if any, and continue until it reaches the global scope. And if it’s not there, then it’s a reference error.

Insidiously, if you reference a variable that hasn’t been declared in function scope or in global scope, it will be added to the global scope for you!

const adder = function adder(x, y) {
    result = x + y;
    return result;
}
console.log(result)  // Uncaught ReferenceError: result is not defined
adder(3,2)
console.log(result)  // 5

HOISTING

As an aside, javascript also has name hoisting.

In some languages like C, you can only refer to functions and variables that you have already declared earlier in your code. That is, you must declare variables before you can reference them.

For example, in such a language, this would be an error:

adder();

int adder(int x, int y) {
    return x + y;
}

…because you’re trying to reference adder becuase you have defined it.

But javascript don’t care!

Javascript goes through and finds all those function definitions and stuff and then HOISTS them to the top of the scope. So you can reference functions before you declare them.

LEXICAL SCOPE WITH LET AND CONST

When ECMA of the Coast published JSNext, the introduced a new kind of scope, which is lexical or block scope. And they introduced some new keywords to mess around with this scope: let and const1. And they changed how var works.

So now all vars (which is 100% of all variables in existing code at the time of ESNext because that’s the only keyword we had for declaring variables) are now global variables. They break out of their function scope and are global now.

New let and const keywords now create block scope. So now you can have values scoped locally to if statements and other blocks as well as to functions.

so you can have something like:

const result = 24

if (result == 24) {
    const result = 36
    console.log(result) // 36
}

let const = 48 // error

THIS

this is one of javascript’s quirkiest quirks.

functions and objects in javascript have a context that you can access with the keyword this.

const fruit = {
    name: 'banana',
    color: 'yellow',
    print() {
        console.log(`I am a ${this.color} ${this.name}!`)
    }
}

fruit.print() // I am a yellow banana!

const anotherfruit = { name: 'apple', color: 'red' }
anotherfruit.print = fruit.print
anotherfruit.print() // I am a red apple!

There is a family of function methods, bind, apply, and call , that take a this context as its first argument.

The one time I have continued to run into this over and over again is with eventlisteners. Sometimes when you are adding an event listener to an element, you want to be able to pass some variables into the callback function to do something with them.

This won’t work because despite your wishful thinking, the callback function is passed the event object as its first parameter:

const fruit = {
    name: 'banana',
    color: 'yellow',
}

function handleClick(fruit) {
    window.alert(fruit.name) // doesn't work
}

document.querySelector('#clickybutton')
    .addEventListener('click', handleClick(fruit));

But using bind will work!

const fruit = {
    name: 'banana',
    color: 'yellow',
}

function handleClick(fruit) {
    window.alert(fruit.name)
}

document.querySelector('#clickybutton')
    .addEventListener('click', handleClick.bind(fruit)); // works!

ARROW FUNCTIONS

Of course there are now arrow functions. These don’t just look cool. They also also close over their parent scope.

const fruit = {
    name: 'banana',
    color: 'yellow',
    print() {
        console.log(`I am a ${this.color} ${this.name}!`)
    }
}
fruit.print() // I am a yellow banana!
              // this === fruit

const fruit = {
    name: 'banana',
    color: 'yellow',
    print: () => {
        console.log(`I am a ${this.color} ${this.name}!`)
    }
}
fruit.print() // I am a undefined !
              // this === Window