前端存储全攻略 & JavaScript this 绑定完全指南

2 阅读13分钟

🌐 前端存储全攻略 & JavaScript this 绑定完全指南

从浏览器缓存到服务端存储,从 this 指向到事件驱动 —— 一套完整的前端核心知识体系


📖 写在前面

在前端开发中,数据存储this 指向 是两个绕不开的核心话题。无论是面试八股文,还是日常业务开发,它们都无处不在。

本文将从 存储体系全景 出发,涵盖浏览器端存储、服务端存储、缓存策略,并深入剖析 JavaScript 中令人头疼的 this 绑定机制,配合可运行的 Demo 代码,帮你彻底掌握这些知识点。


一、存储体系全景图 🌍

在现代 Web 应用中,数据存储是一个多层次、多维度的系统工程。我们先来俯瞰全貌:

┌─────────────────────────────────────────────────────────┐
│                    前端存储体系                         │
├───────────────┬──────────────────┬──────────────────────┤
│   浏览器端     │     服务端       │      智能层         │
├───────────────┼──────────────────┼──────────────────────┤
│ • Cookie      │ • MySQL (关系型)  │ • Redis (缓存)      │
│ • LocalStorage│ • PostgreSQL     │ • LLM Embedding      │
│ • SessionStg  │ • MongoDB (文档)  │   (向量存储)        │
│ • IndexedDB   │ • 云存储 (OSS)    │                     │
│ • Cache API   │ • JSON/CSV 文件  │                      │
└───────────────┴──────────────────┴──────────────────────┘

1.1 浏览器缓存 —— 为什么"秒开"?

你是否有过这样的体验:打开一个之前访问过的页面,速度快得惊人

这就是浏览器缓存的功劳。当你第一次访问一个页面时,浏览器会把静态资源(HTML、CSS、JS、图片等)缓存到本地磁盘。下次再访问时,直接从本地读取,省去了网络请求的时间。

首次访问:  🌐 网络请求 → ⏳ 等待响应 → 📥 下载资源 → 📄 渲染页面
再次访问:  💾 本地缓存 → 📄 直接渲染(几乎瞬间完成)

缓存策略核心 HTTP 头:

头部字段作用示例
Cache-Control控制缓存行为max-age=31536000
ETag资源版本标识"33a64df5"
Last-Modified最后修改时间Wed, 21 Oct 2025
Expires过期时间(HTTP 1.0)Thu, 01 Dec 2026

1.2 浏览器本地存储四剑客

特性CookieLocalStorageSessionStorageIndexedDB
容量~4KB~5-10MB~5-10MB几十MB~几百MB
生命周期可设过期时间永久(手动清除)标签页关闭即清除永久
作用域同源+同路径同源同源+同标签页同源
与服务器通信每次请求自动携带 ❌不自动发送 ✅不自动发送 ✅不自动发送 ✅
API 易用性需手动解析简单同步 API简单同步 API异步 API,较复杂
存储类型字符串字符串字符串任意类型(文件、Blob等)

选择建议:

🔐 需要传给服务端?                  → Cookie
📝 少量配置/用户偏好,需长期保存?     → LocalStorage
🔒 仅当前会话的敏感临时数据?         → SessionStorage
📦 大量结构化数据/离线应用?          → IndexedDB

1.3 服务端存储 —— 从 MySQL 到 Redis

MySQL(关系型数据库)
-- 最经典的关系型数据库,适合结构化数据
-- 比如:用户表、订单表、文章表
SELECT * FROM articles WHERE status = 'published' ORDER BY created_at DESC;

适用场景: 数据之间有强关联关系、需要事务支持、数据一致性要求高。

Redis(缓存层)⚡

这是一个非常重要的性能优化策略:

