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

Это второй пост, и в нем я делюсь своими знаниями из второй книги серии Области действия и замыкания. Если вас интересует первый пост (о книге 1 Вверх и вперед), пост — здесь.

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

Является ли JavaScript компилируемым или интерпретируемым языком?

Хотя JavaScript подпадает под категорию динамических или интерпретируемых языков, на самом деле он является компилируемым языком.

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

Результат компиляции также не переносим в различные распределенные системы, как в Java.

Движок JavaScript имеет компилятор, который генерирует машинный код из читаемого человеком кода JS, а затем движок запускает этот скомпилированный машинный код.

Несколько важных указаний по компиляции:

  • В большинстве компилируемых языков код перед выполнением проходит три основных этапа, которые в совокупности называются компиляцией. Это следующие шаги:
     – токенизация/лексирование (создание потока токенов из кода),
     – синтаксический анализ (создание AST из потока токенов). ,
    - генерация кода(создание машинного кода из AST)
    Это всего лишь упрощенное объяснение, компиляция и компиляторы гораздо сложнее, существуют различные оптимизации, которые созданы для эффективного выполнения кода.
  • Движок JavaScript не может позволить себе роскошь иметь много времени для оптимизации, как другие языки, потому что в этих языках код компилируется во время сборки, как в Java, C++, в отличие от JavaScript, в котором код компилируется всего за микросекунды до этого. выполняется.
    Чтобы обеспечить максимальную производительность, JS-движки используют всевозможные трюки (например, JIT, которые ленивой компиляции и даже горячей перекомпиляции и т. д.).

Что такое область применения?

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

Четко определенный набор правил для хранения переменных и последующего поиска этих переменных — это Область. Правила поиска переменных по их именам-идентификаторам, доступным для текущего исполняемого кода.

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

var a = 2; выглядит как один оператор, в отношении которого мы можем предположить, что компилятор выдаст код, который суммируется в псевдокоде: «выделите память для переменной, пометьте ее a, поместите в нее значение 2», но движок видит это не так, движок видит это как два отдельных оператора, один из которых компилятор будет обрабатывать во время компиляции, а другой — во время выполнения.

  • Компилятор обнаруживает var a, проверяет в области видимости, существует ли уже переменная с именем a для этой коллекции областей видимости. Если она существует, оператор игнорируется, в противном случае компилятор просит область видимости объявить переменную с меткой a для этой коллекции областей видимости.
  • Затем компилятор обнаруживает a = 2 и генерирует код, который будет выполнен позже механизмом. Механизм запросит область, есть ли переменная с именем a в текущей коллекции областей видимости. Если найдено, значение 2 присваивается a, если не найдено, область видимости выглядит в окружающих областях, если переменная нигде не найдена, движок выдает ошибку.

Поиск по левому и правому краю

Одна новая вещь, которую я узнал: существует два типа поиска переменных, которые выполняет область видимости: LHS и RHS, тип выполняемого поиска зависит от выполняемой операции, присваивается ли что-нибудь переменной? или требуется только текущее значение переменной?

кто является целью задания (LHS) и кто является источником задания (RHS)

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

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

LHS и RHS приводят к разным ошибкам, когда искомая переменная не найдена.

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

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

Существует две преобладающие модели работы области видимости: лексическая область и динамическая область видимости.

Лексическая область — это область, которая определяется во время lex-time, lex-time — это время, в которое выполняется лексирование, как обсуждалось ранее, лексирование — это первый шаг процесса компиляции, который отвечает за токенизацию потока символов, который является вашим код.

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

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

Область действия устанавливается во время разработки и не может быть изменена позже во время выполнения, за исключением некоторых неодобрительных способов сделать это (измените область действия, установленную во время разработки, позже во время выполнения).

Одним из таких способов является использование eval(), рассмотрим этот пример:

function foo(str, a) {
 eval( str ); // cheating!
 console.log( a, b );
}

var b = 2;

foo( "var b = 3;", 1 ); // 1 3

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

однако есть одна загвоздка: если eval() выполняется в строгом режиме, он выполняется в своей собственной лексической области, что означает, что объявления, сделанные внутри eval, не влияют на окружающую лексическую область.

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

  • setTimeout и setInterval, они могут принимать строки в качестве первого аргумента, который оценивается с помощью eval.
  • Конструктор функции new Function() принимает в качестве последнего параметра строку кода, которая преобразуется в динамически создаваемую функцию.

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

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

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

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

