company_banner

Почему пора перестать использовать JavaScript IIFE

Автор оригинала: John Au-Yeung
  • Перевод
Немедленно вызываемая функция (Immediately Invoked Function Expression — IIFE) в JavaScript — это конструкция, позволяющая вызывать функцию непосредственно после ее определения.

image

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

Мы можем объявлять переменные внутри автономных блоков


С тех пор как появился стандарт ES6, мы можем объявлять переменные и константы внутри блока с помощью let и const. Вместе с этим стандартом также появилась возможность выделять переменные и константы в автономные блоки, недоступные извне.

Например:

{
 let x = 1;
}

Тогда x не будет доступен извне. Это явно лучше, чем:

(()=>{
 let x = 1;
})();

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

Мы можем избавиться почти от всех замыканий


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

Например, здесь мы можем получить некоторые побочные эффекты:

const id = (() => {
 let count = 0;
 return () => {
   ++count;
   return `id_${count}`;
 };
})();

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

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

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

let count = 0;
export const id = () => {
 ++this.count;
 return `id_${count}`
}

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

Мы можем по-другому создавать алиасы для переменных


Мы могли бы написать такое:

window.$ = function foo() {
 // ...
};(function($) {
 // ...
})(jQuery);

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

И тогда достаточно написать:

import { $ as jQuery } from "jquery";
const $ = () => {};

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

Мы можем легко получить глобальный объект


С появлением globalThis нам не нужно беспокоиться об имени глобального объекта в различных окружениях, поскольку он становится стандартом.

Мы могли бы использовать IIFE для захвата глобального объекта:

(function(global) {
 // ...
})(this);

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

const globalObj = self || window || global;

Если быть более точным, то можно написать вот что:

const getGlobal = () => {
 if (typeof self !== 'undefined') { return self; }
 if (typeof window !== 'undefined') { return window; }
 if (typeof global !== 'undefined') { return global; }
 throw new Error('unable to locate global object');
};

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

Мы можем проще выполнять минификацию


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

Webpack, Browserify, Parcel, Rollup и так далее могут работать с модулями должным образом, поэтому мы должны использовать их для создания более чистого кода.

Вывод


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

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

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

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Похожие публикации