┌─────────────────────────────────────────────┐
│                                                 │
│   用户请求文章列表                                │
│        │                                        │
│        ▼                                        │
│   ┌─────────┐    命中     ┌──────────┐          │
│   │  Redis  │ ◀────────── │  应用层   │          │
│   │ (内存)  │ ──────────▶ │          │          │
│   └─────────┘    未命中    └──────────┘          │
│        │                         │              │
│        │  未命中时回源            │              │
│        ▼                         │              │
│   ┌─────────┐                    │              │
│   │  MySQL  │ ◀──────────────────┘              │
│   │ (磁盘)  │  读取后写入 Redis                 │
│   └─────────┘                                   │
│                                                 │
└─────────────────────────────────────────────┘
// 典型的 "Cache Aside" 模式
async function getArticleList() {
  // 1️⃣ 先查 Redis 缓存
  const cached = await redis.get('articles:list');
  if (cached) {
    return JSON.parse(cached); // 🎯 命中!直接返回,毫秒级响应
  }

  // 2️⃣ 未命中,查 MySQL
  const articles = await mysql.query('SELECT * FROM articles ORDER BY id DESC LIMIT 20');

  // 3️⃣ 写入 Redis,设置过期时间(比如 60 秒)
  await redis.setex('articles:list', 60, JSON.stringify(articles));

  return articles;
}

为什么要加 Redis? MySQL 的数据存在磁盘上,每次查询都要走磁盘 I/O,性能有瓶颈。Redis 是基于内存的,读写速度比 MySQL 快 100-1000 倍。把热点数据放在 Redis 里,可以大幅减轻数据库压力。

LLM Embedding 向量存储 🧠

这是 AI 时代的新型存储需求。当你做语义搜索、推荐系统时,需要存储文本的向量表示(Embedding):

// 概念示意:将文本转为向量,存储后进行相似度搜索
const embedding = await llm.embed("前端存储最佳实践");
// → [0.023, -0.451, 0.789, ..., 0.134]  // 1536 维向量

// 存储到向量数据库(如 Pinecone、Milvus、pgvector)
await vectorDB.insert({ id: 'article_1', vector: embedding });

// 相似度搜索:"浏览器缓存" 相关的文章
const results = await vectorDB.search(queryEmbedding, { topK: 10 });

二、实战:LocalStorage 应用 🛠️

让我们来构建一个基于 LocalStorage 的 "小吃清单"(Local TAPAS) 应用:

2.1 HTML 结构

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LocalStorage Demo</title>
    <link rel="stylesheet" href="./common.css">
</head>
<body>
    <div class="wrapper">
        <h2>🍢 Local TAPAS</h2>
        <p>你的美食清单</p>
        <ul class="plates">
            <li>Loading Tapas ...</li>
        </ul>
        <form class="add-items">
            <input type="text" name="item"
                   placeholder="添加一道小吃..." required>
            <input type="submit" value="+ 添加">
        </form>
    </div>
    <script src="./common.js"></script>
</body>
</html>

2.2 核心逻辑

// ==========================================
// 一个完整的 LocalStorage 增删查 Demo
// ==========================================

const addItems = document.querySelector('.add-items');
const itemsList = document.querySelector('.plates');

// 🗄️ 从 LocalStorage 读取已有的数据
// 注意:LocalStorage 只能存字符串,所以要用 JSON 转换
const items = JSON.parse(localStorage.getItem('items')) || [];

// 📝 核心函数:添加新项
function addItem(e) {
  // 🛑 阻止表单默认提交行为(否则页面会刷新!)
  e.preventDefault();

  // 获取用户输入
  const text = this.querySelector('[name=item]').value;

  const item = {
    text,        // 文本内容
    done: false  // 是否已完成/勾选
  };

  items.push(item);
  populateList(items, itemsList);

  // 💾 关键步骤:序列化后存入 LocalStorage
  localStorage.setItem('items', JSON.stringify(items));

  // 清空输入框
  this.reset();
}

// 🖼️ 渲染列表到 DOM
function populateList(plates = [], platesList) {
  platesList.innerHTML = plates.map((plate, i) => {
    return `
      <li>
        <input type="checkbox" data-index=${i} id="item${i}"
               ${plate.done ? 'checked' : ''} />
        <label for="item${i}">${plate.text}</label>
      </li>
    `;
  }).join('');
}

// 🎧 绑定事件
addItems.addEventListener('submit', addItem);

💡 核心要点: LocalStorage 的 API 是同步的,只能存储字符串。存对象时要用 JSON.stringify(),读取时用 JSON.parse()


三、深度解析:JavaScript this 绑定机制 🔍

this 是 JavaScript 中最令人困惑的概念之一。它的核心规则只有一句话:

this 的值是在函数运行时(调用时)决定的,而不是在函数声明时决定的。this 指向函数的调用者。

记住这个口诀之后再来看各种场景,就会豁然开朗:

3.1 五大绑定规则 📋

规则一:普通函数调用 → this 指向全局对象
// ⚠️ 非严格模式
function sayHi() {
  console.log(this); // window(浏览器)/ global(Node.js)
}
sayHi();

// ✅ 严格模式(推荐!)
"use strict";
function sayHiStrict() {
  console.log(this); // undefined —— 更安全!
}
sayHiStrict();

为什么要启用严格模式?

// 😱 可怕的 var 陷阱 —— 会污染 window 对象!
var name = "李四";       // window.name = "李四"
let age = 25;           // window.age = undefined ✅ 不污染

// 这导致全局变量和 window 属性混在一起,极易出 bug
console.log(window.name); // "李四"
console.log(window.age);  // undefined

面试要点: var 声明的变量会挂载到 window 上,let/const 不会。这也是不再推荐使用 var 的重要原因之一。

规则二:作为对象方法调用 → this 指向调用对象
const obj = {
  name: "张三",
  say() {
    console.log(`${this.name} 在说话`);
  }
};

obj.say(); // "张三 在说话" —— this → obj

⚠️ 经典陷阱:引用式赋值

const obj = {
  name: "张三",
  say() {
    console.log(this.name);
  }
};

// 🚨 陷阱!把方法赋值给变量后调用
const fn = obj.say;   // 引用式赋值,丢失了对象上下文
fn();                  // undefined(严格模式)/ window.name(非严格模式)

// ✅ 修复方式一:使用 bind
const boundFn = obj.say.bind(obj);
boundFn(); // "张三"

// ✅ 修复方式二:使用箭头函数包裹
const wrappedFn = () => obj.say();
wrappedFn(); // "张三"

为什么会丢失? 因为 fn 现在是"普通函数调用"了,它不再是通过 obj.xxx() 的形式调用的,所以 this 不再指向 obj。

规则三:作为构造函数调用 → this 指向实例对象
function Person(name) {
  this.name = name;  // this → 新创建的实例对象
  this.greet = function() {
    console.log(`你好,我是 ${this.name}`);
  };
}

const p1 = new Person("张三");
const p2 = new Person("李四");

p1.greet(); // "你好,我是 张三"
p2.greet(); // "你好,我是 李四"
规则四:作为事件处理函数 → this 指向事件触发元素
const button = document.querySelector('.submit-btn');

button.addEventListener('click', function(e) {
  console.log(this);       // <button> 元素本身
  console.log(e.target);   // 实际触发事件的元素(可能有区别)
  console.log(e.currentTarget); // 绑定事件的元素 === this
});
规则五:手动指定 this —— call / apply / bind

这是 JavaScript 提供给开发者最灵活的能力——我们可以手动指定 this 的指向

const person = {
  name: "张三",
  speak(a, b) {
    console.log(`${this.name}: ${a}, ${b}`);
  }
};

const another = { name: "王五" };

// 🔹 call —— 逐个传参,立即执行
person.speak.call(another, "你好", "世界");
// → "王五: 你好, 世界"

// 🔹 apply —— 数组传参,立即执行
person.speak.apply(another, ["你好", "世界"]);
// → "王五: 你好, 世界"

// 🔹 bind —— 不立即执行,返回一个新函数
const boundSpeak = person.speak.bind(another);
// 此时还没有执行!boundSpeak 是一个新函数
boundSpeak("你好", "世界");
// → "王五: 你好, 世界"

三者的核心区别:

callapplybind
执行时机立即执行立即执行返回新函数,不立即执行
传参方式逐个传入数组/类数组逐个传入(可分次柯里化)
最常用场景借用方法可变参数函数事件绑定、回调函数

实际场景 —— 为什么事件绑定要用 bind?

// 场景:在事件处理函数中需要访问特定对象
const controller = {
  name: "表单控制器",
  handleSubmit(e) {
    e.preventDefault(); // 阻止表单默认提交
    console.log(`${this.name} 处理了提交`);
  }
};

// ❌ 错误 —— 事件触发时 this 指向按钮元素,而非 controller
form.addEventListener('submit', controller.handleSubmit);

// ✅ 正确 —— bind 让 this 永远指向 controller
form.addEventListener('submit', controller.handleSubmit.bind(controller));

