JavaScript для самых маленьких. Часть 2

в 7:01, , рубрики: javascript, Блог компании ГК ЛАНИТ, Ланит, Программирование, разработка

Не так давно в блоге ЛАНИТ на Хабре мы представили вашему вниманию часть нашей методички по JavaScript. Сегодня же предлагаем ознакомиться с ещё одним блоком, который затрагивает самые базовые и необходимые понятия для тех, кто только начинает свой путь в мире JavaScript. Первую часть можно найти тут ― ну, чтобы всё слилось воедино. Продолжаем?

JavaScript для самых маленьких. Часть 2 - 1

Лексическое окружение

Никто не знает, как рассказать про лексическое окружение (ЛО), даже если слышал такой термин. Тут и пошутить не о чем.

Официальная спецификация ES6 определяет этот термин так:

Lexical Environment — это тип спецификации, используемый для разрешения имён идентификаторов при поиске конкретных переменных и функций на основе лексической структуры вложенности кода ECMAScript. Лексическое окружение (Lexical Environment) состоит из записи среды и, возможно, нулевой ссылки на внешнюю лексическую среду.

Сложно + не понятно, как запомнить = плохо. Давайте попроще.

Технически ЛО представляет собой объект с двумя свойствами:

  • запись окружения (именно тут хранятся все объявления),

  • ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).

Это служебный объект, и получить к нему доступ напрямую мы никак не можем.

Глобальное лексическое окружение хранит объявление переменных и функций, которые были созданы в глобальном контексте выполнения. Ссылка же на внешнее ЛО у него равна null (потому что нет ничего «более глобального»).

Рассмотрим пример выполнения функции:

    let outer = 'Hello';

function sayHelloDearFriend(name) {
  let inner = 'dear friend'
  console.log(`${outer}, ${inner}, ${name}!` )
}

sayHelloDearFriend('Chester');

Как выглядит лексическое окружение функции sayHelloDearFriend:

JavaScript для самых маленьких. Часть 2 - 2

Итак, лексическое окружение функции состоит из записи окружения, в которой хранятся как переменные, созданные внутри этой функции, так и параметры, пришедшие при вызове функции. Кстати, это явно указывает на то, что лексическое окружение будет при каждом вызове функции создаваться заново, так как функция может быть вызвана с разными входными параметрами. Таким образом, мы можем при работе функции получить доступ к name и inner, не выходя за пределы записи окружения самой функции, но вот переменную outer там найти не сможем. Настало время ссылки на внешнее окружение. С её помощью мы можем получить внешнее лексическое окружение и уже оттуда достать переменную outer.

Маленький бонус. На иллюстрации видно, что декларация функции находится во внешнем окружении функции. Здесь, думаю, не надо ничего объяснять, зато сразу становится понятно, как мы найдём функцию, если будем работать с рекурсией. При  вызове функции внутри самой себя мы найдем её, пройдя по ссылке во внешнее лексическое окружение.

На иллюстрации показан простой пример с функцией, созданной сразу в глобальном контексте, так что у её внешнего лексического окружения ссылка на внешнее окружение равна null. Давайте усложним.

      let outer = 'Hello';

function sayHelloDearFriend() {
  let inner = 'dear friend'
  return function sayHello(name) {
    console.log(`${outer}, ${inner}, ${name}!` )
          }
}

let say = sayHelloDearFriend();
        say('Chester')

Вот так вот мы всё усложнили.

А теперь распишем всё в порядке следования:

JavaScript для самых маленьких. Часть 2 - 3

Таким образом, по ссылке на внешнее окружение мы можем получить окружение «отца», за ним по его ссылке ― «окружение деда», потом «прадеда» и так далее, пока не дойдём до глобального окружения.

Глобальное ЛО существует на всем протяжении жизни скрипта, а вот лексические окружения функции уничтожаются по мере выполнения функции, так как при последующем её вызове будет создано новое окружение. Нет смысла хранить старое, надо освобождать место в памяти (если ваш шкаф забит, то нужно выкинуть старые вещи, чтоб поместились новые).

