你有没有想过:刷新一下页面,购物车还在、主题色没变、登录状态也没掉——这些数据都存在哪?答案就是浏览器的各种存储。本文逐个介绍 localStorage、sessionStorage、Cookie 和 IndexedDB 的用法与适用场景,并带你做一个主题切换 + 跨标签页同步的完整小案例,学完你就知道"数据该往哪儿放"。

1. 先认识存储家族

浏览器提供的存储手段很多,选错了会带来麻烦:数据存不下、被意外清掉、或者每发一次请求都背上沉重的 Cookie。先看一张对比表:

存储容量持久性特点
localStorage约 5MB永久同步 API,简单易用
sessionStorage约 5MB标签页关闭即清每个标签页独立
Cookie约 4KB可设过期时间每次请求自动携带
IndexedDB数百 MB 以上永久异步,支持索引与事务

选型口诀:小数据、要持久、纯前端用 → localStorage;只在本标签页临时用 → sessionStorage;需要服务器读取 → Cookie;数据量大、结构复杂 → IndexedDB。

2. localStorage 基础操作

localStorage 的 API 只有 4 个方法,所有值都会以字符串形式保存。注意一个坑:数字存进去再取出来是字符串,别拿它直接做加法运算,否则会得到字符串拼接的结果。

localStorage.setItem('username', 'linwan');
console.log(localStorage.getItem('username'));
console.log(localStorage.getItem('not-exist'));
localStorage.removeItem('username');
localStorage.setItem('a', '1');
localStorage.setItem('b', '2');
console.log(localStorage.length);
localStorage.clear();

getItem 找不到键时返回 null,所以判断"是否存在"用 !== null 比直接 if 更严谨,能区分"不存在"和"存了空字符串"两种情况。clear() 会清空当前域名下的全部数据,生产代码里慎用。

localStorage 还有两个容易被忽略的点:一是它按"协议 + 域名 + 端口"隔离,http://a.com 和 https://a.com 的数据互不相通,本地调试时用 localhost 和用 127.0.0.1 也互相看不见;二是容量约 5MB,写满会抛 QuotaExceededError,所以写入时最好包一层 try/catch,存不下时给用户降级提示,而不是让页面静默出错。

一个实用技巧:存数据前先估算大小,JSON.stringify(obj).length 就是这条记录占用的字符数,心里有数就不会突然撞上限;如果发现快满了,可以按写入时间清理最旧的数据,很多"缓存类"功能的淘汰策略就是这么写的。

3. 存对象:先序列化再反序列化

localStorage 只能存字符串,对象要先用 JSON.stringify 变成字符串,读取时再用 JSON.parse 还原。下面是一个待办事项的存取封装,顺便演示了带前缀的键名和容错解析:

const todo = { id: 1, text: '学完浏览器存储', done: false };

function saveTodo(todo) {
  localStorage.setItem('todo:' + todo.id, JSON.stringify(todo));
}

function loadTodo(id) {
  const raw = localStorage.getItem('todo:' + id);
  if (raw === null) {
    return null;
  }
  try {
    return JSON.parse(raw);
  } catch (err) {
    console.error('数据损坏:', err);
    return null;
  }
}

saveTodo(todo);
console.log(loadTodo(1));
console.log(loadTodo(999));

用 todo:{id} 这种带业务前缀的键名,可以避免不同模块的数据互相覆盖。JSON.parse 包在 try/catch 里也很有必要——用户手改过数据、或者旧版本代码写入过非法 JSON 时,直接解析会抛异常,整个页面跟着崩。

想遍历所有已存的键,Object.keys(localStorage) 就能拿到键名数组——localStorage 实现了 Storage 接口,支持用对象的方式访问。给每个键加上业务前缀还有个额外好处:清理时按前缀过滤,只删自己模块的数据,不会误伤其他功能。真实项目里建议把存取封装成统一的模块,暴露 get/set/remove 三个函数,内部处理序列化和异常,业务代码永远不直接碰 localStorage。

4. sessionStorage 与 Cookie

sessionStorage 的 API 和 localStorage 完全一样,区别只在生命周期:它绑定当前标签页,标签页一关数据就没了。典型用途是"表单填写到一半,误刷新不丢"这类临时状态。Cookie 则完全不同——它会被浏览器自动附在每次请求头上,所以容量被限制得很小:

sessionStorage.setItem('draft', '正在写的草稿');
console.log(sessionStorage.getItem('draft'));

function getCookie(name) {
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const match = document.cookie.match(
    new RegExp('(?:^|; )' + escaped + '=([^;]*)')
  );
  return match ? decodeURIComponent(match[1]) : null;
}

document.cookie = 'theme=dark; max-age=3600';
console.log(getCookie('theme'));

Cookie 的读写都是字符串操作:document.cookie 一次只能写一个键值对,读出来却是所有 Cookie 拼成的大字符串,所以要写解析函数。代码里那串正则先把键名里的特殊字符转义,再精确匹配,max-age=3600 表示 1 小时后过期。

