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

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

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

Могут быть следующие причины:

  • Доступ к внутренней переменной нарушает своего рода принцип инкапсуляции в коде.
  • Внутренняя переменная вообще не относится к внешней области видимости.
  • У блока кода могут быть соседи, которые тоже рады бы использовать это же имя переменной.
  • Имя переменной, которое мы хотим использовать во внутреннем блоке, уже задействовано в одном из его родительских блоков.

Например:

function produceCake(name) {
 let description;
 //здесь нужно определить продукты для пирога (products), но чтобы к этой переменной не было доступа и возможности изменить извне (согласно принципу наименьших привилегий)
}

Для этих целей раньше обычно использовались немедленно вызываемые функциональные выражения — IIFE (Immediately Invoked Function Expression) которые представляют собой функции, выполняющиеся сразу же после того, как они были определены. Подробнее о функциях и их видах будет рассказано в следующих частях курса. Сейчас рассмотрим лишь один классический вариант такого функционального выражения:

(function IIFE() {
 let a = 3;
 console.log(a); // 3
})();

Поэтому, наш предыдущий пример, до выхода стандарта ES6, выглядел бы так:

function produceCake(name) {
 let description;

 (function IIFE() {
  var products;
  if (name == "napoleon") {
   products = "Ingredients for Napoleon Cake";
  } else {
   products = "Standard cake ingredients";
  }
  description = cake + " made from " + products;
 })();

 console.log(description);
}

С выходом ES6 блок инструкций {...} теперь тоже может ограничивать область видимости. Только в отличие от функций, ограничивающих область видимости переменных, объявленных любым способом (через var, const и let), блок инструкций ограничивает область видимости только для переменных, которые объявлены через const и let.

