localStorage и sessionStorage
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Пользователь обновил страницу — и всё, что жило в переменных, исчезло. localStorage и sessionStorage — два простых ящика внутри браузера: кладёте строку по ключу и находите её там же после перезагрузки, а иногда и через месяц.
У обоих хранилищ один и тот же интерфейс Storage: положить — setItem, взять — getItem, удалить — removeItem, стереть всё — clear. Доступ синхронный: значение возвращается сразу, без промисов и колбэков. Данные привязаны к origin — паре «схема + домен + порт», так что чужой сайт в ваш ящик не заглянет.
Одна деталь, на которой спотыкаются все: если ключа нет, getItem возвращает null, а не undefined. Проверка if (value === null) — самая честная. К хранилищу можно обращаться и как к объекту (localStorage.theme), но так вы рискуете задеть служебные имена вроде length или clear, поэтому методы надёжнее.
Отсюда и самый частый сценарий — черновик формы, который переживает случайно закрытую вкладку. Сохраняем на каждое изменение поля, а при загрузке страницы читаем обратно.
Хранилище знает ровно один тип — строку. Всё, что вы передадите, молча пройдёт через приведение: число 5 станет "5", false станет "false", а объект превратится в бесполезное "[object Object]". Особенно коварна строка "false": она непустая, а значит истинная.
Поэтому объекты и массивы кладут через JSON.stringify, а достают через JSON.parse. Сериализация не бесплатна, и потери у неё те же, что разбирала статья «JSON: обмен данными»: undefined и функции пропадают, Date превращается в строку, Map и Set — в пустой объект. Приятный бонус — JSON.parse(null) не падает, а возвращает null, так что отсутствующий ключ можно разбирать без лишней проверки.
sessionStorage устроен точно так же, но живёт только в пределах одной вкладки. Перезагрузку он переживает, закрытие вкладки — нет. Новая вкладка того же сайта получает пустое хранилище; дубликат вкладки (Duplicate Tab) — копию, которая дальше живёт своей жизнью. Это ровно то, что нужно для состояния шага в мастере или выбранной корзины сравнения, которую не хочется тащить в соседнее окно.
Полезно держать рядом и третьего участника — куки. Они меньше, но уходят на сервер с каждым запросом, а с флагом httpOnly вообще недоступны из JavaScript. Именно поэтому куки — про аутентификацию, а Storage — про удобство интерфейса; их правила целиком разбирает следующая статья — «Cookie: что нужно знать».
| localStorage | sessionStorage | cookie | |
|---|---|---|---|
| Живёт | пока не удалят | пока открыта вкладка | до Expires / Max-Age |
| Переживает перезагрузку | да | да | да |
| Виден в соседней вкладке | да, тот же origin | нет | да |
| Объём | около 5 МБ | около 5 МБ | около 4 КБ |
| Уходит на сервер | нет | нет | да, с каждым запросом |
| Доступ из JS | всегда | всегда | нет, если httpOnly |
Ориентир — примерно 5 МБ на origin: текущий Chrome вмещает около 5,2 млн символов (в байтах это вдвое больше), другие браузеры и старые версии дают меньше, точное число зависит от браузера и настроек, и sessionStorage считается отдельно. Считаются символы: тысяча кириллических букв стоит ровно столько же, сколько тысяча латинских, и ключи тоже занимают место. Каждый символ списывается в квоту двумя байтами UTF-16, эмодзи суррогатной парой — вдвое дороже: замер в Chrome даёт один потолок и для латиницы, и для кириллицы, и вдвое меньший для эмодзи. Когда она кончилась, setItem бросает DOMException с именем QuotaExceededError — не возвращает false, а именно бросает.
Второе ограничение важнее первого: запись синхронная и происходит в главном потоке. Сохранить пару килобайт настроек — незаметно, сохранять картинку в base64 на каждое движение мыши — верный способ получить дёрганый интерфейс. Для мегабайтов есть IndexedDB, для файлов и ответов сети — Cache API. Какую обёртку над IndexedDB берут и когда хватает простого ящика localStorage — на полке браузера.
Обёртка с try/catch нужна не только из-за квоты: в приватном режиме и при запрете хранилища в настройках сам доступ к localStorage может бросить SecurityError. Приложение от этого падать не должно — просто останется без сохранённых данных.
Когда одна вкладка меняет localStorage, во всех остальных вкладках того же origin на window прилетает событие storage. В той вкладке, которая писала, событие не срабатывает — это специально: она и так знает, что сделала. Так синхронизируют тему оформления и разлогинивают все вкладки разом.
У события есть key, oldValue, newValue, url страницы-виновника и storageArea — ссылка на само хранилище. Удаление ключа приходит с newValue: null, а clear() — с key: null. Если нужен полноценный обмен сообщениями между вкладками, а не отслеживание значений, берите BroadcastChannel: он не требует ничего записывать на диск.
Хранилище открыто любому скрипту на странице — вашему коду, виджету поддержки, счётчику аналитики и внедрённому через XSS чужому коду одинаково. Флага вроде httpOnly здесь нет. Поэтому токены доступа, пароли, номера карт и персональные данные там не хранят: одна дыра в сторонней библиотеке — и всё содержимое ящика уезжает наружу.
Второй запрет мягче, но нарушают его чаще: 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.
Ключевой момент — превратить строку в число до сложения. Number(null) даёт 0, поэтому отдельная ветка «первый запуск» не нужна.
Хвостик || 0 страхует от мусора: если в ключе окажется "abc", Number вернёт NaN, и счётчик навсегда сломался бы. При записи число само приведётся к строке — это тот редкий случай, когда неявное приведение работает на нас.
Настройки с запасным вариантом
Под ключом settings лежит JSON с настройками. Напиши readSettings(), которая возвращает объект настроек, а если ключа нет или в нём испорченная строка — значения по умолчанию { theme: "light", fontSize: 16 }. Функция не должна бросать исключение ни при каком содержимом ключа.
JSON.parse на битой строке бросает SyntaxError, поэтому разбор оборачиваем в try/catch, а недостающие поля добираем спредом из значений по умолчанию.
Спред решает и вторую задачу — совместимость со старыми версиями формата: если в сохранённом объекте нет fontSize, значение придёт из DEFAULTS. Каждый раз возвращается новая копия, чтобы вызывающий код не испортил общий объект по умолчанию.