🌐 前端存储全攻略 & 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 浏览器本地存储四剑客
| 特性 | Cookie | LocalStorage | SessionStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | ~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("你好", "世界");
// → "王五: 你好, 世界"
三者的核心区别:
| call | apply | bind | |
|---|---|---|---|
| 执行时机 | 立即执行 | 立即执行 | 返回新函数,不立即执行 |
| 传参方式 | 逐个传入 | 数组/类数组 | 逐个传入(可分次柯里化) |
| 最常用场景 | 借用方法 | 可变参数函数 | 事件绑定、回调函数 |
实际场景 —— 为什么事件绑定要用 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 指向事件触发元素 | handleAdd 中 this.querySelector |
| 事件委托(Event Delegation) | platesList 上监听 checkbox 点击 |
| LocalStorage 的 CRUD | loadItems / saveItems 函数 |
| JSON 序列化/反序列化 | JSON.parse / JSON.stringify |
严格模式 "use strict" | 脚本第一行 |
| 箭头函数的作用域 | clearBtn 的事件绑定 |
| call / apply / bind 区别 | 末尾的 demoObj 演示代码 |
八、常见面试题速答 💬
Q1:localStorage 和 sessionStorage 的区别?
- 生命周期: localStorage 永久存储(除非手动删除);sessionStorage 在标签页关闭时自动清除。
- 作用域: localStorage 同源共享;sessionStorage 同源 + 同标签页隔离(不同标签页有各自独立的 sessionStorage)。
- API: 两者完全相同(
getItem、setItem、removeItem、clear)。
Q2:Cookie 的缺点是什么?
- 容量极小(~4KB)
- 每次 HTTP 请求都自动携带,增加请求体积
- 原生 API 不友好(
document.cookie是字符串,需手动解析) - 存在 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指向看起来是语法细节,实际上决定了你的代码在运行时的行为和正确性。理解底层原理,才能写出稳健的代码。希望这篇文章能帮你建立起完整的知识体系!