为什么不用 call/apply? 因为事件处理函数需要异步执行(等用户触发了才执行),而 call/apply 会立即执行函数。只有 bind 返回一个新函数,"等"着被调用。

3.2 规则六:箭头函数 —— 没有自己的 this 🏹

箭头函数是 ES6 引入的特殊函数,它没有自己的 this。箭头函数中的 this 由外层作用域决定(词法作用域)。

const obj = {
  name: "张三",
  say() {
    // 普通函数:this → obj
    console.log(`外层 this.name = ${this.name}`);

    // setTimeout 的回调是普通函数
    // this 本应指向全局对象(或被 bind 修改)
    setTimeout(function() {
      console.log(`普通函数 this.name = ${this.name}`);
      // 非严格模式:undefined 或 ""
      // 严格模式:报错 Cannot read properties of undefined
    }, 1000);

    // ✅ 箭头函数:this 继承自 say() 的 this
    setTimeout(() => {
      console.log(`箭头函数 this.name = ${this.name}`);
      // → "张三" —— 完美!
    }, 1000);
  }
};

obj.say();

图解箭头函数的 this 查找链:

箭头函数本身没有 this
      │
      ▼
沿着作用域链向外查找
      │
      ▼
找到 say() 方法 —— this → obj
      │
      ▼
✅ 箭头函数中的 this === obj

四种常见解决方案对比(解决 setTimeout 中 this 丢失问题):

const obj = {
  name: "张三",
  say() {
    // 方案 1️⃣:箭头函数(最推荐 ✅)
    setTimeout(() => {
      console.log(this.name); // "张三"
    }, 1000);

    // 方案 2️⃣:bind(传统方案)
    setTimeout(function() {
      console.log(this.name); // "张三"
    }.bind(this), 1000);

    // 方案 3️⃣:保存 this 引用(古老方案)
    const self = this;
    setTimeout(function() {
      console.log(self.name); // "张三"
    }, 1000);

    // 方案 4️⃣:使用闭包(不常见)
    setTimeout((function(ctx) {
      return function() {
        console.log(ctx.name); // "张三"
      };
    })(this), 1000);
  }
};

四、Form 表单:从传统到现代 📝

4.1 传统表单提交 —— "远古"方式

<!-- 传统方式:点击 submit 按钮 → 向 action 地址提交 → 页面刷新 😫 -->
<form action="https://api.example.com/submit" method="POST">
  <input type="text" name="username" />
  <input type="submit" value="提交" />
</form>

问题:

  • 🔄 会刷新整个页面,用户体验极差(SPA 时代的噩梦)
  • 📊 无法做前端验证和中间处理
  • 🎯 用户看不到提交中和提交后的状态变化

4.2 现代方式 —— AJAX / Fetch

"use strict";

const oForm = document.querySelector('.add-items');

