Skip to content

localStorage

localStorage 是 HTML5 Web Storage API 提供的客户端持久化存储方案,允许网页在用户的浏览器中存储键值对数据。与 Cookie 不同,localStorage 的数据不会随每次 HTTP 请求发送到服务器,容量更大(通常 5MB),且没有过期时间,适合存储用户偏好设置、主题选择、本地缓存等数据。

前置知识

阅读本节前,建议先了解:拖放实战示例

基础概念

什么是 localStorage

localStoragewindow.localStorage 的简写,属于 Web Storage API 的一部分。它提供了一种在浏览器中存储字符串键值对的机制,数据以域名+端口为单位隔离,关闭浏览器后数据依然保留。

localStorage 特性概览

特性说明
存储容量通常 5MB(因浏览器而异)
数据持久性永久保存,除非手动清除
作用域同源(协议+域名+端口)
数据格式键和值均为字符串
安全性同源策略隔离,不发送到服务器
浏览器支持IE8+、所有现代浏览器

语法与 API

基本增删改查

javascript
// ========== 存储数据 ==========

// 方式一:setItem 方法
localStorage.setItem('username', '张三');
localStorage.setItem('age', '25');
localStorage.setItem('theme', 'dark');

// 方式二:直接赋值(简写)
localStorage.username = '张三';
localStorage['age'] = '25';

// ========== 读取数据 ==========

// 方式一:getItem 方法
const name = localStorage.getItem('username'); // '张三'
const age = localStorage.getItem('age');       // '25'

// 方式二:直接访问
const theme = localStorage.theme;    // 'dark'
const user = localStorage['username']; // '张三'

// 不存在的键返回 null
const notExist = localStorage.getItem('not-exist'); // null

// ========== 删除数据 ==========

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

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

// ========== 检查数据 ==========

// 判断键是否存在
if (localStorage.getItem('username') !== null) {
  console.log('用户名已存储');
}

// 使用 in 操作符(注意:包含原型链上的属性)
if ('username' in localStorage) {
  console.log('username 键存在');
}

// 检查是否有数据
if (localStorage.length === 0) {
  console.log('localStorage 为空');
}

存储复杂数据类型

localStorage 只能存储字符串,复杂数据需要序列化:

javascript
// ========== 存储对象 ==========
const user = {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  preferences: {
    theme: 'dark',
    language: 'zh-CN',
    fontSize: 14
  }
};

// 存储:JSON.stringify
localStorage.setItem('user', JSON.stringify(user));

// 读取:JSON.parse
const storedUser = JSON.parse(localStorage.getItem('user'));
console.log(storedUser.name); // '张三'
console.log(storedUser.preferences.theme); // 'dark'

// ========== 存储数组 ==========
const todos = [
  { id: 1, text: '学习 HTML5', done: true },
  { id: 2, text: '学习 CSS3', done: false },
  { id: 3, text: '学习 JavaScript', done: false }
];

localStorage.setItem('todos', JSON.stringify(todos));
const storedTodos = JSON.parse(localStorage.getItem('todos'));

// ========== 存储数字和布尔值 ==========
// 注意:数字和布尔值会变成字符串
localStorage.setItem('count', '42');
localStorage.setItem('flag', 'true');

// 读取时需要类型转换
const count = Number(localStorage.getItem('count'));    // 42
const flag = localStorage.getItem('flag') === 'true';   // true

详细说明

存储容量检测

javascript
// 检测 localStorage 可用空间
function getStorageUsage() {
  let total = 0;
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    const value = localStorage.getItem(key);
    // 每个字符占 2 字节(UTF-16)
    total += (key.length + value.length) * 2;
  }
  return {
    usedBytes: total,
    usedKB: (total / 1024).toFixed(2),
    usedMB: (total / (1024 * 1024)).toFixed(4)
  };
}

console.log('存储使用情况:', getStorageUsage());

// 检测剩余空间
function getRemainingSpace() {
  const testKey = '__storage_test__';
  let data = 'a';

  try {
    // 不断填充直到达到上限
    while (true) {
      localStorage.setItem(testKey, data);
      data += data;
    }
  } catch (e) {
    localStorage.removeItem(testKey);
    // 计算已用空间
    const used = getStorageUsage().usedBytes;
    return {
      total: data.length * 2,
      used: used,
      remaining: data.length * 2 - used
    };
  }
}

