Skip to content

Storage API 详解

localStoragesessionStorage 都实现了统一的 Storage 接口。本节将深入讲解 Storage 接口的所有方法和属性,包括 StorageEvent 跨标签页通信、key() 遍历、JSON 序列化最佳实践以及存储操作的异常处理。

前置知识

阅读本节前,建议先了解:sessionStorage

基础概念

Storage 接口定义

Storage 接口定义了 Web Storage 的标准 API,localStoragesessionStorage 都是 Storage 接口的实例。

属性/方法返回值说明
lengthnumber存储的键值对数量
key(index)`stringnull`
getItem(key)`stringnull`
setItem(key, value)void存储键值对
removeItem(key)void删除指定键
clear()void清空所有数据

语法与 API

length 属性

javascript
// 获取存储的键值对数量
const count = localStorage.length;
console.log(`存储了 ${count} 条数据`);

// 监测存储变化
function checkStorageEmpty(storage) {
  if (storage.length === 0) {
    console.log('存储为空');
    return true;
  }
  return false;
}

key(index) 方法

javascript
// key() 返回指定索引位置的键名
const firstKey = localStorage.key(0);    // 第一个键
const secondKey = localStorage.key(1);   // 第二个键
const lastKey = localStorage.key(localStorage.length - 1); // 最后一个键

// 索引超出范围返回 null
const invalid = localStorage.key(999); // null

// 注意:键的顺序由浏览器决定,不保证插入顺序

完整 CRUD 操作

javascript
// ========== Create - 创建数据 ==========
localStorage.setItem('config:theme', 'dark');
localStorage.setItem('config:lang', 'zh-CN');
localStorage.setItem('config:fontSize', '16');

// ========== Read - 读取数据 ==========
const theme = localStorage.getItem('config:theme');    // 'dark'
const allLang = localStorage.getItem('config:lang');   // 'zh-CN'
const notExist = localStorage.getItem('config:color'); // null

// ========== Update - 更新数据 ==========
// setItem 对已存在的键会覆盖
localStorage.setItem('config:theme', 'light');
localStorage.getItem('config:theme'); // 'light'(已更新)

// ========== Delete - 删除数据 ==========
localStorage.removeItem('config:fontSize');
localStorage.getItem('config:fontSize'); // null(已删除)

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

详细说明

StorageEvent - 跨标签页通信

StorageEvent 是 Web Storage 最重要的特性之一,当同源的另一个标签页(或 iframe)修改了 localStorage 时,会触发该事件。这提供了一种轻量级的跨标签页通信方式。

javascript
// 监听 storage 事件
window.addEventListener('storage', (e) => {
  console.log('存储变化事件:', {
    key: e.key,             // 被修改的键名
    oldValue: e.oldValue,   // 修改前的值
    newValue: e.newValue,   // 修改后的值
    url: e.url,             // 触发事件的页面 URL
    storageArea: e.storageArea // localStorage 或 sessionStorage
  });

  // 根据键名执行不同操作
  switch (e.key) {
    case 'user:logout':
      handleLogout();
      break;
    case 'theme:changed':
      document.body.className = e.newValue;
      break;
    case 'notification:new':
      showNotification(e.newValue);
      break;
  }
});

StorageEvent 属性

属性类型说明
key`stringnull`
oldValue`stringnull`
newValue`stringnull`
urlstring触发变化的页面 URL
storageAreaStorage被修改的 Storage 对象

StorageEvent 的重要特性

javascript
// 1. 不会在当前页面触发(只触发在其他标签页)
// 在标签页 A 中
localStorage.setItem('test', '1');
// 标签页 A 的 storage 事件监听器不会触发

// 在标签页 B 中(同源)
// storage 事件监听器会触发

// 2. sessionStorage 不触发 storage 事件
// sessionStorage 的变化不会触发 storage 事件
// 只有 localStorage 会

// 3. 只在同源页面间触发
// 不同域名、不同协议、不同端口不会触发