oForm.addEventListener('submit', async function(e) {
  // 🛑 第一步:阻止默认提交行为
  e.preventDefault();

  // 📊 第二步:收集表单数据
  const formData = new FormData(this);
  const data = Object.fromEntries(formData.entries());
  console.log('收集到的数据:', data);

  // ✅ 第三步:前端验证
  if (!data.item || data.item.trim() === '') {
    alert('请输入内容!');
    return;
  }

  // 🌐 第四步:通过 fetch/ajax 提交(不刷新页面)
  try {
    const response = await fetch('/api/items', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (response.ok) {
      // 🎉 第五步:更新 UI 反馈给用户
      addToList(data.item);
      this.reset(); // 清空表单
    }
  } catch (error) {
    console.error('提交失败:', error);
  }
});

核心原则: e.preventDefault() 是第一步要做的事,阻止浏览器的默认刷新行为。然后一切交给 JavaScript 来控制——这就是 事件驱动 的编程思想。


五、事件驱动编程思想 ⚡

JavaScript 在浏览器中的执行模型是事件驱动的:

// 整个流程就是:等待事件 → 响应事件 → 等待下一个事件
// 所有的用户交互都是"事件"

// 注册事件监听器 = "订阅"某个事件
element.addEventListener('事件类型', 处理函数);

// 当事件发生时,浏览器调用你的处理函数
// 这就是"事件驱动"——代码的执行由事件触发,而非线性顺序执行

事件处理函数中的 this 总览:

事件绑定方式this 指向
el.addEventListener('click', fn)触发事件的元素(el)
el.onclick = fn触发事件的元素(el)
el.addEventListener('click', fn.bind(obj))obj(手动指定 ✅)
el.addEventListener('click', () => {...})外层作用域的 this
HTML 内联 onclick="fn()"window(⚠️ 避免使用)

六、完整的 this 绑定规则速查表 🎯

┌──────────────────────────────────────────────────────────────┐
│               JavaScript this 绑定规则总览                    │
├─────────────────────┬────────────────────────────────────────┤
│ 调用方式             │             this 指向                  │
├─────────────────────┼────────────────────────────────────────┤
│ 普通函数调用          │  window(非严格)/ undefined(严格)   │
│ fn()                │                                        │
├─────────────────────┼────────────────────────────────────────┤
│ 对象方法调用          │  调用该方法的对象                      │
│ obj.fn()            │                                        │
├─────────────────────┼────────────────────────────────────────┤
│ 构造函数调用          │  新创建的实例对象                      │
│ new Fn()            │                                        │
├─────────────────────┼────────────────────────────────────────┤
│ 事件处理函数          │  事件绑定的 DOM 元素                   │
│ el.onclick = fn     │                                        │
├─────────────────────┼────────────────────────────────────────┤
│ call / apply 调用    │  手动指定的对象                        │
│ fn.call(obj)        │                                        │
├─────────────────────┼────────────────────────────────────────┤
│ bind 绑定            │  永久绑定到指定对象                     │
│ fn.bind(obj)()      │                                        │
├─────────────────────┼────────────────────────────────────────┤
│ 箭头函数调用          │  定义时外层作用域的 this               │
│ () => {}            │  (没有自己的 this)                    │
└─────────────────────┴────────────────────────────────────────┘

优先级(从高到低):

1. new 绑定(构造函数)
2. 显式绑定(call / apply / bind)
3. 隐式绑定(对象方法调用)
4. 默认绑定(普通函数调用)
注:箭头函数的 this 在定义时确定,上述规则对其无效

七、综合实战 Demo 🏗️

接下来我们把上面所有知识串联起来,构建一个完整的、带 LocalStorage 持久化的待办事项应用:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>🍢 TAPAS 待办清单</title>
  <style>
    :root {
      --yellow: #ffc600;
      --black: #272727;
      --gray: #f1f1f1;
    }

    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: var(--yellow);
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
    }

    .wrapper {
      background: white;
      border-radius: 16px;
      padding: 40px;
      width: 400px;
      box-shadow: 0 10px 40px rgba(0,0,0,0.15);
    }

    h2 {
      text-align: center;
      font-size: 28px;
      margin-bottom: 8px;
      color: var(--black);
    }

    .subtitle {
      text-align: center;
      color: #666;
      margin-bottom: 24px;
      font-size: 14px;
    }

    .plates {
      list-style: none;
      margin-bottom: 20px;
    }

    .plates li {
      display: flex;
      align-items: center;
      padding: 12px 0;
      border-bottom: 1px solid var(--gray);
    }

    .plates input[type="checkbox"] {
      display: none;
    }

    .plates input[type="checkbox"] + label {
      position: relative;
      padding-left: 32px;
      cursor: pointer;
      flex: 1;
      font-size: 16px;
    }

    .plates input[type="checkbox"] + label::before {
      content: '';
      position: absolute;
      left: 0; top: 50%;
      transform: translateY(-50%);
      width: 20px; height: 20px;
      border: 2px solid #ddd;
      border-radius: 4px;
      transition: all 0.2s;
    }

    .plates input[type="checkbox"]:checked + label {
      text-decoration: line-through;
      color: #bbb;
    }

    .plates input[type="checkbox"]:checked + label::before {
      background: var(--yellow);
      border-color: var(--yellow);
      content: '✓';
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 14px;
    }

    .add-items {
      display: flex;
      gap: 8px;
    }

    .add-items input[type="text"] {
      flex: 1;
      padding: 12px 16px;
      border: 2px solid #eee;
      border-radius: 8px;
      font-size: 16px;
      outline: none;
      transition: border-color 0.2s;
    }

    .add-items input[type="text"]:focus {
      border-color: var(--yellow);
    }

    .add-items input[type="submit"] {
      padding: 12px 20px;
      background: var(--yellow);
      border: none;
      border-radius: 8px;
      font-size: 16px;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.1s;
    }

    .add-items input[type="submit"]:active {
      transform: scale(0.96);
    }

    .clear-btn {
      display: block;
      width: 100%;
      margin-top: 12px;
      padding: 10px;
      background: transparent;
      border: 2px solid #eee;
      border-radius: 8px;
      cursor: pointer;
      font-size: 14px;
      color: #999;
      transition: all 0.2s;
    }

    .clear-btn:hover {
      border-color: #ff6b6b;
      color: #ff6b6b;
    }
  </style>
