# localStorage 与 sessionStorage

> localStorage 与 sessionStorage 背后的同步 Storage 接口：方法语法、Firefox 的 5 MiB 配额、Chromium 的配额报错文本，以及 storage 事件。

`window.localStorage` 与 `window.sessionStorage` 是同一源下两个只存字符串键值的 `Storage` 对象：`localStorage` 在关闭标签页和重启浏览器后仍在，`sessionStorage` 属于一个顶级浏览上下文，该标签页或窗口关闭即丢弃。两者都是同步的、只在主线程可用，worker 和 Service Worker 里没有，因此只适合存放小型标志和偏好，而不是应用数据。

## 语法

```js
localStorage.setItem(key, value)
localStorage.getItem(key)
localStorage.removeItem(key)
localStorage.clear()
localStorage.key(index)
localStorage.length
```

`sessionStorage` 的接口完全相同。值以字符串存储：`setItem('n', 1)` 存入 `"1"`，对象不先序列化就会存成 `"[object Object]"`。Chrome 4、Firefox 3.5、Safari 4 起支持（BCD `api.Window.localStorage`）；没有安全上下文要求，但同一主机的两种 scheme 是不同的源，互不共享存储。

## 参数

| 方法 | 参数 | 类型 | 含义 |
|---|---|---|---|
| `setItem` | `key`、`value` | string、string | 创建或覆盖一条。非字符串参数经 `String()` 转换。 |
| `getItem` | `key` | string | 返回值，键不存在时返回 `null`。空字符串是合法的存储值，返回 `""` 而不是 `null`。 |
| `removeItem` | `key` | string | 删除一条。删除不存在的键不报错。 |
| `clear` | 无 | | 删除该源在该存储区内的全部条目。 |
| `key` | `index` | unsigned long | 返回该位置的键，越界返回 `null`。顺序由实现决定，写入后会变。 |

每次调用都在主线程同步执行；Firefox 和 Chromium 用按源划分的磁盘数据库支撑它，循环写入大字符串会在整个过程中阻塞渲染。

## 可用性

Web Storage 存在于所有引擎（Chrome 4、Firefox 3.5、Safari 4；BCD `api.Window.localStorage`），包括 Android WebView 和 iOS 的 WebView，是唯一没有支持缺口的客户端存储。缺口在于上下文而非浏览器：worker 和 Service Worker 不暴露这两个对象，不透明源访问即抛出，每源上限也很小。

## 异常

| 异常 | 位置 | 触发条件 |
|---|---|---|
| `QuotaExceededError` `DOMException` | `setItem()` | 写入将超出存储区上限。Firefox 把 `localStorage` 限制为每源 5 MiB（[StaticPrefList.yaml](https://github.com/mozilla-firefox/firefox/blob/main/modules/libpref/init/StaticPrefList.yaml) 中的 `dom.storage.default_quota`，5 × 1024 KiB，github.com）。Chromium 采用类似的每源上限，报错文本为 `Setting the value of '<key>' exceeded the quota.`（[storage_area.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/storage/storage_area.cc)，chromium.googlesource.com）。 |
| `SecurityError` `DOMException` | 读取 `window.localStorage` | 源是不透明源（没有 `allow-same-origin` 的沙箱 `<iframe>`、`data:` URL），或用户已为该站点拦截存储。getter 本身抛出，所以 `'localStorage' in window` 也不够，首次访问要包在 `try` 里。 |

在 Firefox 和 Chromium 中隐私窗口不是异常路径：写入成功，数据在隐私会话结束时丢弃。Safari 11 之前则暴露一个配额为零的 `localStorage`，第一次 `setItem()` 就抛出 `QuotaExceededError`；「写一条再删一条」的探测同时覆盖这两种行为。

## 示例

示例使用 `localStorage`；换成 `sessionStorage` 即为按标签页的状态，代码其余部分不变。

### 保存偏好并在另一个标签页响应

`storage` 事件在共享该存储区的其他同源文档上触发，不在写入的那个文档上触发。用它让两个打开的标签页保持同步，而不必轮询。

```js
function setTheme(theme) {
  localStorage.setItem('theme', theme);
  applyTheme(theme); // 本标签页不会收到 storage 事件
}

window.addEventListener('storage', (event) => {
  if (event.storageArea === localStorage && event.key === 'theme') {
    applyTheme(event.newValue ?? 'light'); // removeItem 之后 newValue 为 null
  }
});
```

`event.storageArea` 区分 `localStorage` 和 `sessionStorage` 的写入，`clear()` 之后 `event.key` 为 `null`。

### 带体积守卫地序列化结构化数据

上限按序列化字符串的字符数计算，写入前先检查长度，并把 `QuotaExceededError` 当作「把数据搬到 IndexedDB」的信号而不是重试的信号。

```js
function saveSettings(settings) {
  const json = JSON.stringify(settings);
  try {
    localStorage.setItem('settings', json);
    return true;
  } catch (err) {
    if (err.name === 'QuotaExceededError') return false; // 太大：改存 IndexedDB
    throw err;
  }
}
```

超过几百 KB 的数据应放进 [IndexedDB](/zh/reference/storage/indexeddb/)，它是异步的，并共享该源大得多的配额。

### 检测可用的存储区

探测写入并删除一个键。对不透明源（getter 抛出 `SecurityError`）、被拦截的站点和旧版 Safari 的零配额隐私模式都返回 `false`。

```js
function storageAvailable(type) {
  if (!(type in window)) {
    return false; // 没有 Web Storage：本次会话把状态放在内存里
  }
  try {
    const area = window[type];
    const probe = '__probe__';
    area.setItem(probe, probe);
    area.removeItem(probe);
    return true;
  } catch {
    return false; // 访问被拒或零配额：退回内存
  }
}
```

启动时用 `'localStorage'` 调用一次并记住结果；它检测的条件在页面打开期间不会变化。

:::observed
在 Chromium 中，配额失败表现为 `Uncaught DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'draft' exceeded the quota.`，其中 `draft` 是正在写入的键；该文本在 [storage_area.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/storage/storage_area.cc)（chromium.googlesource.com）的 `StorageArea::setItem` 中拼接。Chrome DevTools 的 Application > Storage > Local storage 列出该源，并在键值表中显示失败写入之后并不存在的那个超大值。
:::

## 另请参阅

- [HTML Standard: Web storage](https://html.spec.whatwg.org/multipage/webstorage.html)（whatwg.org）
- [Using the Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API)（developer.mozilla.org）
- [IndexedDB](/zh/reference/storage/indexeddb/)
- [StorageManager.estimate()](/zh/reference/storage/quota-estimate/)
- [Clear-Site-Data 响应头](/zh/reference/storage/clearing-data/)