HOISTING

Даже те, кто знает про всплытие, часто заблуждаются. Происходит это в основном, когда отвечают на вопрос про различия let и var (какой казалось бы, простой и сложный одновременно вопрос), о котором говорилось уже в рамках рассказа про области видимости. И вот оно, заблуждение: «var поднимаются, а let ― нет».

А теперь, собственно, о поднятии.

Откуда оно взялось? Всё достаточно банально и просто, если прочесть предыдущую главу. В момент создания глобального лексического окружения декларация функций и переменных уже попадает в него, а оно происходит перед непосредственно выполнением. С одной стороны, очень удобно иметь возможность вызвать функцию выше по коду, чем она была задекларирована. Так как у js сильный функциональный уклон, это здорово выручает, с другой стороны - получаем вот такие вещи:

        console.log(a);
var a;

Вывод в консоль: 

> undefined

И тут уже один java-разработчик (ну или любой другой бэкендер, который решил «вырасту ― фулстеком буду»), решивший изучить JavaScript, бросает эту идею и идет рассказывать всем, какой JavaScript ужасный язык и «как только люди на нём пишут».

Однако разработчики JavaScript признали свою оплошность и порадовали всех новыми способами создания переменных let, который добавили в es6. С ним уже не забалуешь.

        console.log(a);
let a;

Вывод в консоль: 

> ReferenceError: a is not defined

Вот так хорошо. Так правильно. И тут же сделали вывод, что значит let не всплывает. Так и будем всем говорить. Но давайте обратимся к самому официальному источнику. Согласно 13ECMAScript Language: Statements and Declarations (ссылку прилагаю).

The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.

Что означает, что они всё так же всплывают, но, учитывая ошибки прошлого, разработчики сделали так, что доступ к ним не может быть получен, пока не присвоено какое-либо значение, что произойдёт только когда по коду мы дойдём до этого присвоения и не раньше. А если какая-то функция вызвана раньше, то такое присвоение произойти не успело, а значит, будет ошибка.

И отсюда сразу вопрос: тогда получается, в данной ситуации тоже будет error?

      let a;
console.log(a);
a = 2;

Вроде как привязка значения идет ниже по коду? Но нет, не всё так просто. Тут будет undefined, потому что конструкция let a считается идентичной конструкции let a = undefined.

Маленькое лирическое отступление.

Раз уж затронули тему отличия let от var, то необходимо упорядочить и перечислить всё.

  1. Области видимости: у let ― блочная, у var ― функциональная.

  2. Поведение при HOISTING: к var можно обратиться до момента создания, и это не вызовет ошибку. Если повторить то же с let, то получим reference error.

  3. Пересоздание переменной: переменную, созданную с помощью var, можно пересоздать. Это не вызовет ошибки, просто актуальным станет последнее значение, установленное при создании. При попытке повторить это с let получим uncaught syntax error.

        var a = 1
var a = 2
console.log(a)

Вывод в консоль:

> 2

        let b = 1
let b = 2
console.log(b)

Вывод в консоль:

> Uncaught SyntaxError: Identifier 'b' has already been declared

Замыкания

Замыкания ― явление воистину великолепное и прекрасное.

Самое простое определение, которое можно найти в интернете, действительно очень простое и легко запоминается:

Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определена. Другими словами, замыкание даёт вам доступ к Scope внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз при создании функции, во время её создания.

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

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

Итак, мы помним, что для функции лексическое окружение создаётся в момент её вызова, чтобы собрать все актуальные данные. Если функция была вызвана внутри другой, то она получает ссылку на внешнее лексическое окружение, которым будет являться как раз ЛО внешней функции. Но что, если функция задекларирована (создана) внутри другой функции? Нужна картинка, пусть код будет нам уже слегка знаком по главе о лексическом окружении.

Сперва напомню тот код, что мы уже видели:

        let outer = 'Hello';

function sayHello(text) {
  console.log(`${outer}, ${text}!` )
}

function sayHelloDearFriend(name) {
  let inner = 'dear friend'
  sayHello(`${inner}, ${name}`)
}

sayHello('Mike');
sayHelloDearFriend('Chester');

А теперь давайте его преобразим, чтобы он больше подходил под ситуацию.

function sayHello(name) {
  let hello = 'Hello'

  function say() {
    console.log(`${hello}, ${name}!` )
  }

  say()
}

sayHello('Chester');

В JavaScript замыканием является любая функция. Для иллюстрации возможностей данного функционала и большей наглядности предлагаю рассматривать частный случай. Необходимое нам замыкание было создано, когда и функция say() внутри функции sayHello. Пункту про лексическое окружение это никак не противоречит. Внутри функции say нет никаких переменных, поэтому переменные hello и name будем искать через ссылку на внешнее окружение, которым как раз является ЛО функции sayHello(). Оттуда мы забираем нужные переменные, и ничего интересного не происходит.

Пойдём дальше.

Сделаем так, чтобы функция sayHello не вызывала внутри себя функцию say, а возвращала её в результате выполнения: 

function sayHello(name) {
  let hello = "Hello";

  function say() {
    console.log(`${hello}, ${name}!`);
  }

  return say;
}

let sayHelloChester = sayHello("Chester");

sayHelloChester();
sayHelloChester();
sayHelloChester();

То есть мы вернули результат выполнения sayHello(‘Chester’) в переменную sayHelloChester. И хотя функция sayHello уже отработала, по идее, её лексическое окружение должно быть удалено сборщиком мусора, так как мы помним, что ЛО создаётся заново каждый раз при вызове функции, а лексическое окружение отработавшей функции удаляется за ненадобностью. Сейчас мы видим абсолютно другую картину. Сколько бы раз мы не вызвали теперь  sayHelloChester() у нас будет доступ внутри неё и к hello, и к name, который вообще параметром передавался.

Почему? Все просто. Определив функцию say внутри функции sayHello, мы замкнули (в прямом смысле замкнули, никаких переносных значений) запись окружения функции sayHello на неё. И теперь, пока существует sayHelloChester, который является по факту функцией say, со знанием того лексического окружения, в котором её создали, мы запретили сборщику мусора удалять это ЛО. Он не может удалить переменную, пока на неё хоть кто-то ссылается.

С большой силой приходит большая ответственность. Теперь мы понимаем, что пока существует sayHelloChester, он будет хранить в памяти информацию, необходимую для корректной работы. Да, в нашем примере её не то чтобы много, но в реальных может быть достаточно. А потому такие вещи, как sayHelloChester, нужно принудительно уничтожать, если в них больше нет необходимости.

Бонус. На собеседованиях с лайвкодингом есть банальная задача на замыкания, которую часто можно встретить. Она обычно называется «Функция счетчик». Нужно написать функцию, которая при каждом её вызове будет выводить в консоль количество раз, которые её вызвали, при этом нельзя создавать никакие переменные вне этой функции, также нельзя использовать объект window для хранения промежуточных значений.

Вот, собственно, так она выглядит:

let counter = (function(){
  let counter = 0
  return function() {
    console.log(++counter)
  }
})()

counter();
counter();
counter();
counter();

Вывод в консоль:

> 1
> 2
> 3
> 4

Пример банальный, но он хорошо демонстрирует, что замкнутое лексическое окружение не просто висит в памяти мертвым грузом. Мы можем как получать, так и изменять значения переменных внутри.

Таким образом, замыкания ― это механизм, который позволяет функциям запоминать лексическое окружение, в котором они были созданы. С его помощью в JavaScript можно делать следующие вещи: инкапсуляция (сокрытие какого-либо функционала и данных из глобальной области видимости) и состояние (у функции, которая находится в переменной counter, есть своё состояние, которое модифицируется при каждом вызове счетчика).

Надеемся, что вторая часть нашей методички окажется не менее полезной первой. И главное, что нужно запомнить, ― лучше потратить время и хорошенько разобраться, чем бездумно зубрить правила и определения из интернета.

Автор: Александра Липинская

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js