</head>
<body>
  <div class="wrapper">
    <h2>🍢 Local TAPAS</h2>
    <p class="subtitle">记录你想尝试的美食</p>

    <!-- 📋 列表渲染区 -->
    <ul class="plates">
      <li>加载中...</li>
    </ul>

    <!-- 📝 添加表单 -->
    <form class="add-items">
      <input type="text" name="item" placeholder="添加一道小吃..." required>
      <input type="submit" value="+ 添加">
    </form>

    <button class="clear-btn">清空全部</button>
  </div>

  <script>
  "use strict";

  // ==========================================
  // 🔧 工具函数:LocalStorage 读写
  // ==========================================
  const STORAGE_KEY = 'tapas_items';

  function loadItems() {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : [];
  }

  function saveItems(items) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
  }

  // ==========================================
  // 📊 应用状态
  // ==========================================
  let items = loadItems();
  const platesList = document.querySelector('.plates');
  const addForm = document.querySelector('.add-items');
  const clearBtn = document.querySelector('.clear-btn');

  // ==========================================
  // 🖼️ 渲染函数
  // ==========================================
  function render() {
    if (items.length === 0) {
      platesList.innerHTML = '<li style="color:#bbb;text-align:center;">还没有添加任何小吃 🍽️</li>';
      return;
    }

    platesList.innerHTML = items.map((item, i) => `
      <li>
        <input type="checkbox"
               data-index="${i}"
               id="item${i}"
               ${item.done ? 'checked' : ''} />
        <label for="item${i}">${escapeHtml(item.text)}</label>
      </li>
    `).join('');
  }

  // 🔒 防止 XSS
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }

  // ==========================================
  // 📝 添加项 —— 事件处理函数
  // ==========================================
  function handleAdd(e) {
    e.preventDefault(); // 🛑 阻止表单默认提交(刷新页面)

    // this → 表单元素(因为是通过 addEventListener 绑定的)
    const input = this.querySelector('[name="item"]');
    const text = input.value.trim();

    if (!text) return;

    items.push({ text, done: false });
    saveItems(items);
    render();
    this.reset(); // 清空输入框
  }

  // ==========================================
  // ✅ 切换完成状态 —— 事件委托
  // ==========================================
  function handleToggle(e) {
    // 只处理 checkbox 的点击
    if (!e.target.matches('input[type="checkbox"]')) return;

    const index = e.target.dataset.index;
    items[index].done = !items[index].done;
    saveItems(items);
    render();
  }

  // ==========================================
  // 🗑️ 清空全部
  // ==========================================
  function handleClear() {
    if (items.length === 0) return;
    if (confirm('确定要清空全部吗?')) {
      items = [];
      saveItems(items);
      render();
    }
  }

  // ==========================================
  // 🎧 事件绑定(演示 this 的各种绑定方式)
  // ==========================================

  // 方式 1:普通函数绑定 —— this 指向事件触发元素
  addForm.addEventListener('submit', handleAdd);

  // 方式 2:事件委托 —— 在父元素上监听子元素的事件
  platesList.addEventListener('click', handleToggle);

  // 方式 3:箭头函数(this 继承自外层作用域)
  clearBtn.addEventListener('click', () => {
    // 这里 this 不是 clearBtn!它继承自外层(此处为 window/undefined)
    // 但我们不需要 this,直接调用 handleClear 即可
    handleClear();
  });

  // ==========================================
  // 🚀 初始化渲染
  // ==========================================
  render();

  console.log('🍢 Local TAPAS 已就绪!');
  console.log(`   当前存储了 ${items.length} 条记录`);

  // ==========================================
  // 🧪 代码末尾:this 绑定演示
  // ==========================================
  const demoObj = {
    name: "张三",
    say() { console.log(`say → ${this.name}`); },
    speak(a, b) {
      console.log(`speak → ${a}, ${b} | this.name = ${this.name}`);
    }
  };

  const anotherObj = { name: "王五" };

  // 对象方法调用 —— this → demoObj
  demoObj.say();  // "张三"

  // call 手动指定 this
  demoObj.say.call(anotherObj);  // "王五"

  // apply 手动指定 this(数组传参)
  demoObj.speak.apply(anotherObj, ["早上好", "吃饭了吗"]);

  // bind 不立即执行,返回新函数
  const boundSay = demoObj.say.bind(anotherObj);
  console.log("bind 返回的是:", boundSay);
  boundSay();  // "王五"
  </script>