使用 StorageEvent 实现标签页同步

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>StorageEvent 标签页同步</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; }
    .panel {
      max-width: 600px;
      margin: 20px auto;
      padding: 20px;
      background: white;
      border-radius: 12px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .event-log {
      padding: 16px;
      background: #1a1a2e;
      color: #0f0;
      font-family: monospace;
      font-size: 13px;
      border-radius: 8px;
      max-height: 300px;
      overflow-y: auto;
    }
    .controls { display: flex; gap: 12px; margin-top: 16px; }
    .controls input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; flex: 1; }
    .controls button {
      padding: 8px 20px;
      border: none;
      border-radius: 6px;
      background: #1a73e8;
      color: white;
      cursor: pointer;
    }
    .badge {
      display: inline-block;
      padding: 2px 10px;
      background: #e3f2fd;
      color: #1565c0;
      border-radius: 12px;
      font-size: 13px;
    }
  </style>
</head>
<body>
  <h2>StorageEvent 跨标签页通信</h2>
  <p><span class="badge">请打开多个标签页查看效果</span></p>

  <div class="panel">
    <h3>操作面板</h3>
    <div class="controls">
      <input type="text" id="inputKey" placeholder="键名" value="message">
      <input type="text" id="inputValue" placeholder="值">
      <button onclick="sendData()">发送</button>
    </div>

    <div class="controls" style="margin-top:8px;">
      <button onclick="broadcastNotification()" style="flex:1;background:#e91e63;">发送通知到所有标签页</button>
    </div>
  </div>

  <div class="panel">
    <h3>事件日志</h3>
    <div class="event-log" id="eventLog">
      等待来自其他标签页的 StorageEvent...
    </div>
  </div>

  <script>
    // 发送数据
    function sendData() {
      const key = document.getElementById('inputKey').value;
      const value = document.getElementById('inputValue').value;
      if (key && value) {
        localStorage.setItem(key, value);
        addLog('本地发送', `${key} = ${value}`);
      }
    }

    // 广播通知
    function broadcastNotification() {
      const message = `通知来自标签页 ${Date.now()}`;
      localStorage.setItem('broadcast:notification', message);
      addLog('广播', message);
    }

    // 监听 storage 事件
    window.addEventListener('storage', (e) => {
      addLog('StorageEvent', `
        key: ${e.key}
        oldValue: ${e.oldValue}
        newValue: ${e.newValue}
        url: ${e.url}
      `);
    });

    // 添加日志
    function addLog(type, content) {
      const log = document.getElementById('eventLog');
      const time = new Date().toLocaleTimeString();
      log.innerHTML += `<p><span style="color:#ff0">[${time}]</span> <strong>${type}:</strong> ${content}</p>`;
      log.scrollTop = log.scrollHeight;
    }
  </script>
</body>
</html>

遍历 Storage 数据

javascript
// 方式一:使用 key() 和 length 遍历
function iterateStorage(storage) {
  for (let i = 0; i < storage.length; i++) {
    const key = storage.key(i);
    const value = storage.getItem(key);
    console.log(`${key}: ${value}`);
  }
}

// 方式二:使用 Object.keys()(不推荐,包含原型链属性)
// Object.keys(localStorage).forEach(key => { ... });

// 方式三:推荐使用 for...of + entries()
function iterateStorageModern(storage) {
  for (let i = 0; i < storage.length; i++) {
    const key = storage.key(i);
    const value = storage.getItem(key);
    yield { key, value };
  }
}

// 使用示例
for (const item of iterateStorageModern(localStorage)) {
  console.log(item.key, '=', item.value);
}

// 方式四:按前缀筛选
function getByPrefix(storage, prefix) {
  const result = {};
  for (let i = 0; i < storage.length; i++) {
    const key = storage.key(i);
    if (key.startsWith(prefix)) {
      result[key] = storage.getItem(key);
    }
  }
  return result;
}

// 获取所有以 'config:' 开头的配置
const configs = getByPrefix(localStorage, 'config:');
console.log(configs);

JSON 序列化与反序列化

javascript
// 安全的 JSON 存储工具
const StorageHelper = {
  // 存储 JSON
  setJSON(storage, key, data) {
    try {
      storage.setItem(key, JSON.stringify(data));
      return true;
    } catch (e) {
      console.error('存储 JSON 失败:', e);
      return false;
    }
  },

  // 读取 JSON
  getJSON(storage, key, defaultValue = null) {
    try {
      const raw = storage.getItem(key);
      return raw !== null ? JSON.parse(raw) : defaultValue;
    } catch (e) {
      console.error('解析 JSON 失败:', e);
      return defaultValue;
    }
  },

  // 删除并返回 JSON
  popJSON(storage, key, defaultValue = null) {
    const data = this.getJSON(storage, key, defaultValue);
    storage.removeItem(key);
    return data;
  },

  // 更新嵌套对象的某个属性
  updateNested(storage, key, path, value) {
    const data = this.getJSON(storage, key, {});
    const keys = path.split('.');
    let target = data;
    for (let i = 0; i < keys.length - 1; i++) {
      if (!target[keys[i]]) target[keys[i]] = {};
      target = target[keys[i]];
    }
    target[keys[keys.length - 1]] = value;
    return this.setJSON(storage, key, data);
  }
};

