跳到主要内容
预计阅读 44 分钟

浏览器篇 | Storage

前言

前端开发离不开”存东西”。用户的登录态要存、主题偏好要存、购物车数据要存、表单草稿也想存……但浏览器提供的存储方案不止一种,而且每种方案的特性差异很大。面试中经常会遇到这样的问题:

  • Cookie 和 localStorage 有什么区别?
  • sessionStorage 关掉标签页就没了,那刷新呢?
  • SameSite 属性到底在防什么?
  • 什么时候该用 IndexedDB?

很多人对这些问题只有一个模糊的印象——“Cookie 小、localStorage 大”,但细节一追就含糊了。

本章我们就把浏览器端的几种主流存储方案——Cookie、localStorage、sessionStorage、IndexedDB——逐个拆解,从属性、限制、适用场景、安全性等维度建立一个清晰的对比框架。读完之后,面试中再遇到存储相关的问题,你应该能做到条理清楚、细节到位。


诊断自测

在开始正文之前,先用几道题测测你目前对浏览器存储的理解。答不上来也没关系,读完全文再回来对照。

Q1:Cookie 的 HttpOnly 属性有什么作用?设置了 HttpOnly 的 Cookie,前端 JS 还能读取到吗?

点击查看答案

设置了 HttpOnly 的 Cookie,前端 JavaScript 无法通过 document.cookie 读取或修改。它只会在浏览器发起 HTTP 请求时自动附带在请求头中。这是防御 XSS 攻击窃取 Cookie 的重要手段——即使页面被注入了恶意脚本,也拿不到标记了 HttpOnly 的 Cookie(比如存放 session ID 的那个)。

Q2:localStorage 和 sessionStorage 的数据,在同一个浏览器的不同标签页之间能共享吗?

点击查看答案

localStorage同源的所有标签页之间共享,只要协议 + 域名 + 端口一致,不同标签页读取到的 localStorage 数据是同一份。sessionStorage不共享——每个标签页都有自己独立的 sessionStorage,即使是同一个 URL。但有一个例外:通过 window.open<a target="_blank"> 打开的新标签页,会复制一份原标签页的 sessionStorage(注意是复制,之后各自独立)。

Q3:Cookie 的 SameSite=LaxSameSite=Strict 有什么区别?

点击查看答案

Strict:完全禁止第三方网站携带该 Cookie。如果用户在 A 站点击链接跳到 B 站,B 站的 Cookie 不会被带上,用户会发现自己”没登录”。Lax:允许部分安全的跨站请求(如顶级导航的 GET 请求)携带 Cookie,但阻止 POST 表单、iframe、AJAX 等方式的跨站携带。Lax 是现代浏览器的默认值,在安全性和用户体验之间取得了平衡。


一、Cookie:最古老也最复杂的存储方案

Cookie 是浏览器最早支持的存储机制,诞生于 1994 年。它最初的设计目的是让无状态的 HTTP 协议能够记住用户信息——服务器通过响应头 Set-Cookie 告诉浏览器”记住这个值”,浏览器在后续请求中通过 Cookie 请求头自动把它带回来。

一个完整的 Cookie 由名值对 + 一组属性组成。面试中经常会让你列举并解释这些属性:

Set-Cookie: sessionId=abc123; Domain=.example.com; Path=/; Expires=Thu, 01 Jan 2026 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax

Domain

  • 指定 Cookie 对哪些域名有效
  • 如果设置为 .example.com,那么 www.example.comapi.example.com 等子域都能拿到这个 Cookie
  • 如果不设置,默认是当前页面的域名(不含子域)
  • 注意:不能设置为与当前域完全无关的域名,比如 a.com 的页面不能给 b.com 设置 Cookie

Path

  • 限制 Cookie 只在指定路径及其子路径下有效
  • 比如 Path=/admin,那么 /admin/dashboard 可以拿到,但 /home 拿不到
  • 默认值是设置 Cookie 时的页面路径

Expires / Max-Age

  • Expires:指定一个绝对的过期时间(UTC 格式)
  • Max-Age:指定 Cookie 的有效时长(单位:秒),优先级高于 Expires
  • 如果两个都不设置,Cookie 就是会话级别的——浏览器关闭后就会被清除(和 sessionStorage 类似)
  • 设置 Max-Age=0 或者过去的 Expires 可以立即删除一个 Cookie

HttpOnly

  • 设置后,JavaScript 无法通过 document.cookie 读取或修改这个 Cookie
  • 它只会在 HTTP 请求中自动附带
  • 防御 XSS 的关键属性:即使攻击者注入了恶意脚本,也拿不到标记了 HttpOnly 的敏感 Cookie

Secure

  • 设置后,Cookie 只在 HTTPS 连接中才会被发送
  • 在 HTTP 页面中,浏览器不会发送带有 Secure 标记的 Cookie
  • 现代最佳实践:所有敏感 Cookie 都应该加上 Secure

SameSite

这是近年来最重要的 Cookie 属性,直接关系到 CSRF 防御:

  • Strict:完全阻止跨站请求携带 Cookie。最安全,但用户体验较差(从外站跳转过来会丢失登录态)
  • Lax现代浏览器默认值):允许顶级导航的 GET 请求携带 Cookie,阻止 POST / iframe / AJAX 等跨站携带
  • None:完全不限制跨站携带,但必须同时设置 Secure,否则会被浏览器拒绝
// SameSite 的典型设置
Set-Cookie: token=xxx; SameSite=Lax; Secure; HttpOnly
  • 大小限制:单个 Cookie 最大约 4KB(包括名称和值)
  • 数量限制:每个域名下通常限制 50 个左右的 Cookie(不同浏览器略有差异)
  • 性能影响:Cookie 会随每个 HTTP 请求自动发送到服务器。如果存了太多 Cookie,每个请求都会带上这些额外数据,增加网络开销
  • 操作不便document.cookie 的 API 设计非常原始,读取时返回一个拼接的字符串,写入时是追加而不是覆盖
// 读取:返回 "name1=value1; name2=value2" 这样的字符串
console.log(document.cookie);

// 写入:一次只能设一个
document.cookie = "username=Alice; Max-Age=3600; Path=/";

// 删除:把 Max-Age 设为 0
document.cookie = "username=; Max-Age=0; Path=/";
  • 身份认证:session ID、JWT token(配合 HttpOnly + Secure + SameSite)
  • 用户偏好:语言设置、主题选择(如果需要服务端读取的话)
  • 追踪与分析:第三方 Cookie 用于广告追踪(正在被各大浏览器逐步限制)

二、Web Storage:localStorage 与 sessionStorage

HTML5 引入了 Web Storage API,提供了比 Cookie 更简洁、容量更大的客户端存储方案。它包含两个兄弟:localStoragesessionStorage

2.1 API 一览

两者的 API 完全一致,非常简洁:

// 写入
localStorage.setItem('theme', 'dark');

// 读取
const theme = localStorage.getItem('theme'); // 'dark'

// 删除单个
localStorage.removeItem('theme');

// 清空所有
localStorage.clear();

// 获取长度
console.log(localStorage.length);

// 按索引获取 key
console.log(localStorage.key(0));

注意:Web Storage 只能存储字符串。 如果你想存对象或数组,需要手动序列化:

// 存对象
localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 25 }));

// 取对象
const user = JSON.parse(localStorage.getItem('user'));

这里有个经典的坑:如果 getItem 返回 null(key 不存在),JSON.parse(null) 的结果是 null,不会报错。但如果存的是一个非法 JSON 字符串,JSON.parse 就会抛异常。所以在取数据时最好加上 try-catch。

2.2 localStorage vs sessionStorage:关键差异

特性localStoragesessionStorage
生命周期永久,除非手动删除或清除浏览器数据会话级别,标签页关闭后清除
跨标签页共享同源下共享不共享(每个标签页独立)
存储大小5-10MB(因浏览器而异)5-10MB
随请求发送不会不会
刷新页面数据保留数据保留

几个容易混淆的点:

  1. sessionStorage 刷新页面不会丢失——很多人以为”会话级别”意味着刷新就没了,其实不是。只有关闭标签页才会清除。
  2. sessionStorage 通过链接打开新标签页时会复制——通过 window.open<a target="_blank"> 打开的新标签页,会拿到原标签页 sessionStorage 的一份副本,之后各自独立修改互不影响。
  3. localStorage 有 storage 事件——当一个标签页修改了 localStorage,同源的其他标签页会收到 storage 事件,可以用来做简单的跨标签页通信。
// 标签页 A 写入
localStorage.setItem('message', 'hello from A');

// 标签页 B 监听
window.addEventListener('storage', (e) => {
  console.log('key:', e.key);         // 'message'
  console.log('oldValue:', e.oldValue); // null 或之前的值
  console.log('newValue:', e.newValue); // 'hello from A'
  console.log('url:', e.url);          // 触发变更的页面 URL
});

注意:storage 事件只在其他标签页触发,当前标签页不会收到自己的修改事件。

2.3 Web Storage 的局限

  • 只能存字符串:需要手动 JSON 序列化/反序列化,不支持二进制数据
  • 同步 API:所有操作都是同步的,大量数据读写可能阻塞主线程
  • 无过期机制:localStorage 没有内置的过期时间,需要自己实现
// 一个简单的带过期时间的 localStorage 封装
const storage = {
  set(key, value, ttl) {
    const item = {
      value,
      expire: ttl ? Date.now() + ttl : null
    };
    localStorage.setItem(key, JSON.stringify(item));
  },
  get(key) {
    const raw = localStorage.getItem(key);
    if (!raw) return null;
    const item = JSON.parse(raw);
    if (item.expire && Date.now() > item.expire) {
      localStorage.removeItem(key);
      return null;
    }
    return item.value;
  }
};

三、IndexedDB:浏览器中的”数据库”

Cookie 和 Web Storage 都只适合存储少量的简单数据。当你需要在浏览器端存储大量结构化数据——比如离线缓存的文章列表、聊天记录、图片资源——就需要 IndexedDB。

3.1 IndexedDB 的核心特性

  • 大容量:理论上没有硬性上限,通常可以使用磁盘空间的很大一部分(Chrome 默认可用空间约为磁盘的 80%,每个源至少 10GB)
  • 异步 API:所有操作都是异步的,不会阻塞主线程
  • 支持事务:读写操作在事务中完成,保证数据一致性
  • 存储类型丰富:可以存 JavaScript 对象、二进制数据(Blob、ArrayBuffer)、文件等
  • 支持索引:可以对对象的属性建立索引,支持范围查询
  • 同源限制:和 Web Storage 一样,受同源策略约束

3.2 基本使用

IndexedDB 的原生 API 比较繁琐,是基于事件回调的风格:

// 1. 打开(或创建)数据库
const request = indexedDB.open('myApp', 1);

// 2. 数据库版本升级时创建对象仓库(表)
request.onupgradeneeded = (e) => {
  const db = e.target.result;
  if (!db.objectStoreNames.contains('users')) {
    const store = db.createObjectStore('users', { keyPath: 'id' });
    store.createIndex('nameIndex', 'name', { unique: false });
  }
};

// 3. 打开成功后进行读写
request.onsuccess = (e) => {
  const db = e.target.result;

  // 写入
  const tx = db.transaction('users', 'readwrite');
  const store = tx.objectStore('users');
  store.put({ id: 1, name: 'Alice', age: 25 });
  store.put({ id: 2, name: 'Bob', age: 30 });

  // 读取
  const getTx = db.transaction('users', 'readonly');
  const getStore = getTx.objectStore('users');
  const getReq = getStore.get(1);
  getReq.onsuccess = () => {
    console.log(getReq.result); // { id: 1, name: 'Alice', age: 25 }
  };
};

在实际项目中,我们通常会用封装好的库来操作 IndexedDB,比如 Dexie.jsidb(由 Jake Archibald 开发的 Promise 封装):

// 使用 idb 库(Promise 风格)
import { openDB } from 'idb';

const db = await openDB('myApp', 1, {
  upgrade(db) {
    db.createObjectStore('users', { keyPath: 'id' });
  }
});

// 写入
await db.put('users', { id: 1, name: 'Alice' });

// 读取
const user = await db.get('users', 1);

3.3 IndexedDB 的适用场景

  • 离线应用:PWA 中缓存大量数据,让应用在断网时仍可使用
  • 大数据集:需要在客户端存储和查询大量记录(比如邮件客户端、笔记应用)
  • 二进制文件:存储图片、音频等 Blob 数据
  • 复杂查询:需要按索引范围检索数据的场景