</body>
</html>

知识点回顾 📚

通过这个 Demo,你练习了:

知识点对应代码位置
e.preventDefault() 阻止默认行为handleAdd 函数第一行
事件驱动的编程思想addEventListener 注册和回调
this 指向事件触发元素handleAddthis.querySelector
事件委托(Event Delegation)platesList 上监听 checkbox 点击
LocalStorage 的 CRUDloadItems / saveItems 函数
JSON 序列化/反序列化JSON.parse / JSON.stringify
严格模式 "use strict"脚本第一行
箭头函数的作用域clearBtn 的事件绑定
call / apply / bind 区别末尾的 demoObj 演示代码

八、常见面试题速答 💬

Q1:localStorage 和 sessionStorage 的区别?

  • 生命周期: localStorage 永久存储(除非手动删除);sessionStorage 在标签页关闭时自动清除。
  • 作用域: localStorage 同源共享;sessionStorage 同源 + 同标签页隔离(不同标签页有各自独立的 sessionStorage)。
  • API: 两者完全相同(getItemsetItemremoveItemclear)。

Q2:Cookie 的缺点是什么?

  1. 容量极小(~4KB)
  2. 每次 HTTP 请求都自动携带,增加请求体积
  3. 原生 API 不友好(document.cookie 是字符串,需手动解析)
  4. 存在 CSRF 安全风险

Q3:call、apply、bind 的区别?

  • call(obj, arg1, arg2, ...) —— 逐个传参,立即执行
  • apply(obj, [arg1, arg2, ...]) —— 数组传参,立即执行
  • bind(obj, arg1, arg2, ...) —— 返回新函数,不立即执行

Q4:箭头函数和普通函数的 this 有什么区别?

  • 普通函数:this 在调用时决定,谁调用指向谁
  • 箭头函数:this 在定义时决定,继承自外层作用域(没有自己的 this)

Q5:为什么事件绑定回调要用 bind 而不是 call/apply?

因为事件回调是异步的(用户触发时才执行),call/apply 会立即执行函数。bind 返回一个新函数,可以"等"着被调用。


九、总结与延伸 🎯

本文核心梳理

                     ┌─────────────┐
                     │  存储体系    │
                     └──────┬──────┘
                            │
          ┌─────────────────┼──────────────────┐
          ▼                 ▼                   ▼
    ┌──────────┐     ┌────────────┐     ┌──────────────┐
    │ 浏览器端  │     │  服务端     │     │   缓存层      │
    │ Storage  │     │  MySQL     │     │   Redis      │
    │ Cookie   │     │  MongoDB   │     │   LLM向量    │
    └──────────┘     └────────────┘     └──────────────┘

                     ┌─────────────┐
                     │ this 绑定    │
                     └──────┬──────┘
                            │
       ┌────────────────────┼──────────────────────┐
       ▼                    ▼                       ▼
  默认绑定           隐式绑定                   显式绑定
  fn() → window     obj.fn() → obj          call/apply/bind
  (严格→undefined)                           (手动指定)
       │                    │                       │
       └────────────────────┼───────────────────────┘
                            │
                            ▼
                    ┌──────────────┐
                    │  箭头函数      │
                    │  无自己的this  │
                    │  继承外层作用域 │
                    └──────────────┘

推荐进一步学习


📝 一点心得: 前端存储不是一个孤立的主题,它和 HTTP 缓存、浏览器渲染、性能优化、安全策略都息息相关。而 this 指向看起来是语法细节,实际上决定了你的代码在运行时的行为和正确性。理解底层原理,才能写出稳健的代码。希望这篇文章能帮你建立起完整的知识体系!