Комментарии 36

    0

    Уже везде есть глобальная переменная globalThis.

      +3

      Хорошо, окей, а если мне в блоке инициализации приложения нужно пользоваться async/await?


      Пока что получается такая конструкция:


      (async () => {
        await someDatabaseInit()
        await someCacheInit()
        // ...
      })()
        +2

        someDatabaseInit().then(() => someCacheInit()).catch(() => ???)

          +1

          Дельный комментарий на самом деле.


          Не нужно забывать об обработке ошибок, await нужно обернуть в try/catch, и конструкция получается более громоздкой чем цепочка с .then/catch

            0

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


            window.onerror = e => {
              // тут может оказаться нормальный обработчик
              console.error(e)
            }
            (async () => {
              // ...
              throw new Error('catch me')
            })().catch(onerror)</source>
            +8
            Top-level await скоро будет это решать v8.dev/features/top-level-await, уже есть в TypeScript v3.8.
              0

              Кстати, можно еще так:


              const promise = (async () => {
                await someDatabaseInit()
              })()
              
              promise.catch(console.error)

              Использую асинхронные замыкания чаще всего в React-хуках.

                0
                Тоже об этом вспомнил. Но похоже, это единственный случай, когда IIFE целесообразен.
                  0
                  А чем плохо
                  async function init() {
                    await ...
                  }
                  
                  init();
                  

                  ?
                    0

                    Тем, что исключения ловить обязательно

                      +1
                      Их и так ловить приходится (.catch), только немного в другом виде. Принципиальной разницы не вижу
                    0
                    Вроде собираются сделать, чтобы можно было await использовать на уровне модуля, не оборачивая в async функцию.
                    Уже Stage 3. Значит сделают.
                    github.com/tc39/proposal-top-level-await
                    0
                    транспайлинг в es5 -> пересечение имен переменных из-за поднятие/высасывания
                      0
                      Поднятие все же
                        0
                        По ссылке что я приложил используется «высасывание». Добавил и ваш источник
                          +1
                          Да. И для того что бы понять о чем идет речь мне пришлось по этой ссылке перейти. В то время как «поднятие» или «hoisting» все же более привычное определение этому явлению.

                          Бритва Оккама…

                          P. S. Спасибо за правку первоначального поста
                      +1
                      А ничего, что вебпак все модули в IIFE заворачивает?
                        0

                        Пусть делает, что должен. Это же инструмент. При минификации тоже происходит много вещей, которые не нужно делать руками. Так и тут — речь про то, что не нужно самим писать IIFE.

                        0
                        Замыкания вызывают побочные эффекты, что…

                        каким образом?
                          0
                          В примере в статье как раз показан пример. Замыкание возвращает функцию, которая изменяет значение переменной count, объявленой снаружи этой функции. Это и есть побочный эффект в терминологи функционального программирования.
                            0
                            переменная count является частью замыкания и не объявлена вне замыкания, следовательно замыкание не имеет побочный эффект. count является «приватным свойством» замыкания.
                            Замыкание являются частью ФП. И не нарушает принципов ФП
                              0
                              переменная count является частью замыкания и не объявлена вне замыкания

                              Верно. Так же верно то что, переменная count объявлена снаружи функции, которую возвращает замыкание.

                              https://en.wikipedia.org/wiki/Side_effect_(computer_science)
                              Первый абзац:
                              function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the main effect) to the invoker of the operation

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

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

                              Если вы вызовем функцию id() из нашего примера дважды (с одним и тем же набором аргументов; если быть точным не передавая никаких аргументов), то она вернёт нам разный результат. Это нарушает принцип ФП. Причиной является то, что функция id() имеет побочный эффект.
                          0
                          я бы лучше предложил перестать использовать классы (ой прости господи за не популярное мнение)
                            0
                            У меня тоже такое не популярное мнение. Но не потому что классы плохие для всего, а потому что в JS имеют особенности. Например мне не нравится прототипное наследование, имеющее контекст только в виде самого объекта и вечного this, и не позволяющее собрать объект несколькими конструкторами и делать в него примеси или замены полей без всякой иерархии (оставляя объект плоским одноуровневым).
                              0
                              вот именно, в JS свои особенности и свои практики. Пришли новые люди — принесли свои практики которые плохо сюда заходят и создали кашу. Сам был в ряду таковых (:facepalm:)
                                0
                                не нравится прототипное наследование, имеющее контекст только в виде самого объекта и вечного this, и не позволяющее собрать объект несколькими конструкторами и делать в него примеси или замены полей без всякой иерархии


                                А как бы вам нравилось чтобы это работало? Интересно понять как вы видите удачное решение этой задачи.
                                  0
                                  Вот обычный принцип, который работал испокон веков (осовременен):

                                  function FirstConstructor(){
                                      const t = this;
                                      let privateVar = 123;
                                      t.publicMethod = val => privateVar + val; // никаких this для приватных полей
                                  }
                                  function SecondConstructor(){
                                      const t = this;
                                      let privateVar = 456; // приватные поля не пересекутся с родителем
                                      delete t.publicMethod; // удаляем метод и его в объекте не будет нигде
                                      t.anotherPublicMethod = ()=> privateVar; // никаких this для приватных полей
                                      function privateMethod(){ return privateVar } // нет this для приватных полей
                                      t.thirdPublicMethod = ()=> privateMethod() + 7; // нет this для прив. методов
                                  }
                                  let myInstance = new FirstConstructor();
                                  SecondConstructor.call( myInstance );

                                  В результате выходит экземпляр, у которого нет прототипов (кроме стандартного), всё на одном уровне, честные приватные поля, работающие без this.#, и удалённый метод. Есть конечно недостатки, и их не мало, но и преимущества есть.
                              0
                              Полностью отказываться от этого подхода не стоит. У него по-прежнему остались применения. Например скоуп для сиюминутных локальных функций, работающих с общей локальной переменной. Или например вычисление значения для ключа при написании объекта литералом. Эти случаи конечно можно обходить иначе, но есть нюансы.
                                0
                                Или например вычисление значения для ключа при написании объекта литералом.

                                Для этого напрашиваются template strings. Какие с ними могут быть нюансы?
                                  0
                                  let obj = {
                                      myKey: (()=>{
                                          if( !someCheck() ) return;
                                          ...
                                          return result;
                                      })
                                  }
                                    0

                                    А где тут IIFE? Да и то сократить можно


                                    let obj = {
                                        myKey(){
                                            if( !someCheck() ) return;
                                            ...
                                            return result;
                                        }
                                    }

                                    Если забыли добавить (). То это костыльный способ заменить do exressions

                                      0
                                      В моём примере функция в скобках вызывается до создания ключа и записывает в ключ готовое значение. А в вашем примере это метод, а не свойство, он каждый раз вычисляет что-то, а не содержит готовое значение.
                                        0
                                        Пардон, забыл после скобок (), так что вы правы
                                      0
                                      Всегда считал, что это костыль и так делать не надо.
                                        0
                                        Без этого пришлось бы вычислять ключ выше. С одной стороны литерал смотрелся бы чище, но с другой — всё в разных местах и приходится собирать логику по кускам.
                                  +1
                                  (function() {
                                      if (something) {
                                          return;
                                      }
                                  
                                      // ...
                                  })();

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

                                  Самое читаемое