离线存储发展史

  1. HTML5 之前,Cookie 是唯一在 HTML 标准中用于离线存储的技术,但是 Cookie 有一些不太友好的特征限制了它的应用场景:

    1. Cookie 会被附加在 HTTP 协议中,每次请求都会被发送到服务器端,增加了不必要的流量损耗;
    2. Cookie 大小限制在 4kb 左右(不同的浏览器有一些区别),对于一些复杂的业务场景可能不够;
  2. 这两个缺点在 Localstorage 中得到了有效的解决,Localstorage 是持久化的本地存储,除非主动删除数据,否则数据永远不会过期;

核心概念与区别

  1. localStoragesessionStorageHTML5 提供的客户端存储方案,用于在浏览器中存储键值对数据 (仅支持字符串类型),属于 Web Storage API 范畴;二者核心区别集中在生命周期、作用域、存储容量三方面,具体对比如下:

    特性 localStorage sessionStorage
    生命周期 永久存储,除非手动删除 会话级存储,页面会话结束后自动销毁 (关闭标签页/浏览器)
    作用域 同源 (协议、域名、端口一致) 下所有标签页/窗口共享 仅当前标签页 / 窗口 (同一窗口的不同 iframe 可共享,不同标签页即使同源也不共享)
    存储容量 5MB (各浏览器略有差异) 5MB (各浏览器略有差异)
    数据共享 同源页面间共享 仅当前会话 (标签页) 内共享
    适用场景 持久化存储 (如用户偏好设置、登录状态缓存) 临时存储 (如表单临时数据、页面跳转临时参数)
    服务器通信 不会随 HTTP 请求自动发送到服务器 不会随 HTTP 请求自动发送到服务器
  2. 关键补充:

    1. 存储类型限制:仅支持 string 类型键值对,存储非字符串数据 (如对象、数组) 需先序列化 (JSON.stringify),读取时需反序列化 (JSON.parse)
    2. 同源策略:二者均受同源限制,不同域名 / 协议 / 端口的页面无法访问彼此的存储数据 (安全特性)
    3. 隐私模式:部分浏览器在隐私模式下,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>

案例展示

注意事项与常见问题

  1. 存储类型限制

    1. 若直接存储非字符串数据 (如 localStorage.setItem(‘obj’, {a:1})),浏览器会自动调用 toString() 转为 [object Object],导致数据丢失;
    2. 解决方案:必须用 JSON.stringify 序列化,读取时用 JSON.parse 反序列化;
  2. 键名重复覆盖:若存储时使用已存在的键名,新值会覆盖旧值 (如再次存储 username=李四,会替换之前的 张三)

  3. 存储容量限制:单个域名下 localStoragesessionStorage 总容量约 5MB,超出会抛出 QuotaExceededError (超出配额) 异常 (可通过 try-catch 捕获)

  4. 安全风险:存储的数据在客户端可见 (F12 → Application → Storage 可查看),不可存储敏感信息 (如密码、token 等,建议用 HttpOnly Cookie 存储敏感数据)

  5. 浏览器兼容性:支持所有现代浏览器 (Chrome、Firefox、Edge、Safari 等),不支持 IE7 及以下 (若需兼容旧浏览器,可使用 cookie 或第三方库如 localForage)

面试题

localStorage 同域跨标签页通信

  1. localStorage 提供了 storage 事件机制,可实现同源下不同标签页 / 窗口间的通信—— 当一个页面修改 localStorage 数据时,其他同源页面会触发 storage 事件,从而同步数据状态

  2. 当前修改页面不触发 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>
    
  1. 核心区别集中在 生命周期、作用域、数据共享 三点:

    特性 Web Storage(localStorage/sessionStorage) Cookie
    存储容量 5MB (远大于 Cookie) 4KB (容量极小)
    生命周期 localStorage 永久,sessionStorage 会话级 可设置过期时间 (Expires/Max-Age),默认会话级
    与服务器通信 不随 HTTP 请求自动发送,完全客户端存储 每次 HTTP 请求都会自动携带 (请求头 Cookie 字段),占用带宽
    存储类型 仅支持字符串键值对 (需手动序列化非字符串) 仅支持字符串键值对 (格式为 key=value; 拼接)
    作用域 同源限制 可通过 Domain/Path 限制作用域 (如子域名共享)
    核心用途 客户端持久化 / 临时存储 (如用户偏好、表单临时数据) 身份认证 (如 JWT 存储)、状态保持 (如登录状态)
  2. 同一浏览器打开两个同源标签页,localStorage 共享,sessionStorage 不共享 (每个标签页是独立会话)

  3. iframe 与父页面同源,sessionStorage 共享 (同一窗口下的同源 iframe 属于同一会话)

