В скриптовых языках, подобных JavaScript, внутри файлов (но вне определений) можно писать любой код: определения функций, вызовы функций, определения и изменения переменных. Такая свобода упрощает разработку, например, создание одноразовых скриптов для каких-то простых или не очень задач. С другой стороны, при неаккуратной разработке появляются ошибки, значительно усложняющие код и его поддержку. Они так часто встречаются в продакшен коде, что об этом нужно поговорить отдельно.
Эти проблемы не специфичны для JavaScript, то же самое встречается и во многих других интерпретируемых языках, таких как Python, Ruby или PHP.
Подробнее о разнице между модулями и скриптами можно прочитать в статье. Здесь же мы сосредоточимся на неверно спроектированных модулях.
Подписывайтесь на канал Кирилла Мокевнина в Telegram — чтобы узнать больше о программировании и профессиональном пути разработчика
Предположим, что у нас есть модуль index.js с таким содержимым:
Где-то в других местах программы он импортируется и используется. Как правило, импорт подобных модулей происходит не в одном месте, а в разных местах программы.
Возникает вопрос, сколько раз реально вызывается содержимое файла index.js? Проверить очень легко — достаточно внутри модуля вызвать печать на экран:
Запустив программу, можно увидеть, что вызов произошел ровно один раз. То же самое относится и к константе. Она была определена ровно один раз. В этом смысле export
сильно отличается от return
внутри функций. return
вызывается на каждый вызов, а export
срабатывает только при первом импорте, и затем всегда переиспользуется.
Из этой особенности есть важное следствие — модуль легко превратить в хранилище глобального состояния.
Где-то в других частях системы:
Обратите внимание. Хотя это и разные файлы, объект, который импортируется из state.js, всегда тот же самый.
Что же здесь произошло? Хотя код кажется удобным для использования, он основывается на практиках, которые в программировании всегда считались плохими. Фактически здесь была создана глобальная переменная, к которой может получить доступ любая часть системы (через импорт) и изменить ее любым способом. Это довольно опасно, и может приводить к серьезным ошибкам. Именно поэтому глобальные переменные рекомендуется избегать всегда, когда это можно сделать.
Частично проблему можно решить методами доступа, добавленными к состоянию:
Так мы получим не просто глобальные данные, а глобальный объект (с точки зрения ООП, а не типов данных). Однако, это мало что меняет. Глобальные данные по прежнему остались глобальными.
Почему это плохо? Представьте, что мы написали библиотеку для автокомплита и подключили ее на странице. Эта библиотека, скорее всего, внутри себя хранит те данные, которые она загрузила с бекенда, например, для ускорения доступа. Затем, нам понадобилось подключить два автокомплита на одной странице. И вот тут начнутся сюрпризы. Изменения в одном автокомплите начнут влиять на другой.
В теории для различных автокомплитов можно сделать разные куски данных, но, например, в случае динамического добавления и удаления автокомплитов, проблема все равно вылезет. То есть не существует теоретического способа сделать подобную реализацию такой, чтобы с ней не возникало никаких проблем во всех ситуациях
Есть и другой пример. Подобные решения сразу всплывают в тестах. В тестировании тесты не должны зависеть друг от друга, но с глобальным состоянием это невозможно. Любые изменения состояния в одном тесте отразятся на втором. Можно, конечно, пытаться их восстанавливать в исходное состояние руками, но это крайне ненадежно (нужно умело обрабатывать ошибки) и добавляет много ненужной сложности.
Содержание
Локальное состояние
Правильный способ работы с состоянием – его локализация относительно приложения. Например, в случае автокомплитов это может быть функция:
И использование:
Подобная организация кода позволяет добавлять на страницу любое число автокомплитов не боясь, что они друг на друга повлияют. У каждого автокомплита свое локальное состояние, с которым он работает.
Точно так же нужно поступать и во всех остальных ситуациях, не важно используются там фреймворки или пишется нативный JavaScript. Общий принцип работы с состоянием остается неизменным – все приложение заворачивается в функцию, которая определяет состояние глобальное для конкретного приложения, но локальное относительно среды запуска этого приложения.
Допустимое глобальное состояние
Есть ли примеры, когда глобальное состояние допустимо? Как минимум, им пользуются многие библиотеки для конфигурации или своих внутренних нужд. Это может рождать определенные сложности, как описано выше, но довольно часто мешает не сильно:
Итого. Никогда не используйте глобальное состояние для данных. Если нужно хранить данные – используйте объекты, создаваемые внутри приложения.
Дополнительные материалы
Kirill Mokevnin
5 лет назад