Функция и область действия блока в JavaScript

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

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

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

function doSomething(a) {
 b = a + doSomethingElse( a * 2 );

 console.log( b * 3 );
}

function doSomethingElse(a) {
 return a - 1;
}

var b;

doSomething( 2 );

это выглядит совершенно нормально, но видите ли вы, что var b объявлен в глобальной области видимости, как и doSomethingElse, но они используются только внутри doSomething, поэтому их присутствие в глобальном масштабе не является обязательным, лучший способ структурировать приведенный выше код:

function doSomething(a) {
 function doSomethingElse(a) {
  return a - 1;
 }

 var b;

 b = a + doSomethingElse( a * 2 );

 console.log( b * 3 );
}

doSomething( 2 ); // 15

Теперь у нас есть doSomethingElse и b, объявленные внутри doSomething, и они не доступны для глобальной области видимости. Это полезно, когда doSomething выполняет какую-то важную задачу, которая, если она присутствует в глобальной или внешней области видимости, может быть каким-либо образом опасной или нежелательной.

Еще одним преимуществом является то, что после завершения выполнения doSomething память, назначенная b и doSomethingElse, может быть освобождена для использования в другом месте.

Еще одно преимущество сокрытия кода внутри области видимости — это позволяет избежать коллизий имен переменных.

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

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

JS предоставляет решение для этой проблемы

Выражение немедленно вызываемой функции (IIFE) в JavaScript

Функциональное выражение, которое немедленно вызывается или выполняется, является IIFE. Например, этот фрагмент кода foo заполняет окружающую область именем функции:

var a = 2;

function foo() { // foo pollutes the enclosing scope
 var a = 3;
 console.log( a ); // 3
} 
foo(); // have to call foo() separately in the enclosing scope

console.log( a ); // 2

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

var a = 2;

(function() { // inserted "(" before function
 var a = 3;
 console.log( a ); // 3

})();  // enclosed with ")" and "()" in the end

console.log( a ); // 2

