Области видимости и замыкания важны в 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 () {
console.log(aFunction);
}
Из-за этих двух возможных вариантов подъем функции потенциально может сбить с толку, поэтому не рекомендуется применять на практике. Всегда сначала объявляйте функции перед тем, как их использовать.
У функций нет доступа к областям видимости других функций
Функции не имеют доступа к областям видимости других функций, когда они объявляются раздельно, даже если одна функция используется в другой.
В примере ниже функция 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:
function (x) {
console.log('A console.log is a side effect!');
}
Когда замыкания используются для контроля побочных эффектов, то, как правило, обращают внимание на такие побочные эффекты, которые могут запутать код (например, Ajax-запросы или таймеры).
Для пояснения рассмотрим пример
Допустим, требуется приготовить торт ко дню рождения вашего друга. Приготовление торта займет секунду, так как написанная функция выводит «торт испечён» через секунду.
Примечание: для краткости и простоты далее используются стрелочные функции из ES6.
function makeCake() {
setTimeout(_ => console.log(`Made a cake`), 1000);
}
Как можно заметить, такая функция имеет побочный эффект в виде таймера.
Далее допустим, вашему другу нужно выбрать вкус торта. Для этого нужно дописать «добавить вкус» к функции makeCake
.
function makeCake(flavor) {
setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
}
После вызова функции торт будет испечён ровно через секунду.
makeCake('banana'); // Made a banana cake!
Проблема в том, что, допустим, не нужно, чтобы торт был испечён сразу после уточнения вкуса, а необходимо, чтобы торт был испечён позже, когда это потребуется.
Для решения этой проблемы можно написать функцию prepareCake
, которая будет хранить вкус торта. Затем передать замыкание в makeCakeLater
через prepareCake
.
С этого момента можно вызывать возвращенную функцию в любое время, когда это требуется, и торт будет приготовлен через секунду.
function prepareCake (flavor) {
return function () {
setTimeout(_ => console.log(`Made a ${flavor} 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 ${flavor} cake!`), 1000);
}
}
const makeCakeLater = prepareCake('banana');
Если открыть DevTools и перейти во вкладку Sources в Chrome (или вкладку Debugger в Firefox), то можно увидеть доступные переменные.
Можно также переместить debugger внутрь замыкания. Обратите внимание, как переменные области видимости изменяться в этот раз:
function prepareCake (flavor) {
return function () {
// Добавляем debugger
debugger
setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
}
}
const makeCakeLater = prepareCake('banana');
Второй способ: добавлять брейкпоинт напрямую в код во вкладке Sources (или Debugger) путем клика на номер строки.
Выводы:
- Области видимости и замыкания не настолько сложны для понимания, как кажется. Они достаточно просты, если рассматривать их через принцип одностороннего зеркала.
- После объявления переменной в функции доступ к ней возможен только внутри функции. Такие переменные называют определенными в контексте этой функции.
- Если вы объявляете любую внутреннюю функцию внутри другой функции, то внутренняя функция называется замыканием, т.к. эта функция сохраняет доступ к переменным внешней функции.
Комментарии (11)
japson
25.09.2017 22:04К выводу по второму пункту надо добавить, что возвращаться должна функция, иначе это не замыкание. В цитате ниже термин модули — но это о функциях с замыканием.
Модули требуют две ключевых характеристики: 1) внешнюю функцию-обертку, которую будут вызывать, чтобы создать закрытую область видимости 2) возвращаемое значение функции-обертки должно включать в себя ссылку на не менее чем одну внутреннюю функцию, у которой потом будет замыкание на внутреннюю область видимости обертки.
Kyle Simpson: Вы не знаете JS
ya-est
Есть ошибка. Если вызвать функцию определенную с помощью function expression до ее определения, будет TypeError: sayHello is not a function
NarekPK Автор
Ошибки нет, т.к. функция sayHello объявлена через const, а не через var.
ya-est
С const согласен, не посмотрел пример сам, тогда в описании к примеру, тоже бы написать const.
NarekPK Автор
В описании к примеру функция вида «function expression» приведена с объявлением через var для наиболее общего вида. Если бы статья не была переводом, то в примере исправил бы const на var, но т.к. это не так, то оставим, как есть.
ya-est
Ну да и ошибка там reference error будет, а не то, что неопределено значение
NarekPK Автор
— Если запустить этот кусок кода в консоли браузера Chrome, то будет выведено: «sayHello is not defined», а это означает, что функция sayHello не определена (не объявлена), что и написано в примере.
— В целом не ставилась задача точно отобразить, какая ошибка будет при выполнении кода. Важно было показать, что функция вида «function expression» не будет выполнена, если будет вызвана до своего объявления.