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

Я уже намекнул на то, что в этом блоге мы рассмотрим еще одну большую тему JavaScript - Scope!

Некоторые из вас могут иметь искусственное представление о том, что такое область видимости, но Кайл Симпсон определяет ее как «четко определенный набор правил для хранения переменных в некоторых местах и ​​для поиска этих переменных в более позднее время».

Чтобы правильно понять Scope, мы углубимся сейчас и поговорим о том, как JavaScript компилируется JavaScript Engine.

Три этапа компиляции

  • Токенизация / Lexing
  • Парсинг
  • Генерация кода

Токенизация

  • разбиение выражений или утверждений на блоки, называемые токенами

Парсинг

  • Построение дерева вложенных токенов, которые представляют грамматическую структуру программы - мы называем это дерево абстрактным синтаксическим деревом (AST).

Генерация кода

  • Этот шаг состоит из фактического превращения AST в исполняемый код.

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

Поговорим о внешности, а точнее о поиске!

Механизм JavaScript может выполнять два разных поиска, которые мы можем разделить на две группы, которые относятся к стороне операции присваивания.

  • Поиск RHS (поиск справа): присвоение переменной
  • Поиск LHS (поиск слева): получение значения переменной

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

Вложенная область

Конечно, мы не просто работаем с одной областью видимости - обычно у нас есть вложенные области, которые нужно учитывать. Если вы не можете найти переменную в ее непосредственной области (самой внутренней области), вы продолжаете проверять все дальше и дальше, исследуя больше внешних областей, пока не достигнете глобальной области и не найдете ее. Это важно и позже станет действительно действенной концепцией, так что помните об этом! На каждом этапе этой цепочки внутренних и внешних областей видимости вы решаете поиски RHS и LHS, пока не найдете то, что ищете.

Если RHS не может найти переменную, мы получаем ошибку ReferenceError из-за ошибки разрешения области видимости. Область действия LHS создаст глобальную переменную, если она не будет найдена, если только она не запущена в строгом режиме. Другая часто встречающаяся ошибка - это TypeError, что означает, что мы нашли область, но пытались выполнить недопустимую операцию.

Лексическая область видимости

Область видимости может работать двумя разными способами - лексическая область видимости и динамическая область видимости. JavaScript использует Lexical Scope, поэтому мы сосредоточимся на нем здесь.

Лексическая область видимости - это область видимости, которая определяется во время лексирования. Это означает, что у нас есть контроль над созданием нашей собственной области видимости во время записи.

Возьмем следующий пример:

function foo(a) {
  var b = a*2;
  function bar(c) {
    console.log(a,b, c);
  }
  bar(b*3);
}
foo(2); // will log 2, 4, 12

Наша самая внутренняя область действия (назовем ее областью действия 1) включает только один идентификатор c. Поэтому мы продолжаем искать в более внешних областях, можем ли мы найти объявления для a или b. В области 2 (большая внешняя область после самой внутренней) объявлены a, bar и b. Самая внешняя область видимости имеет только один идентификатор, а именно foo. Предположим, что каждая функция создает новую область действия. Поиск по области прекращается, когда они находят первое совпадение, начиная с самого вложенного и заканчивая выходом.

Лексическая область видимости определяется только тем, где функция была объявлена ​​- не, где она была вызвана. Это важно помнить, и мы еще вернемся к этому.

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

Функции против области видимости блока

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

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

Глобальные пространства имен

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

Управление модулем

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

Функции как области

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

Один из способов - немедленно вызвать функцию и заключить ее в скобки.

var a = 2;
(function foo() {
  var a = 3;
  console.log(a); // will log 3
})();
console.log(a); // will log 2

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

Анонимные функции

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

Сразу вызываемые функциональные выражения (IIFE)

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

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

(function(){…})(); // only the first bit before the set of round parentheses
(function(){…}()); // the complete code is wrapped in round parentheses

IIFE также можно вызывать с аргументами, что делает их еще лучшим кандидатом для использования.

(function(global){…})(window);

Блоки как область действия

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

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

попробуй поймать

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

позволять

Ключевое слово let было введено в ES6. Ключевое слово let связывает объявление переменной с областью действия любого блока (обычно пары {…}). Объявления, сделанные с помощью let, не распространяются на всю область действия блока, в котором они появляются.

Вы также можете явно определять области блока в своем коде.

пусть петли

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

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

const

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

Подъем

Механизм JavaScript компилирует код перед его интерпретацией. Все объявления, как переменные, так и функции, просматриваются заранее.

Это также относится к неявным объявлениям.

Возьмите следующие фрагменты кода:

a = 2;
var a;
console.log(a);
// The declaration of var a is "hoisted" to the top and handled first, meaning we get 2 as output

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

console.log(a);
a = 2;
// This will not give us 2 as in the previous example but print undefined

Почему второй фрагмент возвращает значение undefined? Посредством передачи необъявленного и неназначенного параметра движок обработает это и неявное объявление, которое приведет к чему-то вроде этого:

var a;
console.log(a); // it's clear now why we get undefined
// the assignment of 2 only comes later
a = 2;

Этот процесс перемещения объявлений наверх называется «Подъем». Мы поднимаем только объявления переменных или функций, но НЕ присваивания - это оставит слишком много места для ошибок и приведет к весьма неожиданным результатам.

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

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

Возьмем следующий пример:

foo(); // not Reference Error but Type Error
var foo = function bar() {
  // some code
};

