Как стать автором
Обновить

Книга «{Вы пока еще не знаете JS} Область видимости и замыкания. 2-е межд. издание»

Время на прочтение9 мин
Количество просмотров3.9K
image Привет, Хаброжители! Вы пока еще не знаете JS. И Кайл Симпсон признается, что тоже его не знает (по крайней мере полностью)… И никто не знает. Но все мы можем начать работать над тем, чтобы узнать его лучше. Сколько бы времени вы ни провели за изучением языка, всегда можно найти что-то еще, что стоит изучить и понять на другом уровне. Вы уже прочитали «Познакомьтесь, JavaScript»? Тогда откройте вторую книгу серии «Вы пока еще не знаете JS», чтобы познакомиться поближе с первым из трех столпов JavaScript — системой областей видимости и функциональными замыканиями, а также с мощным паттерном проектирования «Модуль». Пора освоить правила лексических областей видимости для размещения переменных и функций в правильных позициях. И заглянуть на более низкий уровень, ведь магия с хранением состояния модулей базируется на замыканиях, использующих систему лексических областей видимости.

Вашему вниманию предлагается 2-е издание снискавшей популярность серии книг «Вы не знаете JS»: «Вы пока еще не знаете JS» (YDKJSY).

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

Если вы читаете эти книги впервые, я рад, что они попались вам на глаза. Подготовьтесь к увлекательному путешествию по закоулкам JavaScript.

Если вы недавно занимаетесь программированием или JS, то учтите, что эти книги не задумывались как «деликатный вводный курс по JavaScript». Временами материал становится сложным и требующим серьезных усилий, и многие темы рассматриваются намного глубже, чем в книгах для новичков. Книга может пригодиться всем читателям независимо от уровня подготовки, но я писал ее с прицелом на то, что вы уже знакомы с JS, а ваш практический опыт работы с этим языком составляет хотя бы полгода, если не больше.

Где использовать let?


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

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

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

Если объявление принадлежит блоковой области видимости, используйте let. Если оно принадлежит функциональной области видимости, используйте var (еще раз: это только мое мнение).

Чтобы понять суть этого решения, можно подумать, как бы выглядела версия этой программы до ES6. Например, вспомним приведенную выше функцию diff(..):

function diff(x,y) {
   var tmp;
   if (x > y) {
      tmp = x;
      x = y;
      y = tmp;
   }
   return y - x;
}

В этой версии diff(..) переменная tmp очевидно объявляется в функциональной области видимости. Подходит ли это для tmp? На мой взгляд, нет. Переменная tmp нужна только для этих нескольких команд. Для команды return она не нужна, поэтому должна иметь блоковую область видимости.

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

function diff(x,y) {
   if (x > y) {
      // `tmp` по-прежнему имеет функциональную область
     // видимости,но ее размещение здесь является
      // семантическим сигналом о блоковой области видимости
      var tmp = x;
      x = y;
      y = tmp;
   }
   return y - x;
}

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

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

В другом примере исторически использовалось объявление var, но теперь практически всегда в цикле for должно использоваться let:

for (var i = 0; i < 5; i++) {
   // ...
}

Где бы ни определялся такой цикл, переменная i, по сути, всегда используется только внутри цикла; в этом случае принцип наименьшего раскрытия требует, чтобы она объявлялась с ключевым словом let вместо var:

for (let i = 0; i < 5; i++) {
   // ...
}

Подобное переключение с var на let нарушит работоспособность вашего кода только в одном случае: если он зависит от обращения к переменной цикла (i) за пределами/после цикла:

for (var i = 0; i < 5; i++) {
   if (checkValue(i)) {
      break;
   }
}
if (i < 5) {
   console.log("The loop stopped early!");
}

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

var lastI;
for (let i = 0; i < 5; i++) {
   lastI = i;
   if (checkValue(i)) {
      break;
   }
}
if (lastI < 5) {
   console.log("The loop stopped early!");
}

