- Области видимости и замыкания в JavaScript
- Область видимости
- Глобальная область видимости
- Локальная область видимости
- Область видимости функции
- Область видимости блока
- Подъем функции в области видимости
- У функций нет доступа к областям видимости других функций
- Вложенные области видимости
- Замыкания
- Контроль побочных эффектов с помощью замыканий
- Приватные переменные с замыканиями
- Отладка областей видимости с помощью DevTools
Области видимости и замыкания в JavaScript
Области видимости и замыкания важны в JavaScript, однако они сбивали меня с толку, когда я только начинал их изучать. Ниже приведены объяснения этих терминов, которые помогут вам разобраться в них.
Начнем с областей видимости
Область видимости
Область видимости в JavaScript определяет, какие переменные доступны вам. Существуют два типа областей видимости: глобальная и локальная.
Глобальная область видимости
Если переменная объявлена вне всех функций или фигурных скобок ( <> ), то считается, что она определена в глобальной области видимости.
Примечание: это верно только для JavaScript в веб браузерах. В Node.js глобальные переменные объявляются иначе, но мы не будем касаться Node.js в этой статье.
const globalVariable = 'some value';
Как только происходит объявление глобальной переменной, можно использовать эту переменную везде в коде, даже в функциях.
const hello = 'Hello CSS-Tricks Reader!'; function sayHello () < console.log(hello); >console.log(hello); // 'Hello CSS-Tricks Reader!' sayHello(); // 'Hello CSS-Tricks Reader!'
Хотя можно объявлять переменные в глобальной области видимости, но не рекомендуется это делать. Всё из-за того, что существует вероятность пересечения имен, когда двум или более переменным присваивают одинаковое имя. Если переменные объявляются через const или let , то каждый раз, когда будет происходить пересечение имён, будет показываться сообщение об ошибке. Такое поведение нежелательно.
// Не делайте так! let thing = 'something'; let thing = 'something else'; // Ошибка, thing уже была объявлена
Если объявлять переменные через var , то вторая переменная после объявления перепишет первую. Такое поведение тоже нежелательно, т.к. код усложняется в отладке.
// Не делайте так! var thing = 'something'; var thing = 'something else'; // возможно где-то в коде у переменной совершенно другое значение console.log(thing); // 'something else'
Итак, следует всегда объявлять локальные переменные, а не глобальные.
Локальная область видимости
Переменные, которые используются только в определенной части кода, считаются помещенными в локальную область видимости. Такие переменные называются локальными.
В JavaScript выделяют два типа локальных областей видимости:
Сначала рассмотрим область видимости функции
Область видимости функции
Переменная, объявленная внутри функции, доступна только внутри функции. Код снаружи функции не имеет к ней доступа.
В примере ниже, переменная hello находится внутри области видимости функции sayHello :
function sayHello () < const hello = 'Hello CSS-Tricks Reader!'; console.log(hello); >sayHello(); // 'Hello CSS-Tricks Reader!' console.log(hello); // Ошибка, hello не определена
Область видимости блока
Переменная, объявленная внутри фигурных скобок <> через const или let , доступна только внутри фигурных скобок.
В примере ниже, можно увидеть, что переменная hello находится внутри области видимости фигурных скобок:
< const hello = 'Hello CSS-Tricks Reader!'; console.log(hello); // 'Hello CSS-Tricks Reader!' >console.log(hello); // Ошибка, hello не определена
Блочная область видимости является частным случаем области видимости функции, т.к. функции объявляются с фигурными скобками (кроме случаев использования стрелочных функций с неявным возвращением значения).
Подъем функции в области видимости
Функции, объявленные как «function declaration» (прим. перев.: функция вида function имя(параметры) ), всегда поднимаются наверх в текущей области видимости. Так, два примера ниже эквивалентны:
// Тоже самое, что пример ниже sayHello(); function sayHello () < console.log('Hello CSS-Tricks Reader!'); >// Тоже самое, что пример выше function sayHello () < console.log('Hello CSS-Tricks Reader!'); >sayHello();
Если же функция объявляется как «function expression» (функциональное выражение) (прим. перев.: функция вида var f = function (параметры) ), то такая функция не поднимается в текущей области видимости.
sayHello(); // Ошибка, sayHello не определена const sayHello = function ()
Из-за этих двух возможных вариантов подъем функции потенциально может сбить с толку, поэтому не рекомендуется применять на практике. Всегда сначала объявляйте функции перед тем, как их использовать.
У функций нет доступа к областям видимости других функций
Функции не имеют доступа к областям видимости других функций, когда они объявляются раздельно, даже если одна функция используется в другой.
В примере ниже функция second не имеет доступа к переменной firstFunctionVariable .
function first () < const firstFunctionVariable = `I'm part of first`; >function second () < first(); console.log(firstFunctionVariable); // Ошибка, firstFunctionVariable не определена. >
Вложенные области видимости
Когда функция объявляется в другой функции, то внутренняя функция имеет доступ к переменным внешней функции. Такой поведение называется разграничением лексических областей видимости.
В тоже время внешняя функция не имеет доступа к переменным внутренней функции.
function outerFunction () < const outer = `I'm the outer function!`; function innerFunction() < const inner = `I'm the inner function!`; console.log(outer); // I'm the outer function! >console.log(inner); // Ошибка, inner не определена >
Для визуализации того, как работают области видимости, можно представить одностороннее зеркало. Вы можете видеть тех, кто находится с другой стороны, но те, кто стоят с обратной стороны (зеркальной стороны), не могут видеть вас.
Если одни области видимости вложены в другие, то это можно представить как множество стеклянных поверхностей с принципом действия, описанным выше.
Если вы поняли все, что касается областей видимости, то можно сказать, что вы готовы к тому, чтобы разобраться с тем, что такое замыкания.
Замыкания
Всякий раз, когда вы вызываете функцию внутри другой функции, вы создаете замыкание. Говорят, что внутренняя функция является замыканием. Результатом замыкания обычно является то, что в дальнейшем становятся доступными переменные внешней функции.
function outerFunction () < const outer = `I see the outer variable!`; function innerFunction() < console.log(outer); >return innerFunction; > outerFunction()(); // I see the outer variable!
Так как внутренняя функция является возвращаемым значением внешней функции, то можно немного сократить код, совместив возврат значения с объявлением функции.
function outerFunction () < const outer = `I see the outer variable!`; return function innerFunction() < console.log(outer); >> outerFunction()(); // I see the outer variable!
Благодаря замыканиям появляется доступ к внешней функции, поэтому они обычно используются для двух целей:
Контроль побочных эффектов с помощью замыканий
Побочные эффекты появляются, когда производятся какие-то дополнительные действия помимо возврата значения после вызова функции. Множество вещей может быть побочным эффектом, например, Ajax-запрос, таймер или даже console.log:
Когда замыкания используются для контроля побочных эффектов, то, как правило, обращают внимание на такие побочные эффекты, которые могут запутать код (например, Ajax-запросы или таймеры).
Для пояснения рассмотрим пример
Допустим, требуется приготовить торт ко дню рождения вашего друга. Приготовление торта займет секунду, так как написанная функция выводит «торт испечён» через секунду.
Примечание: для краткости и простоты далее используются стрелочные функции из ES6.
function makeCake() < setTimeout(_ =>console.log(`Made a cake`), 1000); >
Как можно заметить, такая функция имеет побочный эффект в виде таймера.
Далее допустим, вашему другу нужно выбрать вкус торта. Для этого нужно дописать «добавить вкус» к функции makeCake .
function makeCake(flavor) < setTimeout(_ =>console.log(`Made a $ cake!`), 1000); >
После вызова функции торт будет испечён ровно через секунду.
makeCake('banana'); // Made a banana cake!
Проблема в том, что, допустим, не нужно, чтобы торт был испечён сразу после уточнения вкуса, а необходимо, чтобы торт был испечён позже, когда это потребуется.
Для решения этой проблемы можно написать функцию prepareCake , которая будет хранить вкус торта. Затем передать замыкание в makeCakeLater через prepareCake .
С этого момента можно вызывать возвращенную функцию в любое время, когда это требуется, и торт будет приготовлен через секунду.
function prepareCake (flavor) < return function () < setTimeout(_ =>console.log(`Made a $ cake!`), 1000); > > const makeCakeLater = prepareCake('banana'); // Позже в вашем коде. makeCakeLater(); // Made a banana cake!
Так замыкания используются для уменьшения побочных эффектов — вызывается функция, которая активирует внутреннее замыкание по вашему желанию.
Приватные переменные с замыканиями
Как вы теперь знаете, переменные, созданные внутри функции, не могут быть доступны снаружи. Из-за того, что они не доступны, их также называют приватными переменными.
Однако иногда требуется доступ к такой приватной переменной, и для этого используются замыкания.
function secret (secretCode) < return < saySecretCode () < console.log(secretCode); >> > const theSecret = secret('CSS Tricks is amazing'); theSecret.saySecretCode(); // 'CSS Tricks is amazing'
В примере выше saySecretCode — единственная функция (замыкание), которая выводит secretCode снаружи исходной функции secret. По этой причине такую функцию называют привилегированной.
Отладка областей видимости с помощью DevTools
Инструменты разработчика (DevTools) Chrome и Firefox упрощают отлаживание переменных в текущей области видимости. Существует два способа применения этого функционала.
Первый способ: добавлять ключевое слово debugger в код, чтобы останавливать выполнение JavaScript кода в браузерах с целью дальнейшей отладки.
Ниже пример с prepareCake :
function prepareCake (flavor) < // Добавляем debugger debugger return function () < setTimeout(_ =>console.log(`Made a $ cake!`), 1000); > > const makeCakeLater = prepareCake('banana');
Если открыть DevTools и перейти во вкладку Sources в Chrome (или вкладку Debugger в Firefox), то можно увидеть доступные переменные.
Можно также переместить debugger внутрь замыкания. Обратите внимание, как переменные области видимости изменяться в этот раз:
function prepareCake (flavor) < return function () < // Добавляем debugger debugger setTimeout(_ =>console.log(`Made a $ cake!`), 1000); > > const makeCakeLater = prepareCake('banana');
Второй способ: добавлять брейкпоинт напрямую в код во вкладке Sources (или Debugger) путем клика на номер строки.
- Области видимости и замыкания не настолько сложны для понимания, как кажется. Они достаточно просты, если рассматривать их через принцип одностороннего зеркала.
- После объявления переменной в функции доступ к ней возможен только внутри функции. Такие переменные называют определенными в контексте этой функции.
- Если вы объявляете любую внутреннюю функцию внутри другой функции, то внутренняя функция называется замыканием, т.к. эта функция сохраняет доступ к переменным внешней функции.