Scope Chain

YDKJS

Parminder Singh

Senior Frontend Engineer @ Finder UK

Scope Chain

  •  Connections between scopes that are nested within other scopes

Scope Chain

  • Path along which variables can be accessed
  • Directed chain - the lookup moves upward/outward only

"Lookup" Is (Mostly) Conceptual

"Lookup" Is (Mostly) Conceptual

  • "Lookup" - runtime access of a variable
  •  Runtime lookup process works well for conceptual understanding
  • Color of a marble's bucket - "usually determined" during the initial compilation process
  • Based on lexical scoping - finalised at that point - nothing will change it later during runtime.
  • Since Marble colors are known - immutable - info is stored in variable entry in AST.
  • Runtime Engine doesn't need to lookup - already known - optimization benefit of lexical scope.

"Lookup" Is (Mostly) Conceptual

 Color of marble is "usually determined" - compile step

<script type="text/javascript" src="ngx-slick-carousel.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>

"Lookup" Is (Mostly) Conceptual

What happens when marble is uncolored?

  • Consider Variable ref that isn't declared in any lexically available scopes in the current file.
  • JS compilation - each file is its own separate program.
  • If no declaration is found, that's not necessarily an error.
  • Another file (program) in the runtime may indeed declare that variable in the shared global scope.
  • Ultimate determination - deferred to the runtime.
  • Undeclared is left as an uncolored marble
  • Deferred lookup will eventually resolve the color - but only once at runtime - immutable

"Lookup" Is (Mostly) Conceptual

Shadowing

  • Two or more variables, each in different scopes, with the same names.
  • Single scope can't have multiple variables with the same name, only one is assumed.
  • If need to use same name - different scopes (often nested).
  • Key aspect of lexical scope behavior
  • Impact of shadowing - for inward/downward scope, it's impossible for any marble to be colored as the shadowed variable.

Shadowing

var studentName = "Suzy";

function printStudent(studentName) {
    studentName = studentName.toUpperCase();
    console.log(studentName);
}

printStudent("Frank");
// FRANK

printStudent(studentName);
// SUZY

console.log(studentName);
// Suzy

Shadowing

var studentName = "Suzy";

function printStudent(studentName) {
    console.log(studentName);
    console.log(window.studentName);
}

printStudent("Frank");
// "Frank"
// "Suzy"

Global unshadowing trick - anti-pattern

Shadowing

var studentName = "Suzy";

function printStudent(studentName) {
    console.log(studentName);
    console.log(window.studentName);
}

printStudent("Frank");
// "Frank"
// "Suzy"

Global unshadowing trick - anti-pattern

  • window.studentName as a getter/setter that accesses the actual studentName variable
  • Only works for global scope - not for nested - and only for vars and functions.

Shadowing

var one = 1;
let notOne = 2;
const notTwo = 3;
class notThree {}

console.log(window.one);       // 1
console.log(window.notOne);    // undefined
console.log(window.notTwo);    // undefined
console.log(window.notThree);  // undefined

Shadowing

var special = 42;

function lookingFor(special) {
    // The identifier `special` (parameter) in this
    // scope is shadowed inside keepLooking(), and
    // is thus inaccessible from that scope.

    function keepLooking() {
        var special = 3.141592;
        console.log(special);
        console.log(window.special);
    }

    keepLooking();
}

lookingFor(112358132134);
// 3.141592
// 42

Shadowing

var special = 42;

function lookingFor(special) {
    var another = {
        special: special
    };

    function keepLooking() {
        var special = 3.141592;
        console.log(special);
        console.log(another.special);  // Ooo, tricky!
        console.log(window.special);
    }

    keepLooking();
}

lookingFor(112358132134);
// 3.141592
// 112358132134
// 42

Copying Is Not Accessing

what if I'd used objects or arrays as the values instead of the numbers (112358132134, etc.)? - still can't reassign special parameter

Shadowing

function something() {
    var special = "JavaScript";

    {
        let special = 42;   // totally fine shadowing

        // ..
    }
}

Illegal Shadowing

Shadowing

function another() {
    // ..

    {
        let special = "JavaScript";

        {
            var special = "JavaScript";
            // ^^^ Syntax Error

            // ..
        }
    }
}

Illegal Shadowing

let prohibits var to cross the boundaries (or hop over)

Shadowing

function another() {
    // ..

    {
        let special = "JavaScript";

        ajax("https://some.url",function callback(){
            // totally fine shadowing
            var special = "JavaScript";

            // ..
        });
    }
}

Illegal Shadowing

Boundary-crossing prohibition effectively stops at each function boundary

Shadowing

Illegal Shadowing

  • let (in an inner scope) can always shadow an outer scope's var.
  • var (in an inner scope) can only shadow an outer scope's let if there is a function boundary in between.
function something() {
    var special = "JavaScript";

    {
        let special = 42;   // totally fine shadowing

        // ..
    }
}

function another() {
    // ..

    {
        let special = "JavaScript";

        ajax("https://some.url",function callback(){
            // totally fine shadowing
            var special = "JavaScript";

            // ..
        });
    }
}

Function Name Scope

function askQuestion() {
    // ..
}

Function Declaration

Function Expression

var askQuestion = function(){
    // ..
};

What happens to the name identifier of the function?

var askQuestion = function ofTheTeacher(){
    // ..
};

Function Name Scope

What happens to the name identifier of the function?

var askQuestion = function ofTheTeacher() {
    console.log(ofTheTeacher);
};

askQuestion();
// function ofTheTeacher()...

console.log(ofTheTeacher);
// ReferenceError: ofTheTeacher is not defined

Function Name Scope

What happens to the name identifier of the function?

var askQuestion = function ofTheTeacher() {
    console.log(ofTheTeacher);
};

askQuestion();
// function ofTheTeacher()...

console.log(ofTheTeacher);
// ReferenceError: ofTheTeacher is not defined
  • ofTheTeacher is declared as an identifier inside the function itself
  • ofTheTeacher is not exactly in the scope of the function - Implied Scope

Function Name Scope

It's also read-only

var askQuestion = function ofTheTeacher() {
    "use strict";
    ofTheTeacher = 42;   // TypeError

    //..
};

askQuestion();
// TypeError
  • strict-mode, the assignment failure is reported as a TypeError;
  • in non-strict-mode, such an assignment fails silently with no exception.

Function Name Scope

Function expression with no name identifier?

var askQuestion = function(){
   // ..
};

Named function expression

Without name - anonymous function expression.

Arrow Functions

Shorter way to write functions

var askQuestion = () => {
    // ..
};
  • Lexically anonymous
var askQuestion = () => {
    // ..
};

askQuestion.name;   // askQuestion
  • Inferred name - but still anonymous 

Arrow Functions

Syntactic brevity

() => 42;

id => id.toUpperCase();

(id,name) => ({ id, name });

(...args) => {
    return args[args.length - 1];
};

Arrow Functions

Claim: Arrow functions somehow behave differently with respect to lexical scope from standard function functions.

Incorrect!

Arrow Functions

Claim: Arrow functions somehow behave differently with respect to lexical scope from standard function functions.

Incorrect!

  • Other than being anonymous (and having no declarative form), => arrow functions have the same lexical scope rules as function functions do.
  • An arrow function, still creates a separate, inner nested bucket of scope. Variable declarations inside this nested scope bucket behave the same as in a function scope.

Thank You

Scope Chain

By Param Singh