Переменная lastI нужна во всей области видимости, поэтому она объявляется с ключевым словом var. Переменная i нужна только в (каждой) итерации цикла, поэтому она объявляется с ключевым словом let.

В чем загвоздка?

До настоящего момента утверждалось, что var и параметры имеют функциональную область видимости, а let/const сигнализируют об объявлениях с блоковой областью видимости. Существует только одно маленькое исключение, заслуживающее упоминания: секция catch.

С момента появления try..catch в ES3 (в 1999 году) в секции catch использовалась дополнительная (малоизвестная) возможность объявления блоковой области видимости:

try {
   doesntExist();
}
catch (err) {
   console.log(err);
   // ReferenceError: 'doesntExist' is not defined
   // ^^^^ сообщение, выводимое для перехваченного исключения
   let onlyHere = true;
    var outerVariable = true;
}
console.log(outerVariable); // true
console.log(err);
// ReferenceError: 'err' is not defined
// ^^^^ другое (неперехваченное) исключение

Переменная err, объявленная в секции catch, имеет блоковую область видимости для данного блока. Блок секции catch может содержать другие объявления с блоковой областью видимости, создаваемые let. Однако объявление var внутри этого блока все еще присоединяется к внешней функциональной/глобальной области видимости.

В ES2019 (недавно, на момент написания книги) секции catch были изменены и их объявление стало необязательным; если объявление опущено, то блок catch (по умолчанию) уже не является областью видимости, но при этом он остается блоком!

Таким образом, если нужно отреагировать на факт возникновения исключения (после чего можно корректно продолжить работу), но само значение ошибки вас не интересует, объявление catch можно опустить:

try {
   doOptionOne();
}
catch { // Объявление catch опущено
   doOptionTwoInstead();
}

Это небольшое, но приятное упрощение синтаксиса для довольно распространенного случая использования; оно также может быть чуть более эффективным для удаления избыточных областей видимости!

Объявления функций в блоках (FiB)

Итак, вы видели, что объявления с let или const имеют блоковую область видимости, а объявления var — функциональную. А как насчет объявлений, размещаемых непосредственно внутри блоков? Эта возможность называется FiB (Functions in Blocks).

Обычно мы рассматриваем объявления функций как эквиваленты объявлений var. Так значит, они имеют функциональную область видимости, как и var?

Нет и да. Знаю, это звучит странно. Разберем подробнее:

if (false) {
   function ask() {
       console.log("Does this run?");
   }
}
ask();


Как вы думаете, что сделает эта программа? Три возможных варианта:

1. При вызове ask() может произойти исключение ReferenceError, потому что идентификатор ask имеет блоковую область видимости для блока if, а следовательно, недоступен во внешней/глобальной области видимости.

2. При вызове ask() может произойти исключение TypeError, потому что идентификатор ask существует, но он содержит undefined (потому что команда if не выполняется), а следовательно, не является вызываемой функцией.

3. Вызов ask() выполняется правильно и выводит сообщение Does it run?
А теперь самая загадочная часть: в зависимости от того, в какой среде JS будет выполняться этот фрагмент кода, вы можете получить разные результаты! Это одна из немногих странных областей, в которых существующее унаследованное поведение противоречит предсказуемости результата.

Спецификация JS гласит, что объявления функций внутри блоков имеют блоковую область видимости, поэтому ответом должен быть пункт (1). Однако большинство браузерных ядер JS (включая движок v8, который происходит от Chrome, но также используется в Node) ведет себя в соответствии с пунктом (2); это означает, что идентификатор имеет область видимости вне блока if, но значение-функция не инициализируется автоматически, поэтому оно остается равным undefined.

Почему браузерным движкам JS разрешается нарушать своим поведением спецификацию? Потому что эти движки уже обладали поведением, связанным с FiB, до появления блоковой видимости в ES6, и существовали опасения, что изменения, направленные на соответствие спецификации, могут нарушить работоспособность существующего кода JS веб-сайтов. Из-за этого в приложении B спецификации JS было сделано исключение, позволяющее некоторые отклонения для браузерных движков JS (и только!).

