localStorage
localStorage 是 HTML5 Web Storage API 提供的客户端持久化存储方案,允许网页在用户的浏览器中存储键值对数据。与 Cookie 不同,localStorage 的数据不会随每次 HTTP 请求发送到服务器,容量更大(通常 5MB),且没有过期时间,适合存储用户偏好设置、主题选择、本地缓存等数据。
前置知识
阅读本节前,建议先了解:拖放实战示例
基础概念
什么是 localStorage
localStorage 是 window.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.com 与 https://example.com |
| 域名 | 不同域名的数据完全隔离 | example.com 与 test.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 = {};
}最佳实践
- 命名空间化:使用前缀组织数据,如
app_user_、app_cache_ - 异常捕获:始终用 try-catch 包裹 localStorage 操作
- 版本管理:存储数据版本号,便于升级时迁移
- 避免存储大对象:单个键值不宜过大,考虑使用 IndexedDB
- 定期清理:为缓存数据设置过期时间
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