现代开发里,JS 直接读写 Cookie 的场景越来越少,因为安全问题太多了。重要的 Cookie 应该加上 HttpOnly(禁止 JS 读取,防 XSS 窃取)、Secure(只走 HTTPS)、SameSite(防 CSRF)——而这些属性只能由服务器通过 Set-Cookie 响应头设置,前端 JS 碰不到。这也是为什么登录态一般交给服务端管理:前端只负责把服务器返回的 token 妥善保存,别自己往 Cookie 里塞敏感信息。

5. IndexedDB:浏览器的"数据库"

当数据是成百上千条记录、还要按条件查询时,localStorage 就力不从心了。IndexedDB 是浏览器内置的 NoSQL 数据库,支持索引、事务和游标。它的 API 偏底层,先封装一层打开数据库的逻辑,后面用起来就顺手了:

function openDB() {
  return new Promise(function (resolve, reject) {
    const request = indexedDB.open('codelab-db', 1);

    request.onupgradeneeded = function () {
      const db = request.result;
      if (!db.objectStoreNames.contains('todos')) {
        db.createObjectStore('todos', { keyPath: 'id' });
      }
    };

    request.onsuccess = function () {
      resolve(request.result);
    };

    request.onerror = function () {
      reject(request.error);
    };
  });
}

indexedDB.open(name, version) 打开数据库;第一次打开或版本号变大时会触发 onupgradeneeded,在这里建表(官方叫"对象仓库 object store")。keyPath: 'id' 表示用每条记录的 id 字段做主键,相当于关系数据库里的主键。

写入数据要走"事务":事务提交成功才算真正落盘,中途出错会自动回滚。读取是异步的,通过 onsuccess 拿结果:

async function addTodo(todo) {
  const db = await openDB();
  return new Promise(function (resolve, reject) {
    const tx = db.transaction('todos', 'readwrite');
    tx.objectStore('todos').add(todo);
    tx.oncomplete = function () {
      resolve();
    };
    tx.onerror = function () {
      reject(tx.error);
    };
  });
}

async function getTodo(id) {
  const db = await openDB();
  return new Promise(function (resolve, reject) {
    const request = db.transaction('todos')
      .objectStore('todos').get(id);
    request.onsuccess = function () {
      resolve(request.result);
    };
    request.onerror = function () {
      reject(request.error);
    };
  });
}

addTodo({ id: 1, text: '搞定 IndexedDB', done: false })
  .then(function () {
    return getTodo(1);
  })
  .then(function (todo) {
    console.log('读到了:', todo.text);
  });

读操作用只读事务就够了,性能更好;写操作必须用 readwrite。另外提醒一点:每次操作完记得 db.close(),大量标签页同时打开时,连接数有限,不关会耗尽连接导致后续请求排队。

IndexedDB 的能力不止增删改查:建表时可以给某个字段声明 index,之后就能按这个字段快速查询;一次事务里可以连续操作多条记录,要么全部成功、要么全部回滚;数据量大的列表用 getAll() 配合游标分页读取,比"一次全读进内存"稳得多。第一次接触觉得它繁琐很正常,现在有很多库(如 idb、Dexie)把它的 Promise 封装做得很好,但理解底层的事务模型依然重要——出问题时你才知道去哪查。

6. 实战:主题切换与跨标签页同步

把前面学的串起来:主题偏好存 localStorage,页面加载时读取并应用;再用 storage 事件监听其他标签页的修改,实现"一个标签页改主题,所有标签页同步变"的效果:

const select = document.getElementById('theme-select');
const saved = localStorage.getItem('theme') || 'light';

document.documentElement.dataset.theme = saved;
if (select) {
  select.value = saved;
  select.addEventListener('change', function () {
    document.documentElement.dataset.theme = select.value;
    localStorage.setItem('theme', select.value);
  });
}

window.addEventListener('storage', function (event) {
  if (event.key === 'theme' && event.newValue) {
    document.documentElement.dataset.theme = event.newValue;
    if (select) {
      select.value = event.newValue;
    }
  }
});

storage 事件只在"其他标签页"修改 localStorage 时触发,当前页面自己改不会触发,所以不用担心死循环。dataset.theme 会变成 data-theme 属性,配合 CSS 的属性选择器,就能用一套 CSS 实现多套主题。

最后提醒一个环境问题:localStorage、sessionStorage 和 indexedDB 都只存在于浏览器环境,在 Node.js 或服务端渲染(SSR)里直接调用会报 ReferenceError。如果代码要在多种环境跑,先判断 typeof window !== 'undefined' 再访问;某些隐私模式还会禁用存储,访问时同样要容错。把"存储可能不可用"当成默认假设,代码才够健壮。

7. 总结与练习

四种存储各有定位:localStorage 存持久小数据,sessionStorage 存临时状态,Cookie 用于和服务器交换身份信息,IndexedDB 扛大数据量。还要记住:所有存储都有被用户清除的可能,读取时务必做容错处理。

练习:

💡 在 DevTools 的 Application 面板可以可视化查看、编辑甚至删除所有存储数据,调试存储问题比 console.log 高效十倍。