Обычно Node не относится к браузерным средам JS, так как обычно работает на сервере. Однако движок Node v8 является общим с браузером Chrome (и Edge). Так как движок v8 сначала был браузерным движком JS, он включает исключение из приложения B, а это означает, что браузерные исключения распространяются на Node.


Одним из самых распространенных сценариев использования для размещения объявления функции в блоке является условное определение функции тем или иным способом (например, в команде if..else) в зависимости от некоторого состояния среды. Пример:

if (typeof Array.isArray != "undefined") {
   function isArray(a) {
      return Array.isArray(a);
   }
}
else {
   function isArray(a) {
      return Object.prototype.toString.call(a)
      == "[object Array]";
   }
}

Такое структурирование кода по соображениям эффективности выглядит соблазнительно, так как проверка typeof Array.isArray выполняется только один раз, в отличие от определения всего одной версии isArray(..) и размещения команды if внутри нее — в этом случае каждый вызов будет сопровождаться избыточной проверкой.

Кроме рисков, связанных с расхождениями FiB, у условного определения функций есть и другая проблема: оно усложняет отладку таких программ. Если вы столкнетесь с ошибкой в функции isArray(..), вам придется сначала вычислить, какая именно реализация isArray(..) при этом выполнялась! А иногда ошибка может возникнуть из-за того, что была выбрана неправильная реализация из-за ошибки в условии! Если вы определили несколько версий функции, такую программу всегда труднее понять и она всегда создает больше проблем с сопровождением.


В дополнение к предыдущим фрагментам, с FiB также связан ряд других граничных случаев; скорее всего, такое поведение в разных браузерных и небраузерных средах JS (движках JS, которые не базируются на браузерах) будет с большой вероятностью изменяться. Пример:

if (true) {
   function ask() {
      console.log("Am I called?");
   }
}
if (true) {
   function ask() {
      console.log("Or what about me?");
   }
}
for (let i = 0; i < 5; i++) {
   function ask() {
      console.log("Or is it one of these?");
   }
}
ask();
function ask() {
   console.log("Wait, maybe, it's this one?");
}

Напомню, что поднятие функции в соответствии с описанием «Когда можно использовать переменную?» (глава 5) может навести на мысль, что последний вызов ask() из этого фрагмента с сообщением Wait, maybe… поднимется над вызовом ask(). Так как это последнее объявление функции с таким именем, оно должно «победить», верно? К сожалению, нет.

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

Когда я говорю о FiB, меня интересует другое: какой совет я могу дать, чтобы обеспечить предсказуемую работу вашего кода во всех обстоятельствах?

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

Таким образом, в более раннем примере if..else я бы порекомендовал избегать условного определения функций, если это возможно. Да, может быть, такое решение будет чуть менее производительным, но в целом это лучшее решение:

function isArray(a) {
   if (typeof Array.isArray != "undefined") {
      return Array.isArray(a);
   }
   else {
      return Object.prototype.toString.call(a)
          == "[object Array]";
     }
}

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

var isArray = function isArray(a) {
    return Array.isArray(a);
};
// переопределите определение, если это необходимо
if (typeof Array.isArray == "undefined") {
   isArray = function isArray(a) {
      return Object.prototype.toString.call(a)
          == "[object Array]";
     };
}

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

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

FiB не стоит того, и от этой возможности стоит держаться подальше.

Напоследок о блоках

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

И один из самых важных организационных приемов — предотвращение раскрытия переменных в нежелательных областях видимости (принцип наименьшего раскрытия). Хочется верить, что вы теперь понимаете блоковую область видимости намного лучше, чем прежде.

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

Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — JavaScript

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Теги:
Хабы:
+5
Комментарии6

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия