10 ключевых концепций JavaScript

1. Замыкания

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

Вот пример, иллюстрирующий замыкания в JavaScript:

В приведенном выше примере outerFunction определяет outerVariable и возвращает innerFunction, которая ссылается на outerVariable. Когда outerFunction вызывается и присваивается замыканию, создается замыкание, которое сохраняет ссылку на outerVariable. В дальнейшем при вызове замыкания оно будет иметь доступ к outerVariable и может регистрировать ее значение.

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

2. Промисы

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

Для создания промиса используется конструктор Promise, который принимает функцию обратного вызова с двумя параметрами: resolve и reject. Внутри этого обратного вызова выполняется асинхронная задача с вызовом либо resolve(value) для выполнения промиса со значением, либо reject(reason) для отклонения его с указанием причины (обычно это объект error).

Вот пример базового использования промиса:

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

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

Промисы также предоставляют дополнительные методы, такие как finally(), позволяющий указать обратный вызов, который будет вызван независимо от того, выполнен промис или отклонен, и Promise.all(), который можно использовать для ожидания выполнения нескольких промисов.

3. Прототипы и прототипное наследование

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

Вот пример прототипного наследования:

В этой программе используются функции-конструкторы Animal и Dog.

Функция-конструктор Animal принимает параметр name и присваивает его свойству name вновь создаваемого объекта с помощью this.name.

Добавляем метод greet в Animal.prototype, который будет общим для всех экземпляров, созданных с помощью функции-конструктора Animal.

Функция-конструктор Dog расширяет Animal с помощью Animal.call(this, name) для наследования свойств от конструктора Animal.

Образуем цепочку прототипов, создав новый объект с помощью Object.create(Animal.prototype) и присвоив его Dog.prototype. Это связывает прототип экземпляров Dog с Animal.prototype, обеспечивая наследование. 

Добавляем в Dog.prototype метод bark, который предназначен для экземпляров, созданных с помощью конструктора Dog.

Создаем экземпляры Animal и Dog с помощью ключевого слова new. Экземпляр animal использует метод greet, унаследованный от Animal.prototype, а экземпляр dog использует как метод greet, унаследованный от Animal.prototype, так и метод bark, определенный в Dog.prototype.

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

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

4. Цикл событий

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

Вот программа, демонстрирующая цикл событий в действии:

В этом примере запланировано несколько асинхронных операций с помощью setTimeout() и Promise. Вот как выполняется программа:

  1. Программа запускается путем записи ‘Start’ в консоли.
  2. Две функции setTimeout() вызываются с задержкой в 0 миллисекунд. Хотя задержка установлена на 0, JavaScript рассматривает ее как минимальную задержку и обеспечивает добавление обратных вызовов в очередь задач после завершения текущего контекста выполнения.
  3. Вызывается цепочка Promise.resolve().then(), добавляющая обратный вызов в очередь микрозадач. Микрозадачи, такие как промисы, имеют более высокий приоритет, чем обычные задачи/события.
  4. Программа записывает в консоль ‘End’.
  5. Цикл событий проверяет стек вызовов и обнаруживает, что он пуст.
  6. Цикл событий проверяет очередь задач и выбирает самую старую задачу (первый обратный вызов setTimeout()) для выполнения.
  7. В консоль записывается ‘Timeout 1’.
  8. Цикл событий снова проверяет стек вызовов и обнаруживает, что он пуст.
  9. Цикл событий переходит к следующей задаче в очереди задач и выполняет второй обратный вызов setTimeout().
  10. В консоль записывается ‘Timeout 2’.
  11. Цикл событий снова проверяет стек вызовов и обнаруживает, что он пуст.
  12. Цикл событий проверяет очередь микрозадач и выполняет обратный вызов Promise.resolve().then().
  13. В консоль выводится сообщение ‘Promise resolved’.

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

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

5. Модули

Вот пример программы, демонстрирующий, как создавать и использовать модули в JavaScript:

Сначала создадим модуль math.js, который экспортирует математические функции.

Затем создадим другой файл main.js, импортирующий и использующий функции из модуля math.js.

В этой программе есть два файла: math.js и main.js.

Файл math.js  —  это модуль, который экспортирует три функции: add (сложение), subtract (вычитание) и multiply (умножение). Каждая функция определяется с помощью ключевого слова export.

В файле main.js импортируем функции из модуля math.js с помощью оператора import. Внутри фигурных скобок {} указываем имена функций, которые хотим импортировать. Оператор import использует относительный путь (‘./math.js’) для обнаружения файла модуля.

Затем используем импортированные функции add, subtract и multiply в файле main.js для выполнения математических операций и вывода результатов на консоль.

Для выполнения этой программы понадобится среда JavaScript, поддерживающая модули ES, например современный браузер с поддержкой нативных модулей или последняя версия Node.js (с флагом — —experimental-modules).

При выполнении файла main.js вы должны увидеть результаты математических операций, записанные в консоль.

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

6. Генераторы

