离线存储发展史
HTML5 之前,Cookie 是唯一在 HTML 标准中用于离线存储的技术,但是 Cookie 有一些不太友好的特征限制了它的应用场景:
- Cookie 会被附加在 HTTP 协议中,每次请求都会被发送到服务器端,增加了不必要的流量损耗;
- Cookie 大小限制在 4kb 左右(不同的浏览器有一些区别),对于一些复杂的业务场景可能不够;
这两个缺点在 Localstorage 中得到了有效的解决,Localstorage 是持久化的本地存储,除非主动删除数据,否则数据永远不会过期;
核心概念与区别
-
localStorage 和 sessionStorage 是 HTML5 提供的客户端存储方案,用于在浏览器中存储键值对数据 (仅支持字符串类型),属于 Web Storage API 范畴;二者核心区别集中在生命周期、作用域、存储容量三方面,具体对比如下:
特性 localStorage sessionStorage 生命周期 永久存储,除非手动删除 会话级存储,页面会话结束后自动销毁 (关闭标签页/浏览器) 作用域 同源 (协议、域名、端口一致) 下所有标签页/窗口共享 仅当前标签页 / 窗口 (同一窗口的不同 iframe 可共享,不同标签页即使同源也不共享) 存储容量 约 5MB (各浏览器略有差异) 约 5MB (各浏览器略有差异) 数据共享 同源页面间共享 仅当前会话 (标签页) 内共享 适用场景 持久化存储 (如用户偏好设置、登录状态缓存) 临时存储 (如表单临时数据、页面跳转临时参数) 服务器通信 不会随 HTTP 请求自动发送到服务器 不会随 HTTP 请求自动发送到服务器 -
关键补充:
- 存储类型限制:仅支持 string 类型键值对,存储非字符串数据 (如对象、数组) 需先序列化 (JSON.stringify),读取时需反序列化 (JSON.parse);
- 同源策略:二者均受同源限制,不同域名 / 协议 / 端口的页面无法访问彼此的存储数据 (安全特性);
- 隐私模式:部分浏览器在隐私模式下,localStorage 可能临时存储 (关闭隐私窗口后删除),sessionStorage 行为与普通模式一致;
常用 API(二者完全一致)
| API 方法 / 属性 | 说明 |
|---|---|
| setItem(key, value) | 存储数据:key 为存储键名 (字符串),value 为存储值 (仅字符串) |
| getItem(key) | 读取数据:根据 key 获取存储值,不存在则返回 null |
| removeItem(key) | 删除数据:根据 key 删除指定存储项 |
| clear() | 清空存储:删除当前存储对象 (localStorage/sessionStorage) 中所有数据 |
| key(index) | 获取键名:根据索引 (数字) 获取对应的存储键名 |
| length | 属性:返回当前存储对象中存储项的数量 |
案例代码
<head>
<meta charset="UTF-8">
<title>Web Storage Demo(样式隔离版)</title>
<style>
/* 基础样式:不添加前缀(通用重置,避免影响全局) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Arial', sans-serif;
}
/* 所有 Web Storage 相关样式添加 ws- 前缀 */
.ws-body {
padding: 2rem;
background-color: #f5f7fa;
}
.ws-container {
max-width: 1000px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.ws-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
max-width: 100%;
overflow: hidden;
}
.ws-card h2 {
color: #2d3748;
font-size: 1.25rem;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e8f4f8;
}
.ws-form-group {
margin-bottom: 1.2rem;
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
align-items: center;
}
.ws-form-group input {
flex: 1;
min-width: 150px;
padding: 0.6rem;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 0.95rem;
}
.ws-btn {
padding: 0.6rem 1.2rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.95rem;
transition: background 0.2s;
}
.ws-btn-primary {
background-color: #4299e1;
color: white;
}
.ws-btn-primary:hover {
background-color: #3182ce;
}
.ws-btn-danger {
background-color: #e53e3e;
color: white;
}
.ws-btn-danger:hover {
background-color: #c53030;
}
.ws-btn-warning {
background-color: #ed8936;
color: white;
}
.ws-btn-warning:hover {
background-color: #dd6b20;
}
.ws-log-area {
margin-top: 1.5rem;
padding: 1rem;
background-color: #f8f9fa;
border-radius: 4px;
height: 200px;
overflow-y: auto;
font-size: 0.9rem;
color: #4a5568;
white-space: pre-wrap;
}
.ws-tip {
margin-top: 0.8rem;
font-size: 0.85rem;
color: #718096;
line-height: 1.4;
}
.ws-complex-demo {
grid-column: 1 / -1;
}
/* 响应式样式:保留前缀 */
@media (max-width: 768px) {
.ws-container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body class="ws-body">
<div class="ws-container">
<!-- localStorage 卡片:所有类名添加 ws- 前缀 -->
<div class="ws-card">
<h2>📦 localStorage(永久存储)</h2>
<div class="ws-form-group">
<input type="text" id="ws-localKey" placeholder="键名(如:username)">
<input type="text" id="ws-localValue" placeholder="值(如:张三)">
<button class="ws-btn ws-btn-primary" onclick="LocalStorageDemo.save()">存储</button>
</div>
<div class="ws-form-group">
<input type="text" id="ws-localGetKey" placeholder="输入键名查询">
<button class="ws-btn ws-btn-primary" onclick="LocalStorageDemo.get()">读取</button>
<button class="ws-btn ws-btn-danger" onclick="LocalStorageDemo.remove()">删除</button>
<button class="ws-btn ws-btn-warning" onclick="LocalStorageDemo.clear()">清空全部</button>
</div>
<div class="ws-tip">
✨ 特性:关闭浏览器/标签页后数据仍保留<br>
📌 适用:用户偏好、非敏感配置
</div>
<div class="ws-log-area" id="ws-localLog">初始化中...</div>
</div>
<!-- sessionStorage 卡片:所有类名添加 ws- 前缀 -->
<div class="ws-card">
<h2>🔄 sessionStorage(会话存储)</h2>
<div class="ws-form-group">
<input type="text" id="ws-sessionKey" placeholder="键名(如:tempForm)">
<input type="text" id="ws-sessionValue" placeholder="值(如:临时数据)">
<button class="ws-btn ws-btn-primary" onclick="SessionStorageDemo.save()">存储</button>
</div>
<div class="ws-form-group">
<input type="text" id="ws-sessionGetKey" placeholder="输入键名查询">
<button class="ws-btn ws-btn-primary" onclick="SessionStorageDemo.get()">读取</button>
<button class="ws-btn ws-btn-danger" onclick="SessionStorageDemo.remove()">删除</button>
<button class="ws-btn ws-btn-warning" onclick="SessionStorageDemo.clear()">清空全部</button>
</div>
<div class="ws-tip">
⚠️ 特性:关闭标签页后数据自动销毁<br>
📌 适用:表单临时数据、页面跳转参数
</div>
<div class="ws-log-area" id="ws-sessionLog">初始化中...</div>
</div>
<!-- 复杂数据存储演示:所有类名添加 ws- 前缀 -->
<div class="ws-card ws-complex-demo">
<h2>📊 复杂数据存储(对象/数组)</h2>
<div class="ws-form-group">
<button class="ws-btn ws-btn-primary" onclick="ComplexDataDemo.saveObject()">存储用户对象</button>
<button class="ws-btn ws-btn-primary" onclick="ComplexDataDemo.saveArray()">存储任务数组</button>
<button class="ws-btn ws-btn-primary" onclick="ComplexDataDemo.readAll()">读取所有复杂数据</button>
<button class="ws-btn ws-btn-warning" onclick="ComplexDataDemo.clearAll()">清空复杂数据</button>
</div>
<div class="ws-tip">
📝 说明:非字符串数据需通过 JSON.stringify 序列化,读取时用 JSON.parse 反序列化
</div>
<div class="ws-log-area" id="ws-complexLog">点击按钮开始操作...</div>
</div>
</div>
<script>
// ====================== 工具类:Web Storage 基础封装(无修改) ======================
class StorageUtil {
static set(storage, key, value) {
if (!key) throw new Error('键名不能为空');
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
storage.setItem(key, stringValue);
}
static get(storage, key) {
const value = storage.getItem(key);
if (value === null) return null;
try {
return JSON.parse(value);
} catch (e) {
return value;
}
}
static remove(storage, key) {
storage.removeItem(key);
}
static clear(storage) {
storage.clear();
}
static getAll(storage) {
const data = {};
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
data[key] = this.get(storage, key);
}
return data;
}
}
// ====================== 业务模块:localStorage 演示(仅修改 DOM 选择器 ID) ======================
const LocalStorageDemo = {
logElement: document.getElementById('ws-localLog'),
init() {
this.log(`初始化完成,当前存储数据:\n${JSON.stringify(StorageUtil.getAll(localStorage), null, 2)}`);
},
save() {
const key = document.getElementById('ws-localKey').value.trim();
const value = document.getElementById('ws-localValue').value.trim();
if (!key) return this.log('❌ 错误:键名不能为空');
try {
StorageUtil.set(localStorage, key, value);
this.log(`✅ 存储成功:key="${key}", value="${value}"\n当前所有数据:\n${JSON.stringify(StorageUtil.getAll(localStorage), null, 2)}`);
} catch (e) {
this.log(`❌ 存储失败:${e.message}`);
}
},
get() {
const key = document.getElementById('ws-localGetKey').value.trim();
if (!key) return this.log('❌ 错误:请输入要查询的键名');
const value = StorageUtil.get(localStorage, key);
if (value === null) {
this.log(`❌ 读取失败:未找到 key="${key}" 的数据`);
} else {
this.log(`✅ 读取成功:key="${key}", value=${JSON.stringify(value, null, 2)}`);
}
},
remove() {
const key = document.getElementById('ws-localGetKey').value.trim();
if (!key) return this.log('❌ 错误:请输入要删除的键名');
StorageUtil.remove(localStorage, key);
this.log(`✅ 删除成功:key="${key}"\n剩余数据:\n${JSON.stringify(StorageUtil.getAll(localStorage), null, 2)}`);
},
clear() {
StorageUtil.clear(localStorage);
this.log('✅ 已清空所有 localStorage 数据');
},
log(text) {
this.logElement.textContent = text;
}
};
// ====================== 业务模块:sessionStorage 演示(仅修改 DOM 选择器 ID) ======================
const SessionStorageDemo = {
logElement: document.getElementById('ws-sessionLog'),
init() {
this.log(`初始化完成,当前存储数据:\n${JSON.stringify(StorageUtil.getAll(sessionStorage), null, 2)}`);
},
save() {
const key = document.getElementById('ws-sessionKey').value.trim();
const value = document.getElementById('ws-sessionValue').value.trim();
if (!key) return this.log('❌ 错误:键名不能为空');
try {
StorageUtil.set(sessionStorage, key, value);
this.log(`✅ 存储成功:key="${key}", value="${value}"\n当前所有数据:\n${JSON.stringify(StorageUtil.getAll(sessionStorage), null, 2)}`);
} catch (e) {
this.log(`❌ 存储失败:${e.message}`);
}
},
get() {
const key = document.getElementById('ws-sessionGetKey').value.trim();
if (!key) return this.log('❌ 错误:请输入要查询的键名');
const value = StorageUtil.get(sessionStorage, key);
if (value === null) {
this.log(`❌ 读取失败:未找到 key="${key}" 的数据`);
} else {
this.log(`✅ 读取成功:key="${key}", value=${JSON.stringify(value, null, 2)}`);
}
},
remove() {
const key = document.getElementById('ws-sessionGetKey').value.trim();
if (!key) return this.log('❌ 错误:请输入要删除的键名');
StorageUtil.remove(sessionStorage, key);
this.log(`✅ 删除成功:key="${key}"\n剩余数据:\n${JSON.stringify(StorageUtil.getAll(sessionStorage), null, 2)}`);
},
clear() {
StorageUtil.clear(sessionStorage);
this.log('✅ 已清空所有 sessionStorage 数据');
},
log(text) {
this.logElement.textContent = text;
}
};
// ====================== 业务模块:复杂数据存储演示(仅修改 DOM 选择器 ID) ======================
const ComplexDataDemo = {
logElement: document.getElementById('ws-complexLog'),
objectKey: 'userProfile',
arrayKey: 'taskList',
saveObject() {
const user = {
id: 1001,
name: '李四',
age: 28,
isVip: true,
registerTime: new Date().toLocaleString()
};
try {
StorageUtil.set(localStorage, this.objectKey, user);
this.log(`✅ 对象存储成功:\n${JSON.stringify(user, null, 2)}`);
} catch (e) {
this.log(`❌ 对象存储失败:${e.message}`);
}
},
saveArray() {
const tasks = [
{ id: 1, name: '学习 Web Storage', status: 'doing' },
{ id: 2, name: '掌握模块化编程', status: 'todo' },
{ id: 3, name: '验证会话存储特性', status: 'done' }
];
try {
StorageUtil.set(sessionStorage, this.arrayKey, tasks);
this.log(`✅ 数组存储成功:\n${JSON.stringify(tasks, null, 2)}`);
} catch (e) {
this.log(`❌ 数组存储失败:${e.message}`);
}
},
readAll() {
const storedObject = StorageUtil.get(localStorage, this.objectKey);
const storedArray = StorageUtil.get(sessionStorage, this.arrayKey);
let logText = '';
logText += storedObject
? `✅ 读取到用户对象:\n${JSON.stringify(storedObject, null, 2)}\n\n`
: '❌ 未找到存储的用户对象\n\n';
logText += storedArray
? `✅ 读取到任务数组:\n${JSON.stringify(storedArray, null, 2)}`
: '❌ 未找到存储的任务数组';
this.log(logText);
},
clearAll() {
StorageUtil.remove(localStorage, this.objectKey);
StorageUtil.remove(sessionStorage, this.arrayKey);
this.log('✅ 已清空所有复杂数据(对象+数组)');
},
log(text) {
this.logElement.textContent = text;
}
};
// ====================== 页面加载初始化(无修改) ======================
window.onload = function () {
LocalStorageDemo.init();
SessionStorageDemo.init();
};
</script>
</body>
案例展示
注意事项与常见问题
-
存储类型限制:- 若直接存储非字符串数据 (如 localStorage.setItem(‘obj’, {a:1})),浏览器会自动调用 toString() 转为 [object Object],导致数据丢失;
- 解决方案:必须用 JSON.stringify 序列化,读取时用 JSON.parse 反序列化;
-
键名重复覆盖:若存储时使用已存在的键名,新值会覆盖旧值 (如再次存储 username=李四,会替换之前的 张三); -
存储容量限制:单个域名下 localStorage 和 sessionStorage 总容量约 5MB,超出会抛出 QuotaExceededError (超出配额) 异常 (可通过 try-catch 捕获); -
安全风险:存储的数据在客户端可见 (F12 → Application → Storage 可查看),不可存储敏感信息 (如密码、token 等,建议用 HttpOnly Cookie 存储敏感数据); -
浏览器兼容性:支持所有现代浏览器 (Chrome、Firefox、Edge、Safari 等),不支持 IE7 及以下 (若需兼容旧浏览器,可使用 cookie 或第三方库如 localForage);
面试题
localStorage 同域跨标签页通信
-
localStorage 提供了 storage 事件机制,可实现同源下不同标签页 / 窗口间的通信—— 当一个页面修改 localStorage 数据时,其他同源页面会触发 storage 事件,从而同步数据状态
-
当前修改页面不触发 storage 事件,浏览器设计如此,避免同一页面内的重复处理 (当前页面已知道自己修改了数据,无需再监听);
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>localStorage 跨页面监听 Demo</title> <style> /* 延续之前的 ws- 前缀样式隔离 */ .ws-container { max-width: 800px; margin: 2rem auto; padding: 0 1rem; } .ws-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 1.5rem; } .ws-card h2 { color: #2d3748; font-size: 1.2rem; margin-bottom: 1rem; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.5rem; } .ws-form-group { margin: 1rem 0; display: flex; gap: 0.8rem; flex-wrap: wrap; } .ws-input { flex: 1; min-width: 200px; padding: 0.6rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.95rem; } .ws-btn { padding: 0.6rem 1.2rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.95rem; transition: background 0.2s; } .ws-btn-primary { background-color: #4299e1; color: white; } .ws-btn-danger { background-color: #e53e3e; color: white; } .ws-log-area { margin-top: 1rem; padding: 1rem; background-color: #f8f9fa; border-radius: 4px; height: 250px; overflow-y: auto; font-size: 0.9rem; color: #4a5568; white-space: pre-wrap; } .ws-tip { font-size: 0.85rem; color: #718096; margin-top: 0.5rem; } </style> </head> <body> <div class="ws-container"> <!-- 发送端:修改 localStorage(触发事件) --> <div class="ws-card"> <h2>📤 发送端(修改 localStorage)</h2> <div class="ws-form-group"> <input type="text" class="ws-input" id="ws-sendKey" placeholder="输入键名(如:msg)"> <input type="text" class="ws-input" id="ws-sendValue" placeholder="输入值(如:Hello 跨页面)"> <button class="ws-btn ws-btn-primary" onclick="sendData()">发送数据</button> </div> <div class="ws-form-group"> <input type="text" class="ws-input" id="ws-removeKey" placeholder="输入要删除的键名"> <button class="ws-btn ws-btn-danger" onclick="removeData()">删除数据</button> <button class="ws-btn ws-btn-danger" onclick="clearAll()">清空所有</button> </div> <div class="ws-tip"> 操作说明:点击按钮修改 localStorage,其他同源标签页会接收事件 </div> <div class="ws-log-area" id="ws-sendLog">发送日志将显示在这里...</div> </div> <!-- 接收端:监听 storage 事件(同步数据) --> <div class="ws-card"> <h2>📥 接收端(监听跨页面事件)</h2> <button class="ws-btn ws-btn-primary" onclick="startListen()">开始监听</button> <button class="ws-btn ws-btn-danger" onclick="stopListen()">停止监听</button> <div class="ws-tip"> 操作说明:打开多个同源标签页,在任意标签页发送数据,此处会同步接收 </div> <div class="ws-log-area" id="ws-receiveLog">未开始监听...(点击「开始监听」)</div> </div> </div> <script> // 元素获取 const sendLogEl = document.getElementById('ws-sendLog'); const receiveLogEl = document.getElementById('ws-receiveLog'); let isListening = false; // 监听状态标记 // ====================== 发送端:修改 localStorage ====================== /** 发送数据(setItem) */ function sendData() { const key = document.getElementById('ws-sendKey').value.trim(); const value = document.getElementById('ws-sendValue').value.trim(); if (!key) { logSend('❌ 键名不能为空!'); return; } try { const oldValue = localStorage.getItem(key); localStorage.setItem(key, value); logSend(`✅ 发送成功: - 键名:${key} - 旧值:${oldValue || '无'} - 新值:${value} - 时间:${formatTime(new Date())}`); } catch (e) { logSend(`❌ 发送失败:${e.message}`); } } /** 删除数据(removeItem) */ function removeData() { const key = document.getElementById('ws-removeKey').value.trim(); if (!key) { logSend('❌ 请输入要删除的键名!'); return; } const oldValue = localStorage.getItem(key); if (!oldValue) { logSend(`❌ 未找到键名:${key}`); return; } localStorage.removeItem(key); logSend(`✅ 删除成功: - 键名:${key} - 被删除值:${oldValue} - 时间:${formatTime(new Date())}`); } /** 清空所有(clear) */ function clearAll() { if (localStorage.length === 0) { logSend('❌ 暂无数据可清空!'); return; } localStorage.clear(); logSend(`✅ 清空所有 localStorage 数据 - 时间:${formatTime(new Date())}`); } // ====================== 接收端:监听 storage 事件 ====================== /** 开始监听 */ function startListen() { if (isListening) { logReceive('⚠️ 已在监听中,无需重复启动!'); return; } // 绑定 storage 事件 window.addEventListener('storage', handleStorageEvent); isListening = true; logReceive(`✅ 监听已启动(同源标签页修改 localStorage 会触发) - 监听时间:${formatTime(new Date())}`); } /** 停止监听 */ function stopListen() { if (!isListening) { logReceive('⚠️ 未启动监听,无需停止!'); return; } // 移除 storage 事件 window.removeEventListener('storage', handleStorageEvent); isListening = false; logReceive(`❌ 监听已停止 - 停止时间:${formatTime(new Date())}`); } /** storage 事件处理函数 */ function handleStorageEvent(e) { // e 是 StorageEvent 对象,包含修改的关键信息 const log = `📢 收到跨页面存储事件: - 触发页面 URL:${e.url} - 操作类型:${getOperationType(e)} - 键名:${e.key || '无(clear 操作)'} - 旧值:${e.oldValue || '无'} - 新值:${e.newValue || '无(删除/清空操作)'} - 触发时间:${formatTime(new Date())} `; logReceive(log, true); // 追加日志(不覆盖) } // ====================== 辅助函数 ====================== /** 格式化时间 */ function formatTime(date) { return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } /** 判断操作类型(set/remove/clear) */ function getOperationType(e) { if (e.key === null) return '清空所有(clear)'; if (e.oldValue === null) return '新增数据(setItem)'; if (e.newValue === null) return '删除数据(removeItem)'; return '修改数据(setItem)'; } /** 发送端日志输出 */ function logSend(text) { sendLogEl.textContent = text; } /** 接收端日志输出(支持追加) */ function logReceive(text, isAppend = false) { if (isAppend) { receiveLogEl.textContent = text + receiveLogEl.textContent; } else { receiveLogEl.textContent = text; } } // 页面加载时初始化 window.onload = function() { logSend(`初始化完成,当前 localStorage 数据量:${localStorage.length} 条`); }; </script> </body> </html>
Web Storage(localStorage/sessionStorage)与 Cookie 的区别
-
核心区别集中在 生命周期、作用域、数据共享 三点:
特性 Web Storage(localStorage/sessionStorage) Cookie 存储容量 约 5MB (远大于 Cookie) 约 4KB (容量极小) 生命周期 localStorage 永久,sessionStorage 会话级 可设置过期时间 (Expires/Max-Age),默认会话级 与服务器通信 不随 HTTP 请求自动发送,完全客户端存储 每次 HTTP 请求都会自动携带 (请求头 Cookie 字段),占用带宽 存储类型 仅支持字符串键值对 (需手动序列化非字符串) 仅支持字符串键值对 (格式为 key=value; 拼接) 作用域 同源限制 可通过 Domain/Path 限制作用域 (如子域名共享) 核心用途 客户端持久化 / 临时存储 (如用户偏好、表单临时数据) 身份认证 (如 JWT 存储)、状态保持 (如登录状态) -
同一浏览器打开两个同源标签页,localStorage 共享,sessionStorage 不共享 (每个标签页是独立会话);
-
iframe 与父页面同源,sessionStorage 共享 (同一窗口下的同源 iframe 属于同一会话);
Web Storage 受同源策略限制吗?具体是什么限制
-
受同源策略严格限制;
-
具体限制:只有 「协议、域名、端口」 三者完全一致的页面,才能共享同一 localStorage 数据;sessionStorage 除同源外,还限制在 「同一标签页 / 窗口」,即使同源不同标签页也无法共享;
-
延伸考点:跨域页面如何共享 Web Storage 数据?
- 跨域通信 (如 postMessage + Web Storage 结合);
- 服务器中转 (将数据存到服务器,跨域页面从服务器获取);
- 利用 Cookie (设置 Domain 为父域名,支持子域名跨域共享,但容量有限);
Web Storage 支持存储哪些数据类型?如果要存储对象 / 数组,该怎么做?
-
原生支持类型:仅字符串 (键和值都必须是字符串);
-
若存储非字符串数据 (如对象、数组、数字):需先通过 JSON.stringify() 序列化 (将对象 / 数组转为 JSON 字符串),读取时通过 JSON.parse() 反序列化 (还原为原始类型);
// 存储对象 const user = { id: 1, name: "张三" }; localStorage.setItem("user", JSON.stringify(user)); // 读取对象 const storedUser = JSON.parse(localStorage.getItem("user")); console.log(storedUser.name); // 张三 -
延伸考点:
直接存储对象会有什么问题?
- 会自动调用 toString() 方法,导致数据变为 [object Object],无法还原原始数据 (数据丢失);
JSON.stringify 序列化时的注意事项?
-
① 不能序列化函数、undefined、Symbol 类型 (会忽略或转为 null);
-
② 循环引用的对象会报错;
-
③ Date 类型会被转为 ISO 字符串 (读取时需手动转 Date);
Web Storage 的适用场景和不适用场景分别是什么?
-
适用场景:
- localStorage:
- ① 用户偏好设置 (如主题、字体大小);
- ② 非敏感的登录状态标记 (如登录后保存用户名,无需每次输入);
- ③ 本地缓存静态数据 (如城市列表、字典数据,减少接口请求);
- sessionStorage:
- ① 表单临时数据 (如用户填写一半的表单,防止刷新页面丢失);
- ② 页面跳转临时参数 (如从列表页跳详情页,传递临时 ID);
- ③ 临时计算结果 (如页面内复杂计算的中间值,仅当前会话有效);
- localStorage:
-
不适用场景:
- 存储敏感数据 (如密码、Token、银行卡号):Web Storage 数据在客户端可见 (F12 → Application 可直接查看 / 修改),安全性低 (敏感数据建议用 HttpOnly Cookie 存储);
- 存储大量数据 (如超过 5MB 的文件、海量列表数据):容量限制 5MB,且无索引,查询效率低 (改用 IndexedDB);
- 需与服务器实时同步的数据 (如实时更新的用户余额):Web Storage 是客户端本地存储,无法自动同步到服务器 (需手动通过接口提交);
使用 Web Storage 时,有哪些常见的坑?如何避免?
-
坑 1:存储非字符串数据未序列化,导致数据丢失;
解决:存储对象 / 数组时必须用 JSON.stringify() 序列化,读取时用 JSON.parse() 反序列化;
-
坑 2:键名重复覆盖原有数据;
解决:存储前先通过 getItem() 检查键名是否存在,或设计唯一键名 (如加前缀 user_、config_);
-
坑 3:忽略容量限制,导致 QuotaExceededError;
解决:① 存储前捕获异常;② 定期清理无效数据 (如过期数据、临时数据);
-
坑 4:认为 localStorage 是永久存储,依赖其保存关键数据;
解决:用户可手动清除浏览器数据 (如清除缓存),需做好数据备份 (如关键数据同步到服务器);
-
坑 5:跨标签页通信时误用 sessionStorage;
解决:跨标签页共享数据用 localStorage (同源),sessionStorage 仅适用于当前标签页;
如何实现 localStorage 的过期时间功能?
-
Web Storage 原生不支持过期时间,需手动封装逻辑;
-
思路:存储时不仅存值,还存 「过期时间戳」;读取时判断当前时间是否超过过期时间,若过期则删除数据并返回 null;
// 封装带过期时间的 localStorage const LocalStorageWithExpire = { // 存储:value=数据,expire=过期时间(单位:秒) set(key, value, expire) { const data = { value: value, expire: expire ? Date.now() + expire * 1000 : Infinity // 无过期时间则设为无穷大 }; localStorage.setItem(key, JSON.stringify(data)); }, // 读取:过期则返回 null 并删除数据 get(key) { const stored = localStorage.getItem(key); if (!stored) return null; const data = JSON.parse(stored); // 判断是否过期 if (Date.now() > data.expire) { localStorage.removeItem(key); return null; } return data.value; } }; // 使用示例:存储 10 秒后过期的数据 LocalStorageWithExpire.set("tempKey", "tempValue", 10); setTimeout(() => { console.log(LocalStorageWithExpire.get("tempKey")); // 10 秒内返回 tempValue,之后返回 null }, 11000);
Web Storage 是同步还是异步的?有什么影响?
-
Web Storage 的所有 API (setItem/getItem/removeItem 等) 都是 同步阻塞 的;
-
影响:
- ① 若存储大量数据 (如几 MB 的字符串),会阻塞主线程,导致页面卡顿 (尤其是在高频操作时,如循环存储多个键值对);
- ② 不适合存储超大文件或频繁读写的场景 (改用异步的 IndexedDB);
-
延伸考点:
- 问:「IndexedDB 是同步还是异步?与 Web Storage 相比有什么优势?」
- 答:IndexedDB 是异步的,不会阻塞主线程;优势:容量无明确限制、支持事务、索引查询、存储复杂数据结构,适合大量数据存储;
多个标签页同时操作 localStorage,会有冲突吗?如何解决?
-
会有冲突!例如:两个同源标签页同时读写同一个 key,可能导致数据覆盖 (如标签页 A 读取 count=1 后,标签页 B 也读取 count=1,两者都加 1 后存储,最终结果为 2 而非 3);
-
解决方案:
- 利用 storage 事件监听:当一个标签页修改 localStorage 时,其他同源标签页会触发 storage 事件,可在事件中同步数据 (适合简单场景);
- 加锁机制:通过 localStorage 本身模拟 「锁」 (如存储一个 lock 键,操作前检查锁是否存在,操作完成后释放锁),避免并发修改;
- 服务器兜底:关键数据 (如用户积分、订单状态) 最终以服务器数据为准,客户端仅做缓存,定期同步;
Web Storage 在隐私模式下的行为有什么不同?
-
不同浏览器对隐私模式 (无痕模式) 的处理略有差异,但核心规则:
- sessionStorage:行为与普通模式一致,关闭隐私窗口后数据销毁 (因为隐私模式本身就是一个独立会话);
- localStorage:多数浏览器 (如 Chrome、Firefox) 会临时存储数据 (仅在当前隐私窗口生命周期内有效),关闭隐私窗口后数据会被清空 (即使手动调用 clear() 也仅作用于当前隐私会话);部分浏览器 (如 Safari) 在隐私模式下会禁用 localStorage (调用 API 可能报错);
-
延伸考点:
- 问:「如何兼容隐私模式下的 localStorage 禁用场景?」
- 答:可通过 try-catch 捕获异常,降级使用 sessionStorage 或 Cookie,或提示用户切换普通模式;
function safeSetStorage(key, value) { try { localStorage.setItem(key, value); } catch (e) { // 降级到 sessionStorage sessionStorage.setItem(key, value); console.warn("隐私模式下 localStorage 不可用,已降级到 sessionStorage"); } }
Web Storage 支持跨域访问吗?如何实现跨域数据共享?
-
不支持直接跨域访问!同源策略严格限制不同域名 / 协议 / 端口的页面访问彼此的 Web Storage 数据;
-
跨域数据共享方案 (间接实现):
- postMessage + Web Storage:A 域页面存储数据后,通过 postMessage 向 B 域页面发送数据,B 域页面接收后存储到自己的 Web Storage 中 (需双方页面配合监听 message 事件);
// A 域页面(发送数据) const targetIframe = document.getElementById("b-iframe"); targetIframe.contentWindow.postMessage({ type: "shareData", data: "hello" }, "https://b.com"); // B 域页面(接收数据) window.addEventListener("message", (e) => { if (e.origin === "https://a.com" && e.data.type === "shareData") { localStorage.setItem("sharedData", e.data.data); // 存储到 B 域的 localStorage } }); - 服务器中转:A 域页面将数据上传到服务器,B 域页面从服务器接口获取数据 (最通用,无浏览器兼容性问题);
- Cookie 跨域:若两个域名是父子域名 (如 a.xxx.com 和 b.xxx.com),可设置 Cookie 的 Domain=xxx.com,实现跨子域共享 (但容量有限,仅 4KB);
- postMessage + Web Storage:A 域页面存储数据后,通过 postMessage 向 B 域页面发送数据,B 域页面接收后存储到自己的 Web Storage 中 (需双方页面配合监听 message 事件);
API 篇:requestAnimationFrame
上一篇
目录
- 1. 离线存储发展史
- 2. 核心概念与区别
- 3. 常用 API(二者完全一致)
- 4. 注意事项与常见问题
- 5. 面试题
- 5.1. localStorage 同域跨标签页通信
- 5.2. Web Storage(localStorage/sessionStorage)与 Cookie 的区别
- 5.3. Web Storage 受同源策略限制吗?具体是什么限制
- 5.4. Web Storage 支持存储哪些数据类型?如果要存储对象 / 数组,该怎么做?
- 5.5. Web Storage 的适用场景和不适用场景分别是什么?
- 5.6. 使用 Web Storage 时,有哪些常见的坑?如何避免?
- 5.7. 如何实现 localStorage 的过期时间功能?
- 5.8. Web Storage 是同步还是异步的?有什么影响?
- 5.9. 多个标签页同时操作 localStorage,会有冲突吗?如何解决?
- 5.10. Web Storage 在隐私模式下的行为有什么不同?
- 5.11. Web Storage 支持跨域访问吗?如何实现跨域数据共享?