function produceCake(name) {
    let description;

    {
        let products; // видна только в пределах блока

        if (name == "napoleon") {
            products = "Ingredients for Napoleon Cake";
        } else {
            products = "Standard cake ingredients";
        }
        description = cake + " made from " + products;
    }

    // здесь, за пределами блока, переменная products не определена

    console.log(description);

Если в блоке объявить переменную var, то она будет доступна вне блока и будет ограничиваться функцией, в которой этот блок находится.

function produceCake(name) {
    let description;

    {
        var products; // переменная видна и вне текущего блока
        if (name == "napoleon") {
            products = "Ingredients for Napoleon Cake";
        } else {
            products = "Standard cake ingredients";
        }
    }

    description = cake + " made from " + products; // доступ к products вне блока пройдет без ошибок

     console.log(description);

Здесь, несмотря на то, что переменная products объявлена в блоке, она будет доступна и вне его, в пределах всей функции produceCake, так как объявлена через ключевое слово var.

Единственными примерами блочной области видимости еще со стандарта ES3 были:

  • инструкция with, которая сейчас уже устарела и не используется. Подробнее о ней можно прочитать на английском здесь. Подробно эта инструкция здесь разбираться не будет, использование этой инструкции с самого её появления воспринималось неодобрительно, так как приводило к путанице, ошибкам в коде и ухудшению производительности.
  • блок catch в конструкции для обработки ошибок try...catch

    try {
     undeclaredFunc(); // вызов необъявленной ранее функции, чтобы вызвать исключение
    } catch (err) {
     // отловит ошибку и выведет в консоль
    
     console.log(err); // ReferenceError: undeclaredFunc is not defined
    }
    
    console.log(err); // приведет к ошибке ReferenceError: err is not defined
    

    Как видите, err существует только в блоке catch и выбрасывает ошибку, когда вы пытаетесь сослаться на нее где-либо в другом месте.

Переменные let

Когда переменная объявлена в блоке инструкций через ключевое слово let, область видимости этой переменной ограничивается именно этим блоком инструкций { ... }, в котором она объявлена.

var a = 2;

{
 let a = 3;
 console.log(a); // 3
}

console.log(a); // 2

Или

let value = 20;

if (value > 10) {
 let local = value * 2;
 console.log(local);
}

console.log(local); // Uncaught ReferenceError: local is not defined

Помимо блоков инструкций, которые являются частью конструкций JavaScript, такие, как if, for, while и прочие, можно создавать явные отдельные блоки, не являющиеся частью каких-либо конструкций. Иногда такое создание явных блоков может облегчить разработку и поддержку кода, делая более очевидным то, куда присоединены переменные, а куда — нет.

let text = "string value";

{
 // <-- явный блок
 let local = "local " + text;
 console.log(local);
}

console.log(local); // Uncaught ReferenceError: local is not defined

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

Еще один пример, в котором let показывает себя с лучшей стороны — в случае с циклом for.

for (let i = 0; i < 10; i++) {
 console.log(i);
}

console.log(i); // Uncaught ReferenceError: i is not defined

Переменная i, объявленная через let в цикле for не только ограничивается самим телом цикла { ... }, но и при каждой итерации ей присваивается новое значение с окончания предыдущей.

Такую пред-итеративную привязку можно записать так:

{
 let j;
 for (j = 0; j < 10; j++) {
  let i = j; // перепривязка в каждой итерации!
  console.log(i); // 1, 2, 3, 4 ... 10
 }
 console.log(j); // 10
}

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

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

Пример:

var foo = true,
 baz = 10;

if (foo) {
 var bar = 3;

 if (baz > bar) {
  console.log(baz);
 }

 // остальной код...
}

Этот код довольно легко отрефакторить в менее вложенный:

var foo = true,
 baz = 10;

if (foo) {
 var bar = 3; // видна и вне блока if

 // остальной код...
}

if (baz > bar) {
 console.log(baz);
}

Но остерегайтесь таких изменений, когда используете переменные блочной области видимости:

var foo = true,
 baz = 10;

if (foo) {
 let bar = 3; // ограничена блоком if

 if (baz > bar) {
  // <-- этот блок уже необходимо будет переносить вместе с объявлением переменной bar
  console.log(baz);
 }
}

Рассмотрим следующий пример:

let a = 2;

if (a > 1) {
 let b = a * 3;
 console.log(b); // 6

 for (let i = a; i <= b; i++) {
  // i от 2 до 6
  let j = i + 10;
  console.log(j); // 12 13 14 15 16
 }

 let c = a + b;
 console.log(c); // 8
}

Какие переменные существует только внутри оператора if, а какие существует только внутри цикла for? И какие переменные доступны везде в этом коде?

Ответ: оператор if содержит переменные блочной области видимости b и c, а цикл for содержит переменные i и j. Везде доступна переменная a.

Переменные const

В дополнение к let, ES6 представляет ключевое слово const, тоже создающее переменную блочной области видимости, но чьё значение фиксированно (константа). Любая попытка изменить это значение позже приведет к ошибке.

let value = 20;

if (value > 10) {
 var a = 2;
 const b = 3; // ограничена блочной областью видимости `if`

 a = 3;
 b = 4; // TypeError: Assignment to constant variable.
}

console.log(a); // 3
console.log(b); // Uncaught ReferenceError: b is not defined (так была объявлена в блоке)

У переменной const должна быть явная инициализация значением. Если вам нужна константа со значением undefined, то придётся явно её инициализировать этим значением.

const a = undefined;

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

{
 const a = { car: "Honda" };
 a.car = "Porsche"; // здесь изменяется свойство объекта
 console.log(a); // car: "Porsche"
 a = 42; // А здесь попытка присвоить новое значение уже вызовет ошибку TypeError: Assignment to constant variable.
}

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

Повторное объявление переменных в одной области видимости

Обратите внимание, что переменные let и const, в отличие от переменных var, могут быть объявлены только один раз в одной и той же области видимости. При повторном объявлении будет возникать ошибка SyntaxError: Identifier 'name' has already been declared (идентификатор ‘name’ уже был объявлен). Например

var x = 3;
console.log(x);
var x = "Hello";
console.log(x);

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

Для переменных let или const следующий код вызовет ошибки

let x = 3;
console.log(x);
let x = "Hello"; // Uncaught SyntaxError: Identifier 'x' has already been declared

При объявлении переменных let и const такая проверка, на существование переменных с таким же идентификатором, выполняется среди всех переменных в рамках той же области видимости, даже если они были объявлены через var

var x = 3;
const x = "Hello"; // Uncaught SyntaxError: Identifier 'x' has already been declared

Или объявлена как функции

let x = 3;
function x() {
 alert("Hello");
} // Uncaught SyntaxError: Identifier 'x' has already been declared

Функции ограниченные блочными областями видимости

В режиме use strict, функции, объявленные внутри блока {...} тоже имеют область видимости, ограниченную этим блоком.

Например:

"use strict";

{
 foo(); // Будет работать

 function foo() {
  console.log("Вызов foo");
 }
}

foo(); // Uncaught ReferenceError: foo is not defined

Или в случае условной конструкции if-else:

"use strict";

let value = 5;

if (value > 2) {
 function foo() {
  console.log("1");
 }
} else {
 function foo() {
  console.log("2");
 }
}

foo(); // Uncaught ReferenceError: foo is not defined

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

Для корректной работы предыдущего примера в строгом режиме нужно использовать функциональные выражения:

"use strict";

let age = prompt("Сколько Вам лет?", 18);

let welcome;

if (age < 18) {
 welcome = function() {
  console.log("Привет!");
 };
} else {
 welcome = function() {
  console.log("Здравствуйте!");
 };
}

welcome(); // будет работать, так как объявленная переменная welcome видна во всём коде

Блочную область видимости не следует использовать как полную замену функциональной области видимости var. Стоит использовать обе области видимости: функциональную и блочную, в соответствующих местах, чтобы создавать лучший, более читаемый/обслуживаемый код.

Обнаружили ошибку или хотите добавить что-то своё в документацию? Отредактируйте эту страницу на GitHub!

Оставить комментарий