Web Storage 受同源策略限制吗?具体是什么限制

  1. 受同源策略严格限制;

  2. 具体限制:只有 「协议、域名、端口」 三者完全一致的页面,才能共享同一 localStorage 数据;sessionStorage 除同源外,还限制在 「同一标签页 / 窗口」,即使同源不同标签页也无法共享;

  3. 延伸考点:跨域页面如何共享 Web Storage 数据?

    1. 跨域通信 (如 postMessage + Web Storage 结合)
    2. 服务器中转 (将数据存到服务器,跨域页面从服务器获取)
    3. 利用 Cookie (设置 Domain 为父域名,支持子域名跨域共享,但容量有限)

Web Storage 支持存储哪些数据类型?如果要存储对象 / 数组,该怎么做?

  1. 原生支持类型:仅字符串 (键和值都必须是字符串)

  2. 若存储非字符串数据 (如对象、数组、数字):需先通过 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); // 张三
    
  3. 延伸考点:

    直接存储对象会有什么问题?

    1. 会自动调用 toString() 方法,导致数据变为 [object Object],无法还原原始数据 (数据丢失)

    JSON.stringify 序列化时的注意事项?

    1. ① 不能序列化函数、undefinedSymbol 类型 (会忽略或转为 null)

    2. ② 循环引用的对象会报错;

    3. Date 类型会被转为 ISO 字符串 (读取时需手动转 Date)

Web Storage 的适用场景和不适用场景分别是什么?

  1. 适用场景:

    1. localStorage
      • ① 用户偏好设置 (如主题、字体大小)
      • ② 非敏感的登录状态标记 (如登录后保存用户名,无需每次输入)
      • ③ 本地缓存静态数据 (如城市列表、字典数据,减少接口请求)
    2. sessionStorage
      • ① 表单临时数据 (如用户填写一半的表单,防止刷新页面丢失)
      • ② 页面跳转临时参数 (如从列表页跳详情页,传递临时 ID)
      • ③ 临时计算结果 (如页面内复杂计算的中间值,仅当前会话有效)
  2. 不适用场景:

    1. 存储敏感数据 (如密码、Token、银行卡号)Web Storage 数据在客户端可见 (F12 → Application 可直接查看 / 修改),安全性低 (敏感数据建议用 HttpOnly Cookie 存储)
    2. 存储大量数据 (如超过 5MB 的文件、海量列表数据):容量限制 5MB,且无索引,查询效率低 (改用 IndexedDB)
    3. 需与服务器实时同步的数据 (如实时更新的用户余额)Web Storage 是客户端本地存储,无法自动同步到服务器 (需手动通过接口提交)

使用 Web Storage 时,有哪些常见的坑?如何避免?

  1. 坑 1:存储非字符串数据未序列化,导致数据丢失;

    解决:存储对象 / 数组时必须用 JSON.stringify() 序列化,读取时用 JSON.parse() 反序列化;

  2. 坑 2:键名重复覆盖原有数据;

    解决:存储前先通过 getItem() 检查键名是否存在,或设计唯一键名 (如加前缀 user_、config_)

  3. 坑 3:忽略容量限制,导致 QuotaExceededError

    解决:① 存储前捕获异常;② 定期清理无效数据 (如过期数据、临时数据)

  4. 坑 4:认为 localStorage 是永久存储,依赖其保存关键数据;

    解决:用户可手动清除浏览器数据 (如清除缓存),需做好数据备份 (如关键数据同步到服务器)

  5. 坑 5:跨标签页通信时误用 sessionStorage

    解决:跨标签页共享数据用 localStorage (同源)sessionStorage 仅适用于当前标签页;

如何实现 localStorage 的过期时间功能?

  1. Web Storage 原生不支持过期时间,需手动封装逻辑;

  2. 思路:存储时不仅存值,还存 「过期时间戳」;读取时判断当前时间是否超过过期时间,若过期则删除数据并返回 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 是同步还是异步的?有什么影响?

  1. Web Storage 的所有 API (setItem/getItem/removeItem 等) 都是 同步阻塞 的;

  2. 影响:

    1. ① 若存储大量数据 (如几 MB 的字符串),会阻塞主线程,导致页面卡顿 (尤其是在高频操作时,如循环存储多个键值对)
    2. ② 不适合存储超大文件或频繁读写的场景 (改用异步的 IndexedDB)
  3. 延伸考点:

    1. 问:「IndexedDB 是同步还是异步?与 Web Storage 相比有什么优势?」
    2. 答:IndexedDB 是异步的,不会阻塞主线程;优势:容量无明确限制、支持事务、索引查询、存储复杂数据结构,适合大量数据存储;

