Часть 3 из трех частей серии SSENSE-TECH, посвященной основам JavaScript. Также прочтите часть 1, посвященную использованию ключевого слова this, и часть 2, посвященную асинхронному выполнению.

В этой третьей и последней статье серии я расскажу о замыканиях. Замыкания учитывают ключевой принцип объектно-ориентированного программирования: инкапсуляцию. Однако, чтобы понять, как работают замыкания, нам сначала нужно понять, что такое область действия и как объявление и доступ к переменной работает в JavaScript.

ОБЪЕМ

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

Механизмы JavaScript выполняют этап компиляции, а определение области действия происходит во время компиляции. Слишком глубокое рассмотрение процесса компиляции не входит в задачу этой статьи, но нам необходимо помнить о некоторых его деталях с точки зрения высокого уровня. Во-первых, нам нужно вспомнить, что такое лексическое окружение.

ЛЕКСИЧЕСКОЕ ОКРУЖЕНИЕ

Для каждого блока кода { … } существует связанное с ним лексическое окружение, в котором хранятся ссылки на все переменные, доступные для этого кода. Вы можете представить его как объект, содержащий словарь переменных и их соответствующих значений. Исполняемый код запросит у своего лексического окружения значение любой необходимой ему переменной. Лексическое окружение также хранит ссылку на непосредственное внешнее лексическое окружение (то есть на то, которое его содержит), так что, если переменная не может быть найдена в определенном лексическом окружении, она будет последовательно искаться во внешних, пока не будет найдена или пока не будет достигнут конец цепочки, в этом случае будет выдана ошибка. Процесс поиска в каждой внешней лексической области видимости перед выбрасыванием называется цепочкой областей видимости.

В приведенном выше примере переменная nameJohn объявлена ​​и находится внутри лексического окружения функции.

Во втором примере namePeter не объявляется внутри функции, поэтому поиск осуществляется во внешнем лексическом окружении, в данном случае в глобальной области видимости.

ОБЪЯВЛЕНИЕ ПЕРЕМЕННОЙ И ПОДЪЕМ

В JavaScript существует три разных способа объявить переменную: с ключевым словом var, с ключевым словом let и с ключевым словом const. Одним из основных отличий этих ключевых слов является то, как их объявление и присвоение значения выполняются в процессе, называемом подъемом.

Подъем выполняется во время компиляции. На этом этапе все объявления и назначения переменных разделяются, и только объявления перемещаются в начало соответствующего им блока кода. Это позволяет получить доступ к этим переменным различными способами, как описано ниже.

Объявление переменной с помощью var перемещает объявление переменной в начало содержащей ее функции или в начало скрипта, если все находится на уровне глобальной области видимости. Это позволяет вести себя следующим образом:

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

Объявление переменной с помощью let перемещает объявление переменной в верхнюю часть содержащего ее блока кода, то есть в скобки, которые заключают ее. Есть одна загвоздка с let, которая заключается в том, что переменная не получает значение инициализации по умолчанию, такое как var, и будет выдана ошибка, если к ней будет осуществлен доступ до того, как будет присвоено фактическое значение.

Объявление переменной с использованием const будет иметь те же правила подъема, что и let; однако, как мы видим ниже, const нельзя переназначить другое значение:

Имейте в виду, что хотя переназначение объекта невозможно при использовании const, его изменение все же возможно:

ФУНКЦИОНАЛЬНЫЙ ОБЪЕМ

Функции тоже поднимаются, но они полностью доступны до их объявления, а это означает, что им не присваивается значение по умолчанию undefined. Вы можете вызывать функции до того, как они будут объявлены, и выполнение будет вести себя так же, как если бы вы вызывали их позже. См. этот пример:

При вызове функции создается новое лексическое окружение специально для этого вызова функции. Любые другие вызовы одной и той же функции будут создавать разные лексические среды. Это помогает сохранить ссылку на значения переменных и параметров для конкретного вызова. См. пример ниже:

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

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

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

ЗАКРЫТИЕ