四、存储方案对比与选型

这是面试中的经典总结题。把所有方案放在一起对比:

特性CookielocalStoragesessionStorageIndexedDB
容量~4KB~5-10MB~5-10MB数百 MB 以上
生命周期可设过期时间永久标签页关闭清除永久
随请求发送
API 风格原始字符串同步、简洁同步、简洁异步、事务
数据类型字符串字符串字符串结构化数据、二进制
跨标签页共享是(同域)是(同源)是(同源)
可被 JS 访问看 HttpOnly
服务端可读取

选型建议

需要服务端读取?→ Cookie

比如身份认证的 token、session ID。Cookie 的独特之处在于它会自动随请求发送给服务器,这是其他方案做不到的。

简单的客户端状态?→ localStorage / sessionStorage

比如用户偏好(主题、语言)、表单草稿、页面状态。数据量小、结构简单、不需要服务端参与。需要持久化就用 localStorage,只在当前会话有效就用 sessionStorage。

大量数据 / 离线场景?→ IndexedDB

数据量大、需要查询、需要存二进制、或者在 PWA 中做离线支持,IndexedDB 是唯一合适的选择。

需要注意的安全原则:

  • 不要在 localStorage/sessionStorage 中存敏感信息(token、密码等),因为 XSS 攻击可以轻松读取
  • 敏感信息如果必须存在客户端,放 Cookie 里并设置 HttpOnly + Secure + SameSite
  • IndexedDB 中的数据同样可以被 JS 访问,也不适合存敏感信息

五、补充:几个常见面试追问

这是一个常见的混淆点。Cookie 是浏览器端的存储机制,Session 是服务端的会话机制。 它们的关系是:Session 通常依赖 Cookie 来传递 session ID——服务器在 Set-Cookie 里把 session ID 发给浏览器,浏览器后续请求通过 Cookie 头把它带回来,服务器根据这个 ID 找到对应的 Session 数据。

但 Session 不是必须依赖 Cookie——也可以通过 URL 参数、自定义请求头等方式传递 session ID。只是 Cookie 最方便,所以成了主流方案。

第三方 Cookie(由非当前页面域名设置的 Cookie)长期以来被用于跨站追踪(比如广告平台追踪你在不同网站上的浏览行为)。由于隐私问题,各大浏览器正在逐步限制或淘汰第三方 Cookie:

  • Safari 和 Firefox 已经默认阻止第三方 Cookie
  • Chrome 在推进 Privacy Sandbox 方案作为替代

5.3 Storage 配额查询

现代浏览器提供了 Storage API 来查询存储配额:

if (navigator.storage && navigator.storage.estimate) {
  const estimate = await navigator.storage.estimate();
  console.log(`已用: ${estimate.usage} bytes`);
  console.log(`配额: ${estimate.quota} bytes`);
  console.log(`使用率: ${(estimate.usage / estimate.quota * 100).toFixed(2)}%`);
}

常见误区

误区一:“localStorage 是永久的,所以数据绝对不会丢”

虽然 localStorage 不会自动过期,但它可以被清除:用户手动清除浏览器数据、浏览器在存储空间不足时可能会清理(尤其是移动端 Safari 的”7 天限制”策略)、隐私模式下关闭窗口后数据也会被清除。所以不要把 localStorage 当作可靠的持久化方案,重要数据还是应该同步到服务端。

误区二:“Cookie 的大小限制是 4KB,所以能存很多条”

4KB 是单个 Cookie 的大小限制(包含名称、值和属性),而且每个域名下的 Cookie 总数也有限制(通常约 50 个)。更关键的是,所有 Cookie 都会随请求发送,几十个 Cookie 加起来可能有几 KB 的额外开销——每个请求都要带上这些数据,这对性能的影响不可忽视。

误区三:“sessionStorage 和 Session 是一回事”

sessionStorage 是浏览器端的存储 API,数据存在客户端;Session 是服务端的会话机制,数据存在服务器。两者名字相似,但概念完全不同。sessionStorage 的”session”指的是浏览器标签页的生命周期,而 Session 的”session”指的是用户和服务器之间的会话