Хотя foo поднимается и помещается в глобальную область видимости, он еще не имеет значения (как было бы, если бы это было объявление). Таким образом, он терпит неудачу, поскольку не может вызвать undefined. Мы объявляем только var foo - мы пока не присваиваем ему никакого значения. В случае объявления функции мы переместим всю функцию вверх в области видимости, чтобы она могла выполняться.

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

Закрытие объема

Кайл Симпон открывает последнюю главу своей великой книги следующими словами:

«Замыкание - это все вокруг вас в JavaScript, вам просто нужно распознать и принять его».

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

Давайте посмотрим на пример:

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
return bar;
}
var baz = foo();
baz(); // will log 2 --> and executed outside of its declared lexical scope

Так как же мы здесь обманули? Как мы могли получить доступ и зарегистрировать 2? Секрет заключается в передаче bar в качестве значения в конце foo (). Это позволило нам вернуть содержимое панели как объект функции. Поскольку foo () возвращает это содержимое, мы назначаем его переменной baz, которая, в свою очередь, хранит значение выполнения функции bar. Но подождите, здесь тоже творится нечто большее:

Поскольку функция bar () имеет лексическую область видимости над содержимым foo (), которая поддерживает область видимости для использования функцией bar (). У панели ссылок все еще есть над foo - это то, что мы называем закрытием!

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

Вот некоторые популярные примеры:

  • setTimeout (который имеет закрытие над включающей функцией и, следовательно, может получить доступ к любым переменным, объявленным в нем)

Однако IIFE в строгом смысле слова не создают закрытие, хотя и создают свою собственную область видимости.

Это может пригодиться, как в следующем примере:

for(var i=1; i<=5; i++) {
  (function() {
    var j = i;
    setTimeout(function timer() {
      console.log(j);
    }, j*1000);
  })();
}

Если бы мы просто хотели использовать setTimeout () без IIFE и попытаться получить доступ к i, мы бы потерпели неудачу! i вместо этого будет ссылаться на один глобальный i, в данном случае со значением 6 и, следовательно, работать не так, как задумано. Создав собственную область видимости в IIFE и переназначив текущее значение i его собственной новой переменной j, мы можем заставить эту работу работать так, как задумано. И вуаля! Обратные вызовы функции теперь могут закрываться в новой области для каждой итерации.

Мы также узнали о let ранее.

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

for(var i=1; i<=5; i++) {
    let j = i;
    setTimeout(function timer() {
      console.log(j);
    }, j*1000);
}

Таким образом, мы устранили необходимость использования IIFE, который создавал свою собственную область видимости, просто используя объявление let вместо IIFE! Но мы можем еще больше упростить это, используя let в нашем объявлении цикла for, позволяя в конечном итоге коду стать:

for(let i=1; i<=5; i++) {
    setTimeout(function timer() {
      console.log(i);
    }, j*1000);
}

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

Модули

Модуль - это еще одна конструкция кода, которая позволяет нам увидеть Closure в действии в форме шаблона модуля.

Шаблон модуля имеет две ключевые характеристики, которые необходимо выполнить:

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

Но это само по себе немного теоретически, давайте рассмотрим пример кода:

function CoolModule() {
  var something = "cool beans";
  var another = [1, 2, 3];
  function doSomething() {
    console.log(something);
  }
  function doAnother() {
    console.log(another.join( " ! "));
  }
  return {
    doSomething: doSomething,
    doAnother: doAnother
  };
}
var foo = coolModule();
foo.doSomething(); // will log "cool beans"
foo.doAnother(); // will log "1 ! 2 ! 3"

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

Чтобы действительно вызвать какое-либо закрытие, мы должны вызвать функцию CoolModule () и присвоить ее переменной. Вы можете видеть, что CoolModule () возвращает объект, который имеет ссылки на обе наши внутренние функции, doSomething () и doAnother (). Таким образом, этот объект возвращает ссылку на наши функции. Однако он скрывает внутреннюю переменную и детали реализации, что означает, что можно сказать, что он действует как общедоступный API для модуля.

Затем возвращаемое значение объекта присваивается внешней переменной, что означает, что мы можем получить доступ к свойству, вызвав foo.doSomething ().

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

  • Singleton → превращает функцию модуля в IIFE
  • Вы также можете передать параметры в свой модуль и использовать их в
  • Как назвать предмет, который вы собираетесь вернуть

Давайте посмотрим на последнее на другом примере кода:

var foo = (function CoolModule(id) {
  function change() {
    // modifying the public API
    publicAPI.identify = identify2();
  }
  function identify1() {
    console.log(id);
  }
  function identify2() {
    console.log(id.toUpperCase());
  }
  var publicAPI = {
    change: change,
    identify: identify1
  };
  return publicAPI;
})("foo module"); 
// here we invoke the Module directly and pass it a parameter "foo module"
foo.identify(); // will log foo module
foo.change();
foo.identify(); // will log FOO MODULE (change modified the public API to be identify 2)

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

Современные менеджеры модулей делают то же самое и просто соответствуют двум ключевым характеристикам, которые мы определили в начале:

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

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

  • import → import импортирует только один или несколько элементов из API модуля
  • модуль → импортирует весь API модуля в связанную переменную

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

Некоторые заключительные (каламбур!) Замечания

Хотя это была лишь вторая книга из серии YDKJS, я чувствовал, что я многому научился и действительно углубился во внутреннюю работу JavaScript. Я наконец понял концепцию замыкания, которая была для меня загадкой до чтения этой книги. Так что спасибо, Кайл Симпсон, за то, что просветил меня и отправил в путешествие, чтобы наконец овладеть JS!

Ресурсы

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