Как описано ранее, каждое лексическое окружение имеет указатель на внешнее лексическое окружение или указатель на null, если оно является самым внешним. В случае функции, возвращаемой другой функцией, ее собственное лексическое окружение определяет созданные внутри нее переменные, а также указывает на лексическое окружение функции, которое закрывает это.

Было бы интуитивно думать, что после того, как объемлющая функция завершит выполнение, любая переменная внутри нее будет очищена сборщиком мусора — в JavaScript очищается все, на что не указывает ссылка, — но из-за того, как лексическое окружение работает, на внутренние переменные объемлющей функции по-прежнему ссылаются экземпляры возвращаемой функции. Давайте посмотрим на этот классический пример:

Здесь мы видим, что функция makeAdder запускается дважды с разными параметрами. Каждое выполнение makeAdder создает отдельную лексическую среду со значением параметра baseAdderValue, установленным на что-то конкретное для вызова. Первый экземпляр созданных функций получит аргумент numberToAddToBase и будет искать, что означает baseAdderValue. Она пройдет по цепочке областей видимости и найдет baseAdderValue в непосредственном внешнем лексическом окружении со значением 10. Для второго экземпляра функции сумматора она будет искать baseAdderValue и, как и в первом сценарии, найдет его в непосредственном внешнем лексическом окружении. лексическое окружение, но на этот раз со значением 2.

Другими словами, мы можем скрыть доступ к переменным и обрабатывать их только через открытые функции. Это полезно для достижения фундаментальной концепции объектно-ориентированного программирования: инкапсуляции. Замыкания позволяют имитировать поведение частных свойств или методов, чего раньше не предлагал JavaScript.

ШАБЛОН МОДУЛЯ

Модуль — это набор связанных данных и функций, характеризующийся разделением между скрытыми частными данными и общедоступными данными, последние также известны как «публичный API». Модули подразумевают управление доступом с отслеживанием состояния, что в основном означает сохранение внутреннего состояния и предоставление только того, что необходимо (принцип наименьшего раскрытия). В следующем примере показано, как мы можем превратить простой объект JavaScript со свойствами и методами в модуль с общедоступным API, который предоставляет только то, что необходимо для общедоступности.

Этот объект имеет одно свойство, внутреннее состояние products, в котором хранится список продуктов. У него также есть один метод: findProduct. Объект делает то, для чего предназначен, но проблема в том, что он раскрывает свое внутреннее состояние. Следовательно, любой, кто с ним взаимодействует, может изменить внутреннее состояние, и инкапсуляция не достигается. Теперь давайте посмотрим, как замыкания помогают нам создать модуль, контролирующий доступ к своему внутреннему состоянию.

С этим изменением внутреннее состояние products доступно только внутри модуля, и есть единственный общедоступный метод: findProduct. Это значительное улучшение, потому что теперь другие объекты, которые взаимодействуют со средством поиска продуктов, не смогут получить доступ к его внутреннему состоянию и не будут знать процесс доступа ко всем продуктам, чтобы они могли наконец найти.

ПОСЛЕДНИЕ МЫСЛИ

Хотя в настоящее время существуют альтернативы для достижения надлежащей инкапсуляции и соблюдения принципа наименьшего воздействия, замыкания остаются очень важной темой в языке. В этой серии статей SSENSE-TECH мы рассмотрели, насколько JavaScript является очень гибким языком, дающим большую свободу при разработке приложений; это может быть хорошо и плохо одновременно. Несмотря на то, что мы можем довольно быстро приступить к работе, мы также можем создавать код низкого качества с той же скоростью. В SSENSE мы выбираем альтернативу TypeScript, но если вы работаете с устаревшим кодом или хотите придерживаться JavaScript, рекомендуется помнить о замыканиях как о способе управления доступом к вашим объектам.

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

ДОПОЛНИТЕЛЬНЫЕ РЕСУРСЫ И ССЫЛКИ ПО ТЕМЕ

Редакционные обзоры Liela Touré и Pablo Martinez

Хотите работать с нами? Нажмите здесь, чтобы увидеть все открытые вакансии в SSENSE!