多个标签页同时操作 localStorage,会有冲突吗?如何解决?

  1. 会有冲突!例如:两个同源标签页同时读写同一个 key,可能导致数据覆盖 (如标签页 A 读取 count=1 后,标签页 B 也读取 count=1,两者都加 1 后存储,最终结果为 2 而非 3)

  2. 解决方案:

    1. 利用 storage 事件监听:当一个标签页修改 localStorage 时,其他同源标签页会触发 storage 事件,可在事件中同步数据 (适合简单场景)
    2. 加锁机制:通过 localStorage 本身模拟 「锁」 (如存储一个 lock 键,操作前检查锁是否存在,操作完成后释放锁),避免并发修改;
    3. 服务器兜底:关键数据 (如用户积分、订单状态) 最终以服务器数据为准,客户端仅做缓存,定期同步;

Web Storage 在隐私模式下的行为有什么不同?

  1. 不同浏览器对隐私模式 (无痕模式) 的处理略有差异,但核心规则:

    1. sessionStorage:行为与普通模式一致,关闭隐私窗口后数据销毁 (因为隐私模式本身就是一个独立会话)
    2. localStorage:多数浏览器 (如 Chrome、Firefox) 会临时存储数据 (仅在当前隐私窗口生命周期内有效),关闭隐私窗口后数据会被清空 (即使手动调用 clear() 也仅作用于当前隐私会话);部分浏览器 (如 Safari) 在隐私模式下会禁用 localStorage (调用 API 可能报错)
  2. 延伸考点:

    1. 问:「如何兼容隐私模式下的 localStorage 禁用场景?」
    2. 答:可通过 try-catch 捕获异常,降级使用 sessionStorageCookie,或提示用户切换普通模式;
    function safeSetStorage(key, value) {
      try {
        localStorage.setItem(key, value);
      } catch (e) {
        // 降级到 sessionStorage
        sessionStorage.setItem(key, value);
        console.warn("隐私模式下 localStorage 不可用,已降级到 sessionStorage");
      }
    }
    

Web Storage 支持跨域访问吗?如何实现跨域数据共享?

  1. 不支持直接跨域访问!同源策略严格限制不同域名 / 协议 / 端口的页面访问彼此的 Web Storage 数据;

  2. 跨域数据共享方案 (间接实现)

    1. postMessage + Web StorageA 域页面存储数据后,通过 postMessageB 域页面发送数据,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
        }
      });
      
    2. 服务器中转:A 域页面将数据上传到服务器,B 域页面从服务器接口获取数据 (最通用,无浏览器兼容性问题)
    3. Cookie 跨域:若两个域名是父子域名 (如 a.xxx.com 和 b.xxx.com),可设置 CookieDomain=xxx.com,实现跨子域共享 (但容量有限,仅 4KB)
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 离线存储发展史
  2. 2. 核心概念与区别
  3. 3. 常用 API(二者完全一致)
    1. 3.1. 案例代码
    2. 3.2. 案例展示
  4. 4. 注意事项与常见问题
  5. 5. 面试题
    1. 5.1. localStorage 同域跨标签页通信
    2. 5.2. Web Storage(localStorage/sessionStorage)与 Cookie 的区别
    3. 5.3. Web Storage 受同源策略限制吗?具体是什么限制
    4. 5.4. Web Storage 支持存储哪些数据类型?如果要存储对象 / 数组,该怎么做?
    5. 5.5. Web Storage 的适用场景和不适用场景分别是什么?
    6. 5.6. 使用 Web Storage 时,有哪些常见的坑?如何避免?
    7. 5.7. 如何实现 localStorage 的过期时间功能?
    8. 5.8. Web Storage 是同步还是异步的?有什么影响?
    9. 5.9. 多个标签页同时操作 localStorage,会有冲突吗?如何解决?
    10. 5.10. Web Storage 在隐私模式下的行为有什么不同?
    11. 5.11. Web Storage 支持跨域访问吗?如何实现跨域数据共享?