同源策略

localStorage 的数据按以下规则隔离:

维度说明示例
协议http 和 https 的数据不共享http://example.comhttps://example.com
域名不同域名的数据完全隔离example.comtest.example.com
端口不同端口的数据不共享:80:8080
javascript
// 在 https://www.example.com:443 下存储的数据
// 无法从以下地址访问:
// - http://www.example.com:443    (协议不同)
// - https://www.example.com:8080   (端口不同)
// - https://test.example.com:443  (域名不同)
// - https://example.com:443       (子域名不同)

localStorage 的安全注意事项

javascript
// 1. 不要存储敏感信息(明文可见)
// 错误做法
localStorage.setItem('password', '123456');
localStorage.setItem('token', 'eyJhbGciOiJIUzI1...');

// 2. 存储前进行基本的数据验证
function safeSet(key, value) {
  if (typeof key !== 'string' || typeof value !== 'string') {
    throw new Error('localStorage 只接受字符串');
  }
  if (key.length === 0) {
    throw new Error('键名不能为空');
  }
  try {
    localStorage.setItem(key, value);
  } catch (e) {
    if (e.name === 'QuotaExceededError') {
      console.warn('localStorage 存储空间已满');
      // 尝试清理旧数据
      cleanupOldData();
    }
  }
}

// 3. 安全读取 JSON 数据
function safeGetJSON(key) {
  try {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  } catch (e) {
    console.error(`解析 ${key} 的数据失败:`, e);
    localStorage.removeItem(key); // 清除损坏的数据
    return null;
  }
}

实战示例

用户偏好设置管理

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>localStorage 用户偏好设置</title>
  <style>
    body {
      font-family: -apple-system, sans-serif;
      padding: 40px;
      transition: background 0.3s, color 0.3s;
    }
    body.dark { background: #1a1a2e; color: #e0e0e0; }
    body.light { background: #f5f5f5; color: #333; }

    .settings-panel {
      max-width: 500px;
      margin: 0 auto;
      padding: 24px;
      background: rgba(255,255,255,0.1);
      border-radius: 12px;
      backdrop-filter: blur(10px);
    }

    .setting-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 16px 0;
      border-bottom: 1px solid rgba(255,255,255,0.1);
    }

    .setting-item:last-child { border-bottom: none; }

    label { font-weight: 600; }
    select, input[type="range"] { padding: 8px; border-radius: 6px; }

    .toggle {
      position: relative;
      width: 50px;
      height: 26px;
      background: #ccc;
      border-radius: 13px;
      cursor: pointer;
      transition: background 0.3s;
    }
    .toggle.active { background: #4CAF50; }
    .toggle::after {
      content: '';
      position: absolute;
      width: 22px;
      height: 22px;
      background: white;
      border-radius: 50%;
      top: 2px;
      left: 2px;
      transition: transform 0.3s;
    }
    .toggle.active::after { transform: translateX(24px); }

    .status-bar {
      margin-top: 20px;
      padding: 12px;
      font-size: 13px;
      background: rgba(0,0,0,0.1);
      border-radius: 8px;
      font-family: monospace;
    }
  </style>
</head>
<body>
  <h1>用户偏好设置</h1>

  <div class="settings-panel">
    <div class="setting-item">
      <label>深色模式</label>
      <div class="toggle" id="darkToggle" onclick="toggleDark()"></div>
    </div>

    <div class="setting-item">
      <label>语言</label>
      <select id="langSelect" onchange="saveSetting('language', this.value)">
        <option value="zh-CN">简体中文</option>
        <option value="en-US">English</option>
        <option value="ja-JP">日本語</option>
      </select>
    </div>

    <div class="setting-item">
      <label>字体大小</label>
      <input type="range" id="fontSize" min="12" max="24" value="16"
             oninput="updateFontSize(this.value)">
    </div>

    <div class="setting-item">
      <label>保存设置数量</label>
      <span id="saveCount">0</span>
    </div>
  </div>

  <div class="status-bar" id="statusBar"></div>

  <script>
    // 设置管理器
    const SettingsManager = {
      // 默认设置
      defaults: {
        theme: 'light',
        language: 'zh-CN',
        fontSize: 16
      },

      // 获取设置
      get(key) {
        const value = localStorage.getItem(`setting_${key}`);
        return value !== null ? JSON.parse(value) : this.defaults[key];
      },

      // 保存设置
      set(key, value) {
        localStorage.setItem(`setting_${key}`, JSON.stringify(value));
        this.incrementSaveCount();
        this.updateStatus();
      },

      // 增加保存计数
      incrementSaveCount() {
        const count = this.getSaveCount() + 1;
        localStorage.setItem('setting_saveCount', count.toString());
        document.getElementById('saveCount').textContent = count;
      },

      // 获取保存计数
      getSaveCount() {
        return Number(localStorage.getItem('setting_saveCount') || '0');
      },

      // 更新状态栏
      updateStatus() {
        const usage = getStorageUsage();
        const keys = Array.from({ length: localStorage.length }, (_, i) => localStorage.key(i));
        document.getElementById('statusBar').textContent =
          `存储键数: ${localStorage.length} | 已用: ${usage.usedKB} KB | 键: ${keys.join(', ')}`;
      },

      // 加载所有设置
      loadAll() {
        document.getElementById('saveCount').textContent = this.getSaveCount();

        // 深色模式
        const theme = this.get('theme');
        if (theme === 'dark') {
          document.body.classList.add('dark');
          document.getElementById('darkToggle').classList.add('active');
        }

        // 语言
        document.getElementById('langSelect').value = this.get('language');

        // 字体大小
        const fontSize = this.get('fontSize');
        document.getElementById('fontSize').value = fontSize;
        document.body.style.fontSize = fontSize + 'px';

        this.updateStatus();
      }
    };

    // 页面加载时恢复设置
    SettingsManager.loadAll();

    // 切换深色模式
    function toggleDark() {
      const toggle = document.getElementById('darkToggle');
      const isDark = toggle.classList.toggle('active');
      document.body.className = isDark ? 'dark' : 'light';
      SettingsManager.set('theme', isDark ? 'dark' : 'light');
    }

    // 保存设置
    function saveSetting(key, value) {
      SettingsManager.set(key, value);
    }

    // 更新字体大小
    function updateFontSize(value) {
      document.body.style.fontSize = value + 'px';
      SettingsManager.set('fontSize', Number(value));
    }

    // 存储使用量
    function getStorageUsage() {
      let total = 0;
      for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i);
        const value = localStorage.getItem(key);
        total += (key.length + value.length) * 2;
      }
      return { usedBytes: total, usedKB: (total / 1024).toFixed(2) };
    }
  </script>
