Область видимости (Scope) & Scope chain

Концепции scope и что это

  • Scope или scoping(Область действия или видимости): Это определение доступности переменных и функций внутри определенных блоков кода. Существует глобальная область действия, область действия функций и облатсь действия блока.

  • Lexical scoping(scope)(Лексическая область видимости): контролируется размещением функций и блоков в коде. Основная суть в том, что дочерние функции лексически связаны с окружением их родителей и дочернии функции будут иметь доступ к переменным функции родителя. И то, что переменная существует только внутри своей функции, и к ней нельзя получить доступ извне. Лексическую область иногда также называют статической областью из за того что область видимости определяется в момент определения функции (в отличие от динамической области видимости, при которой это происходит в момент вызова функции)

В общем, если лексическое окружение это про то, где переменные находятся, то теперь мы говорим про то, где они доступны.

Виды scope

Как мы уже ранее сказали, существует три вида области действия, сейчас разберем их подробнее.

Global scope

Глобальная область действия относится к коду верхнего уровня.

  • Переменные вне любой функции или блока.
  • Переменные, объявленные в глобальном scope, доступны везде
const hello = 'hello'; // глобальная переменная
console.log(hello) // hello

function hi() {
    console.log(hello) // переменная доступна
}

hi() // hello 

Function scope(local scope)

Каждая функция образует scope функций. Это как бы в противоположность глобальному scope поэтому и называется так же локальным scope.

  • Переменные доступны только внутри функции, не снаружи.
  • Если вы попытаетесь получить доступ к любой переменной, определенной внутри функции, извне или из другой функции, вы получите ошибку.
function hi() {
    const hello = 'hello';
    console.log(hello);
    return hello;
}
hi();  // hello

// если попробуем снаружи, то получим ошибку
console.log(hello) // ReferenceError: hello is not defined

Block scope(es6)

Раньше только функции могли создавать свой scope, но начиная с es6, scope имеют любые блоки кода.

  • Переменные доступны только внутри блока кода.
  • Это применимо только к перемнным let и const.

До es6 существовал только глобальный scope и scope функций. Переменные var была как раз переменной области действия функции. То есть var была доступна везде в глобальном коде, кроме функций. Как раз вспоминаем про variable environment. А в es6 появился block scope и переменные const и let - они переменные области действия блока. Они доступны только в рамках блока кода(цикл, функция, просто блок и тд).

if (true) { // блок кода
    const one = 1;
    let two = 2;
    var three = 3; // var будет доступна и извне
    console.log(one); // 1
    console.log(two); // 2
    console.log(three); // 3
}

console.log(one); // ReferenceError
console.log(two); // ReferenceError
console.log(three); // 3  наша переменная var
  • Функции так же имеют scope блока, но только в strict mode
{ // блок кода
    function hi() {
        console.log('hi');
    }
}

// наша функция доступна
hi(); // hi

Но если мы будем использовать "use strict", то мы не сможем вызвать функцию, так как она станет ограничена блоками кода.

"use strict";

{
    function hi() {

        console.log('hi');
    }
}

hi(); // ReferenceError

Scope chain(Цепь областей видимости)

  • Scope chain – это цепочка лексических окружений.

Вспомним концептуальное представление lexical environment из урока про лексическое окружение. Смотрим на свойство outer.

ExecutionContext:
// global EC
    LexicalEnvironment:
    // global LE
        hi -> 'hi'
        outer: null // вот эта ссылка ведет на родителя
            LexicalEnvironment:
                // if LE
                name -> 'Dima' 
                surname -> "Lubimyi"
                outer: global  // вот эти ссылки и образуют цепь областей видимости

Пример с функциями:

let c = 3;
function a() {
    let a = 1;
    console.log(c); // 3
    function b() {
        let b = 2;
        console.log(a); // 1
        console.log(с); // 3
    }

    b();
}

a();

Теперь разберем каждый контекст:

// глобальный контекст
ExecutionContext:
// global EC
    LexicalEnvironment:
    // global LE
        c -> 3
        outer: null 

// контекст функции a
ExecutionContext:
// fun a EC
    LexicalEnvironment:
    // fun a LE
        a -> 1
        // ищет в глобальной области
        outer: global  

// контекст функции b
ExecutionContext:
// fun b EC
    LexicalEnvironment:
    // fun b LE
        b -> 2
        // ищет в области функции a
        outer: <function a>   

Когда во время фазы выполнения компилятор ищет переменную в лексическом окружении и не находит ее, он идет по ссылке в свойсте outer которая ссылается на родителя и ищет переменную там, если ее нет и там, то так дальше по цепи. Если переменная не найдена ни в одном из лексических окружений, возникает ошибка. Это и есть scope chain. А сам процесс называется разрешение идентификаторов(identifier resolution)

Посмотрим еще на один пример:

function f1() { // родительская функция
    const all = `ВСЕМ`;
    function hi() { // дочерняя функция
        const hello = 'ПРИВЕТ';
        console.log(`${all} ${bye}`);
    }
    hi()
    function bye() { // еще дочерняя функция
        const bye = 'ПОКА';
        console.log(`${all} ${hello}`);
    }
    bye()
}

f1();

В коде выше будет ошибка, так как мы пытаемся получить переменные одной дочерней функции у другой дочерней функции в том же scope. Но обе эти функции имеют доступ к переменной их родительской функции. Происходит это из за правил lexical scoping, не одна из этих функций не записана друг в друге, у них один родитель и доступ они имеют только к нему.

17.01.2022Обновлено 20.03.2023