В этой программе мы определяем функцию-генератор под названием countUp. Генератор countUp выдает числа от start (заданного начального значения) до end (конечного значения) с помощью цикла for. Ключевое слово yield используется для приостановки генератора и выдачи текущего значения.

Затем создаем объект-генератор, вызывая функцию countUp с нужными аргументами (в данном случае 1 и 5).

Для использования значений, генерируемых генератором, мы применяем цикл for…of для итерации по объекту-генератору. На каждой итерации цикл извлекает следующее значение, выданное генератором, и присваивает его переменной num. Затем мы выводим значение num на консоль.

При выполнении этой программы вы должны увидеть, что в консоль выводятся числа от 1 до 5.

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

7. Стрелочные функции

В этой программе используются три функции: multiply (умножение), divide (деление) и add (сложение).

Функция multiply  —  это обычная функция, определяемая с помощью ключевого слова function. Она принимает два параметра (a и b) и возвращает их произведение.

Функция divide  —  это стрелочная функция, определяемая с помощью синтаксиса arrow (=>). Она также принимает два параметра и возвращает результат их деления. Стрелочные функции обеспечивают более лаконичный синтаксис по сравнению с обычными функциями.

Функция add  —  это еще одна стрелочная функция, но с неявным возвратом. Когда стрелочная функция имеет в своем теле одно выражение, можно опустить фигурные скобки {} и ключевое слово return. Результат выражения возвращается неявно.

Наконец, мы вызываем функции с разными аргументами и записываем результаты в консоль. 

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

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

8. Асинхронная итерация

Асинхронная итерация в JavaScript позволяет выполнять итерацию над асинхронными источниками данных, такими как промисы и асинхронные генераторы. Вот пример программы, демонстрирующий асинхронную итерацию с использованием цикла for await…of:

В этой программе определяем функцию асинхронного генератора под названием getData. Генератор выдает значения из массива (data) после имитации асинхронной операции. Ключевое слово await используется в цикле для приостановки работы генератора в ожидании разрешения промиса.

Для выполнения асинхронной итерации используем самовызывающуюся асинхронную функцию, содержащую цикл for await…of. Цикл выполняет итерации над асинхронным объектом-генератором, возвращаемым функцией getData(). На каждой итерации цикл ожидает следующего значения, выдаваемого генератором, и присваивает его переменной value. Затем выводим value в консоль.

При выполнении этой программы вы должны увидеть значения 1, 2, 3, 4 и 5, выведенные на консоль, с задержкой в 1 секунду между каждым значением из-за имитации асинхронной работы.

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

9. Прокси

В этой программе есть объект target, к которому нужно применить прокси. Создаем объект Proxy, передавая целевой объект target в качестве первого аргумента и объект-обработчик (handler) в качестве второго аргумента.

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

  1. get. Эта ловушка вызывается при доступе к свойству прокси. Она регистрирует свойство, к которому осуществляется доступ, и возвращает соответствующее значение из целевого объекта target.
  2. set. Эта ловушка вызывается при установке свойства прокси. Она регистрирует устанавливаемое свойство и присваивает значение соответствующему свойству в целевом объекте target.
  3. deleteProperty. Эта ловушка вызывается при удалении свойства из прокси. Она регистрирует удаляемое свойство и удаляет его из целевого объекта target.

Затем создаем объект proxy, который действует как прозрачный посредник между кодом и целевым объектом target. Любые операции, выполняемые над proxy, вызывают соответствующие методы-ловушки, определенные в объекте-обработчике.

В программе обращаемся к свойствам  —  name и age. Устанавливаем новое значение для свойства age, удаляем свойство name и снова обращаемся к свойству name через прокси. Каждая операция вызывает соответствующую ловушку, и связанные с ней сообщения журнала выводятся на консоль.

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

10. API Reflect

В этой программе есть объект obj, к которому нужно применить прокси с помощью Reflect API.

Определяем объект-обработчик handler, содержащий методы-ловушки (get и set), которые будут перехватывать операции доступа к свойствам и присвоения прокси.

Внутри методов-ловушек get и set используем соответствующие методы Reflect (Reflect.get и Reflect.set) для выполнения операций доступа и присвоения свойств целевого объекта target.

Далее создаем объект proxy с помощью конструктора Proxy, передавая obj в качестве целевого объекта target и объекта-обработчика handler.

Затем получаем доступ к свойствам (name и age) через прокси и устанавливаем новое значение для свойства age. Каждая операция вызывает соответствующую ловушку, а связанные с ней сообщения журнала выводятся на консоль с помощью console.log.

API Reflect предоставляет набор вспомогательных методов, которые отражают основные операции, выполняемые над объектами, такие как доступ к свойствам (Reflect.get), присвоение свойств (Reflect.set) и удаление свойств (Reflect.deleteProperty). Эти методы можно использовать вместе с Proxy API для обеспечения пользовательского поведения и тонкого контроля над операциями с объектами.

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи TK Rose: JavaScript Ideas Every Web Developer Should Understand

Предыдущая статьяОсновы реактивного программирования
Следующая статьяНужно ли дизайнеру уметь писать код?