</body>
</html>

注意事项

常见陷阱

问题原因解决方案
对象变字符串localStorage 只存字符串使用 JSON.stringify/parse
隐私模式限制Safari 隐私模式配额为 0检测配额,提供降级方案
存储满报错超过 5MB 限制捕获 QuotaExceededError
同步阻塞读写操作是同步的大量数据使用 IndexedDB
XSS 风险恶意脚本可读取数据不存敏感信息

隐私模式检测

javascript
function isLocalStorageAvailable() {
  try {
    const test = '__test__';
    localStorage.setItem(test, test);
    localStorage.removeItem(test);
    return true;
  } catch (e) {
    return false;
  }
}

if (!isLocalStorageAvailable()) {
  console.warn('localStorage 不可用(可能处于隐私模式)');
  // 降级使用内存存储
  window.memoryStorage = {};
}

最佳实践

  1. 命名空间化:使用前缀组织数据,如 app_user_app_cache_
  2. 异常捕获:始终用 try-catch 包裹 localStorage 操作
  3. 版本管理:存储数据版本号,便于升级时迁移
  4. 避免存储大对象:单个键值不宜过大,考虑使用 IndexedDB
  5. 定期清理:为缓存数据设置过期时间
javascript
// 带过期时间的存储
function setWithExpiry(key, value, ttlSeconds) {
  const item = {
    value: JSON.stringify(value),
    expiry: Date.now() + ttlSeconds * 1000
  };
  localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const item = JSON.parse(localStorage.getItem(key));
  if (!item) return null;
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return JSON.parse(item.value);
}

// 使用示例
setWithExpiry('cache_data', { items: [1, 2, 3] }, 3600); // 1 小时过期
const data = getWithExpiry('cache_data');

下一节

继续学习:sessionStorage

参考链接