Выражение функции — это оператор, в котором первый идентификатор не function, как и в приведенном выше фрагменте, первый идентификатор — (, а не function.

Итак, добавив выражение функции в пару ( ), мы немедленно выполним функцию, добавив () в конце.

Аргументы также можно добавлять в IIFE, например:

var argToPass;
(function foo(arg1) {
  ...
})(argToPass);

Именованные и анонимные функции

При написании выражения функции можно пропустить имя функции и записать ее как function() { ... }, это анонимная функция.

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

Выражения функций чаще всего встречаются в функциях обратного вызова, например, как обратный вызов setTimeout и setInterval, как показано ниже.

setTimeout(function() {
  // do something
}, 1000);

эти выражения также можно назвать функциями, например:

setTimeout(function doSomething() {
  // do something
}, 1000);

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

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

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

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

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

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

for(var i = 0; i < 10; i++) {
  // do something with i
}

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

В более ранних версиях JavaScript не поддерживал область блоков «из коробки»,

Одна новая вещь, которую я узнал, заключается в том, что объявления переменных в блоке catch в инструкции try catch привязаны к блоку catch, и к ним нельзя получить доступ за пределами блока catch.

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

приведенный выше фрагмент кода цикла for работает намного лучше с let, вот так

for(let i = 0; i < 10; i++) {
 // do something with i
}

теперь i будет ограничен блоком, созданным циклом for.

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

Подъем в JavaScript

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

a = 2;
var a;
console.log(a);

Это будет 2, и причина этого — Подъем. Еще один интересный фрагмент:

console.log(a);
var a = 2;

этот регистрирует undefined, также из-за Подъёма. Что такое подъем?

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

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

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

// snippet 1

var a;
a = 2;
console.log(a);

// snippet 2

var a;
console.log(a);
a = 2;

Вы видите, как объявления a перемещаются вверх, это Hoisting.

Объявления функций также поднимаются. Но функциональные выражения — нет. Например

foo(); // works fine because the declaration is hoisted
bar(); // throws type error because bar is undefined as 
       //function expression is not hoisted

function foo() {
  // do something
}

var bar = function bar() {
 // do something else
}

Учитывая подъем, приведенный выше код можно интерпретировать так:

function foo() {
  // do something
}
var bar;

foo();
bar(); // cannot execute undefined, so type error

bar = function bar() {
  // do something else
}

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

Подъем осуществляется для каждой области, этот процесс повторяется для каждой области вашего кода.

Функции поднимаются первыми, а затем переменными.

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

foo(); // 1

var foo;

function foo() {
 console.log( 1 );
}

foo = function() {
 console.log( 2 );
};

Объявления функций, которые появляются внутри обычных блоков, обычно поднимаются до охватывающей их области, а не являются условными, как подразумевает этот код:

foo(); // "b"

var a = true;
if (a) {
   function foo() { console.log( "a" ); }
}
else {
   function foo() { console.log( "b" ); }
}

Замыкания в JavaScript

Автор отмечает, что «понимание завершенности может показаться особой нирваной, ради достижения которой нужно стремиться и жертвовать».

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

Давайте посмотрим фрагмент кода, который сделает определение более понятным:

function multiplyBy(multiplier) {
  // multiplier is in multiplyBy's scope

  function multiply(number) {
    // number is in multiply's scope
    // multiplier is also accessbile in multiply's scope
    // as per lexical scope rules

    return number * multiplier;
  }
  return multiply;
}  

var multiplyBy2 = multiplyBy(2); // returns a function
multiplyBy2(4) // returns 8

Здесь у нас есть функция multiplyBy, которая принимает параметр multiplier и возвращает function, которая принимает параметр number и возвращает результат multiplier*number. multiplyBy можно использовать для создания функций, которые могут умножать число на любой множитель. Как это работает? и где замыкание?

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

Но замыкания не позволяют этому случиться, внутренняя область multiplyBy все еще используется, она используется возвращаемой функцией multiply.

мы создаем переменную multiplyBy2 = multiplyBy(2), выполнив multiplyBy с параметром multiplier = 2, и она возвращает функцию, которая будет храниться в multiplyBy2 и при выполнении вернет результат умножения любого переданного вами числа на 2.

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

Самый задаваемый вопрос на собеседованиях

Каков вывод этого фрагмента кода?

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

// it logs "6", five times

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

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

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

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

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

Модули в JavaScript с использованием замыканий

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

function ApiModule(endpoint) {
  var apiKey = "dfgdg";
  var apiEndpoint = endpoint;
  var user = "";

  function loginUser(user) {
    // do something
  }

  function fetchData() {
    //do something
  }

  /* the functions and data declared above
   * are in the internal scope of ApiModule
   * and are not exposed until returned from the function
  */

  return {
    loginUser,
    fetchData,
    user,
  };

  // only this returned data is accessible outside
}

var Module = ApiModule("endpoint");
Module.loginUser(user);
Module.fetchData();
Module.apiKey; //undefined as it's not exposed

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

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

var foo = (function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];

  function doSomething() {
    console.log( something );
   }

  function doAnother() {
    console.log( another.join( " ! " ) );
  }

  var publicApi = {
    doSomething: doSomething,
    doAnother: doAnother
  };

  return publicApi;
})();

foo.doSomething();
foo.doAnother();

Этот шаблон использует IIFE для выполнения функции создания модуля только один раз.

Модули ES6

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

В каждом файле может быть только один модуль.

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

Компилятор также проверяет при импорте материала из модулей, существует ли импортированный материал в модуле, если нет, компилятор выдает раннюю ошибку.

Некоторые указания из Приложения

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

В то время как лексическая область определяется во время автора, динамическая область определяется во время выполнения и зависит не от порядка написания кода, а от того, как он вызывается, динамическая область действия основана на стеке вызовов, this в JS является своего рода как динамическая область видимости. Lisp, LaTeX, Emacs используют динамическую область видимости.

Область действия блока полифиллинга

Теперь мы можем использовать let для доступа к области блока, но он недоступен в средах до ES6. Как мы можем заполнить область блока в этих средах?

catch в ES6 имеет область действия блока, поэтому он используется многими транспиляторами, такими как Babel, для заполнения области блока в версиях до ES6. Как?

try { throw 2 }
  catch(a) {
  // a is not block scoped
  console.log(a)
}

Не очень аккуратно, но нам не следует использовать IIFE, потому что функция, обернутая вокруг любого произвольного кода, меняет значение внутри этого кода this, return, break и continue.

По производительности IIFE лучше, чем try catch, но это не самый разумный полифил для использования по вышеуказанным причинам.

На этом завершается вторая книга серии. Как всегда, это было для меня хорошим уроком, надеюсь, это было полезно. На следующем. ❤