localStorage и sessionStorage

хранение≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

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

Разделы темы:·

У обоих хранилищ один и тот же интерфейс Storage: положить — setItem, взять — getItem, удалить — removeItem, стереть всё — clear. Доступ синхронный: значение возвращается сразу, без промисов и колбэков. Данные привязаны к origin — паре «схема + домен + порт», так что чужой сайт в ваш ящик не заглянет.

Одна деталь, на которой спотыкаются все: если ключа нет, getItem возвращает null, а не undefined. Проверка if (value === null) — самая честная. К хранилищу можно обращаться и как к объекту (localStorage.theme), но так вы рискуете задеть служебные имена вроде length или clear, поэтому методы надёжнее.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Хранилище знает ровно один тип — строку. Всё, что вы передадите, молча пройдёт через приведение: число 5 станет "5", false станет "false", а объект превратится в бесполезное "[object Object]". Особенно коварна строка "false": она непустая, а значит истинная.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Поэтому объекты и массивы кладут через JSON.stringify, а достают через JSON.parse. Сериализация не бесплатна, и потери у неё те же, что разбирала статья «JSON: обмен данными»: undefined и функции пропадают, Date превращается в строку, Map и Set — в пустой объект. Приятный бонус — JSON.parse(null) не падает, а возвращает null, так что отсутствующий ключ можно разбирать без лишней проверки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

sessionStorage устроен точно так же, но живёт только в пределах одной вкладки. Перезагрузку он переживает, закрытие вкладки — нет. Новая вкладка того же сайта получает пустое хранилище; дубликат вкладки (Duplicate Tab) — копию, которая дальше живёт своей жизнью. Это ровно то, что нужно для состояния шага в мастере или выбранной корзины сравнения, которую не хочется тащить в соседнее окно.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Полезно держать рядом и третьего участника — куки. Они меньше, но уходят на сервер с каждым запросом, а с флагом httpOnly вообще недоступны из JavaScript. Именно поэтому куки — про аутентификацию, а Storage — про удобство интерфейса; их правила целиком разбирает следующая статья — «Cookie: что нужно знать».

localStoragesessionStoragecookie
Живётпока не удалятпока открыта вкладкадо Expires / Max-Age
Переживает перезагрузкудадада
Виден в соседней вкладкеда, тот же originнетда
Объёмоколо 5 МБоколо 5 МБоколо 4 КБ
Уходит на сервернетнетда, с каждым запросом
Доступ из JSвсегдавсегданет, если httpOnly

Ориентир — примерно 5 МБ на origin: текущий Chrome вмещает около 5,2 млн символов (в байтах это вдвое больше), другие браузеры и старые версии дают меньше, точное число зависит от браузера и настроек, и sessionStorage считается отдельно. Считаются символы: тысяча кириллических букв стоит ровно столько же, сколько тысяча латинских, и ключи тоже занимают место. Каждый символ списывается в квоту двумя байтами UTF-16, эмодзи суррогатной парой — вдвое дороже: замер в Chrome даёт один потолок и для латиницы, и для кириллицы, и вдвое меньший для эмодзи. Когда она кончилась, setItem бросает DOMException с именем QuotaExceededError — не возвращает false, а именно бросает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Второе ограничение важнее первого: запись синхронная и происходит в главном потоке. Сохранить пару килобайт настроек — незаметно, сохранять картинку в base64 на каждое движение мыши — верный способ получить дёрганый интерфейс. Для мегабайтов есть IndexedDB, для файлов и ответов сети — Cache API. Какую обёртку над IndexedDB берут и когда хватает простого ящика localStorage — на полке браузера.

Обёртка с try/catch нужна не только из-за квоты: в приватном режиме и при запрете хранилища в настройках сам доступ к localStorage может бросить SecurityError. Приложение от этого падать не должно — просто останется без сохранённых данных.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Когда одна вкладка меняет localStorage, во всех остальных вкладках того же origin на window прилетает событие storage. В той вкладке, которая писала, событие не срабатывает — это специально: она и так знает, что сделала. Так синхронизируют тему оформления и разлогинивают все вкладки разом.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У события есть key, oldValue, newValue, url страницы-виновника и storageArea — ссылка на само хранилище. Удаление ключа приходит с newValue: null, а clear() — с key: null. Если нужен полноценный обмен сообщениями между вкладками, а не отслеживание значений, берите BroadcastChannel: он не требует ничего записывать на диск.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Второй запрет мягче, но нарушают его чаще: Storage — не база данных. У записей нет срока годности, их не видит сервер, они не переезжают на другое устройство и исчезают вместе с очисткой истории. Всё, что дороже удобства — заказы, прогресс обучения, настройки аккаунта — должно жить на сервере, а в браузере лежать разве что копией для быстрого старта. И срок годности, если он нужен, придётся хранить самому: рядом со значением кладут метку времени и проверяют её при чтении.

Простое правило: localStorage — это кеш удобства, а не источник правды. Если потеря записи испортит пользователю день, её место на сервере.

  • localStorage живёт до явного удаления, sessionStorage — до закрытия вкладки; оба привязаны к origin и не уходят на сервер.
  • Внутри только строки: числа и объекты сохраняют через JSON.stringify, читают через JSON.parse, а отсутствующий ключ даёт null.
  • Объём — около 5 МБ на origin, запись синхронная: большие данные держат в IndexedDB, а setItem страхуют try/catch от QuotaExceededError.
  • Событие storage срабатывает в других вкладках того же сайта, но не в той, что писала — этим синхронизируют тему и выход из аккаунта.
  • Токены, пароли и персональные данные туда не кладут: хранилище целиком читает любой скрипт страницы, и срока годности у записей нет.

1/3Что выведет typeof после localStorage.setItem("count", 5) и чтения этого ключа?

2/3В каких вкладках сработает событие storage при записи в localStorage?

3/3Что произойдёт с данными в sessionStorage после нажатия F5?

Счётчик визитов

Напиши функцию visit(): при каждом вызове она увеличивает счётчик в localStorage и возвращает новое значение. На чистом хранилище первый вызов должен вернуть 1. Помни, что из хранилища приходит строка, а отсутствующий ключ — null.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ключевой момент — превратить строку в число до сложения. Number(null) даёт 0, поэтому отдельная ветка «первый запуск» не нужна.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Хвостик || 0 страхует от мусора: если в ключе окажется "abc", Number вернёт NaN, и счётчик навсегда сломался бы. При записи число само приведётся к строке — это тот редкий случай, когда неявное приведение работает на нас.

Настройки с запасным вариантом

Под ключом settings лежит JSON с настройками. Напиши readSettings(), которая возвращает объект настроек, а если ключа нет или в нём испорченная строка — значения по умолчанию { theme: "light", fontSize: 16 }. Функция не должна бросать исключение ни при каком содержимом ключа.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

JSON.parse на битой строке бросает SyntaxError, поэтому разбор оборачиваем в try/catch, а недостающие поля добираем спредом из значений по умолчанию.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Спред решает и вторую задачу — совместимость со старыми версиями формата: если в сохранённом объекте нет fontSize, значение придёт из DEFAULTS. Каждый раз возвращается новая копия, чтобы вызывающий код не испортил общий объект по умолчанию.