误区四:“IndexedDB 太复杂了,不如都用 localStorage”

IndexedDB 的原生 API 确实繁琐,但使用 idbDexie.js 这类封装库后,体验和 localStorage 差不多。而且 IndexedDB 的异步特性意味着它不会阻塞主线程,在存储大量数据时反而比 localStorage 的同步读写更不容易卡顿。选型应该基于实际需求(数据量、数据类型、是否需要查询),而不是 API 的复杂度。


小结

本章我们从浏览器最古老的 Cookie 讲到现代的 IndexedDB,完整梳理了浏览器端的存储方案。

核心要点

  1. Cookie 是唯一会随 HTTP 请求自动发送的存储方案,适合身份认证,但容量小(4KB)、API 原始
  2. Cookie 的安全属性(HttpOnly、Secure、SameSite)是前端安全的重要防线
  3. localStorage 持久化、同源共享、约 5-10MB,适合简单的客户端状态
  4. sessionStorage 标签页级别、关闭即清、刷新保留,适合临时状态
  5. IndexedDB 容量大、异步、支持结构化数据和索引,适合离线应用和大数据集
  6. 不要在 localStorage 中存敏感信息,XSS 可以轻松读取
  7. 选型核心逻辑:需要服务端读取 → Cookie;简单客户端状态 → Web Storage;大量数据 → IndexedDB

本章思维导图

浏览器 Storage
  • Cookie
    • 核心属性
      • Domain / Path:作用范围
      • Expires / Max-Age:生命周期
      • HttpOnly:禁止 JS 访问
      • Secure:仅 HTTPS
      • SameSite:跨站策略(Strict / Lax / None)
    • 限制:4KB / 约 50 个 / 随请求发送
    • 场景:身份认证、服务端需要读取的状态
  • Web Storage
    • localStorage
      • 永久存储、同源共享
      • 支持 storage 事件(跨标签页通信)
    • sessionStorage
      • 标签页级别、关闭清除、刷新保留
      • 新标签页打开时复制(独立副本)
    • 共同特点
      • ~5-10MB、只能存字符串、同步 API
      • 不随请求发送
  • IndexedDB
    • 大容量、异步、事务
    • 支持对象、二进制、索引查询
    • 适合离线应用、大数据集
  • 选型决策
    • 需要服务端读取 → Cookie
    • 简单客户端状态 → Web Storage
    • 大量/结构化数据 → IndexedDB
  • 安全原则
    • 敏感信息不放 Web Storage
    • Cookie 加 HttpOnly + Secure + SameSite

练习挑战

第一题 ⭐(基础):判断对错

下面四个说法,哪些是正确的?

  1. localStorage 的数据在浏览器关闭后会被清除
  2. sessionStorage 在页面刷新后数据仍然保留
  3. 设置了 HttpOnly 的 Cookie 无法通过 document.cookie 读取
  4. CookieSameSite 默认值是 None
点击查看答案
  • ❌ 第 1 条错误:localStorage 是永久存储,浏览器关闭后数据仍然保留,除非手动删除
  • ✅ 第 2 条正确:sessionStorage 在刷新页面后数据保留,只有关闭标签页才会清除
  • ✅ 第 3 条正确:HttpOnly 的作用就是阻止 JavaScript 访问
  • ❌ 第 4 条错误:现代浏览器(Chrome 80+)的 SameSite 默认值是 Lax,不是 None

第二题 ⭐⭐(进阶):实现一个带过期时间的 localStorage 封装

要求:

  1. set(key, value, ttl) —— ttl 为过期时间(毫秒),可选
  2. get(key) —— 如果已过期则返回 null 并自动清除
  3. 存取对象时不需要调用方手动 JSON.stringify/JSON.parse
点击查看答案
class ExpiringStorage {
  set(key, value, ttl) {
    const item = {
      value,
      expire: ttl ? Date.now() + ttl : null
    };
    try {
      localStorage.setItem(key, JSON.stringify(item));
    } catch (e) {
      console.warn('localStorage 写入失败:', e);
    }
  }