// 使用示例
StorageHelper.setJSON(localStorage, 'user', { name: '张三', settings: { theme: 'dark' } });
StorageHelper.updateNested(localStorage, 'user', 'settings.theme', 'light');
const user = StorageHelper.getJSON(localStorage, 'user');
console.log(user.settings.theme); // 'light'

实战示例

Storage 封装工具类

javascript
/**
 * Web Storage 封装工具类
 * 支持命名空间、过期时间、类型转换
 */
class WebStorage {
  constructor(storage, namespace = '') {
    this.storage = storage;
    this.namespace = namespace;
  }

  // 生成带命名空间的键
  _key(key) {
    return this.namespace ? `${this.namespace}:${key}` : key;
  }

  // 存储数据
  set(key, value, ttl) {
    const data = {
      value: value,
      type: typeof value
    };

    // 设置过期时间
    if (ttl) {
      data.expiry = Date.now() + ttl * 1000;
    }

    try {
      this.storage.setItem(this._key(key), JSON.stringify(data));
      return true;
    } catch (e) {
      console.error('存储失败:', e);
      return false;
    }
  }

  // 读取数据
  get(key, defaultValue = null) {
    try {
      const raw = this.storage.getItem(this._key(key));
      if (raw === null) return defaultValue;

      const data = JSON.parse(raw);

      // 检查过期
      if (data.expiry && Date.now() > data.expiry) {
        this.remove(key);
        return defaultValue;
      }

      return data.value;
    } catch (e) {
      console.error('读取失败:', e);
      return defaultValue;
    }
  }

  // 删除数据
  remove(key) {
    this.storage.removeItem(this._key(key));
  }

  // 清空命名空间下的数据
  clear() {
    const prefix = this._key('');
    const keysToRemove = [];
    for (let i = 0; i < this.storage.length; i++) {
      if (this.storage.key(i).startsWith(prefix)) {
        keysToRemove.push(this.storage.key(i));
      }
    }
    keysToRemove.forEach(key => this.storage.removeItem(key));
  }

  // 判断键是否存在
  has(key) {
    return this.storage.getItem(this._key(key)) !== null;
  }

  // 获取命名空间下的所有数据
  getAll() {
    const result = {};
    const prefix = this._key('');
    for (let i = 0; i < this.storage.length; i++) {
      const key = this.storage.key(i);
      if (key.startsWith(prefix)) {
        const shortKey = key.slice(prefix.length);
        result[shortKey] = this.get(shortKey);
      }
    }
    return result;
  }
}

// 使用示例
const appStorage = new WebStorage(localStorage, 'myapp');

// 存储数据(带 1 小时过期时间)
appStorage.set('user', { name: '张三' }, 3600);
appStorage.set('token', 'abc123', 7200);

// 读取数据
const user = appStorage.get('user');
const token = appStorage.get('token', 'default-token');

// 获取命名空间下所有数据
const allData = appStorage.getAll();

// 清空命名空间
appStorage.clear();

注意事项

异常处理

javascript
// 1. 存储空间满
try {
  localStorage.setItem('big-data', largeData);
} catch (e) {
  if (e.name === 'QuotaExceededError') {
    console.error('存储空间已满');
    // 清理策略:删除最旧的数据
  }
}

// 2. 浏览器禁用存储
function isStorageAvailable(storage) {
  try {
    const test = '__test__';
    storage.setItem(test, '1');
    storage.removeItem(test);
    return true;
  } catch (e) {
    return false;
  }
}

// 3. JSON 解析失败
function safeParse(raw) {
  try {
    return JSON.parse(raw);
  } catch {
    return null;
  }
}

最佳实践

  1. 使用命名空间避免键名冲突
  2. 为缓存数据添加过期时间
  3. 封装统一的 Storage 工具类
  4. 利用 StorageEvent 实现标签页间同步
  5. 对所有操作进行异常捕获

下一节

继续学习:IndexedDB 入门

参考链接