  get(key) {
    const raw = localStorage.getItem(key);
    if (raw === null) return null;

    try {
      const item = JSON.parse(raw);
      // 兼容不是本封装写入的数据
      if (!item || typeof item !== 'object' || !('value' in item)) {
        return item;
      }
      if (item.expire && Date.now() > item.expire) {
        localStorage.removeItem(key);
        return null;
      }
      return item.value;
    } catch {
      return raw; // 如果 parse 失败,返回原始字符串
    }
  }

  remove(key) {
    localStorage.removeItem(key);
  }
}

// 使用
const storage = new ExpiringStorage();
storage.set('token', 'abc123', 60 * 1000); // 1 分钟过期
storage.set('theme', 'dark'); // 永不过期

console.log(storage.get('token')); // 'abc123'(1 分钟内)
console.log(storage.get('theme')); // 'dark'

关键点:将值和过期时间一起序列化后存入 localStorage,取出时先检查是否过期。注意 try-catch 处理 JSON 解析异常和存储空间满的情况。

第三题 ⭐⭐⭐(综合):利用 localStorage 的 storage 事件实现简单的跨标签页消息总线

要求:

  1. 实现 send(channel, data)on(channel, callback) 方法
  2. 消息只在其他标签页被接收(和 BroadcastChannel API 类似)
  3. 发送后自动清理 localStorage 中的临时数据
点击查看答案
class CrossTabBus {
  constructor(prefix = '__cross_tab_bus__') {
    this.prefix = prefix;
    this.listeners = {};

    window.addEventListener('storage', (e) => {
      if (!e.key || !e.key.startsWith(this.prefix)) return;
      if (e.newValue === null) return; // 删除事件,忽略

      const channel = e.key.slice(this.prefix.length);
      const callbacks = this.listeners[channel];
      if (!callbacks || callbacks.length === 0) return;

      try {
        const data = JSON.parse(e.newValue);
        callbacks.forEach(cb => cb(data.payload));
      } catch {
        // 忽略解析失败
      }
    });
  }

  send(channel, data) {
    const key = this.prefix + channel;
    const message = JSON.stringify({
      payload: data,
      timestamp: Date.now()
    });
    localStorage.setItem(key, message);
    // 发送后立即清理,避免污染 localStorage
    // 用 setTimeout 确保 storage 事件已经触发
    setTimeout(() => localStorage.removeItem(key), 100);
  }

  on(channel, callback) {
    if (!this.listeners[channel]) {
      this.listeners[channel] = [];
    }
    this.listeners[channel].push(callback);

    // 返回取消订阅函数
    return () => {
      this.listeners[channel] = this.listeners[channel].filter(cb => cb !== callback);
    };
  }
}

// 使用
const bus = new CrossTabBus();

// 标签页 A:监听
const unsubscribe = bus.on('notification', (data) => {
  console.log('收到消息:', data);
});

// 标签页 B:发送
bus.send('notification', { title: '新消息', content: 'Hello!' });

核心原理:利用 localStorage 的 storage 事件天然支持跨标签页通知的特性。写入 localStorage 时,同源的其他标签页会收到事件。发送后用 setTimeout 清理临时数据,避免 localStorage 被污染。这种方案比 BroadcastChannel 的兼容性更好(IE 也支持 storage 事件),但只能用于同源场景。


自我检测

读完本章后,对照下面的清单检验一下自己的掌握程度:

  • 能列举 Cookie 的六个核心属性(Domain、Path、Expires/Max-Age、HttpOnly、Secure、SameSite)并解释每个的作用
  • 能说清楚 SameSite 的三个值(Strict、Lax、None)的区别,以及为什么 Lax 是默认值
  • 能区分 localStorage 和 sessionStorage 的生命周期与作用域,特别是 sessionStorage 刷新不丢失、新标签页复制等细节
  • 能说出 Web Storage 的三个主要局限(只能存字符串、同步 API、无过期机制)
  • 能简要描述 IndexedDB 的核心特性(异步、事务、支持索引和二进制数据、大容量)
  • 能在给定场景下选择合适的存储方案并说出理由
  • 能解释为什么敏感信息不应该放在 localStorage 中,以及 Cookie 的 HttpOnly 属性如何防御 XSS
  • 能利用 localStorage 的 storage 事件做简单的跨标签页通信

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90