深入理解 JavaScript 中的 this 与数据存储的奥秘
从浏览器缓存到函数调用,一次搞懂前端核心概念
作为一名前端开发者,我们每天都要面对两个绕不开的话题:数据存储 和 this 指向。它们看似毫不相关,却在日常开发中频繁交织。今天,就让我们从存储技术切入,再到 JavaScript 中让人头疼的 this,一步步揭开它们的神秘面纱。
一、数据存储的多重宇宙
数据存储从来不是一个单一的概念,它就像一座分层的大厦,每一层都有自己的使命。
1.1 持久化存储:MySQL 与关系型数据库
当我们需要永久保存数据时,MySQL 这类关系型数据库是首选。它以表格形式组织数据,通过 SQL 语言进行增删改查,保证了数据的一致性 和持久性。用户注册信息、订单记录、文章内容......这些需要长期保存的数据,最终都会落入 MySQL 的"怀抱"。
sql
-- 典型的 MySQL 查询
SELECT * FROM articles WHERE user_id = 123;
但 MySQL 也有它的"软肋"------每次查询都需要磁盘 I/O 和复杂的解析执行,当并发量上来后,性能瓶颈就会出现。
1.2 缓存层:Redis 的 KV 哲学
为了弥补 MySQL 的性能短板,Redis 应运而生。它基于内存运行,采用键值对(Key-Value)存储,读写速度比 MySQL 快数个数量级。
第一次请求:MySQL 查询 → 结果存入 Redis
后续请求:直接走 Redis → 无需再查 MySQL
这种"先读缓存,缓存未命中再查数据库"的模式,是现代高并发系统的标配。Redis 就像 MySQL 的"加速器",让数据访问变得飞快。
1.3 浏览器缓存与本地存储
当数据来到前端,存储形态又发生了变化:
- 浏览器缓存(HTTP Cache):让页面二次打开时"秒开",资源文件(CSS、JS、图片)被缓存下来,无需重新下载。
- LocalStorage / SessionStorage:以键值对形式存储在浏览器中,容量约 5-10MB,适合存储用户偏好、主题设置等轻量数据。
- Cookie:容量小(约 4KB),但会自动携带在请求头中,常用于身份认证。
1.4 前沿阵地:LLM 与 Embedding 存储
在 AI 时代,存储的边界被进一步拓展。大语言模型(LLM)使用 Embedding 向量来存储文本的语义信息,通过向量数据库(如 Pinecone、Milvus)进行相似度检索。这是一种全新的存储范式------数据智能。
存储技术的发展,从 MySQL 到 Redis,从 LocalStorage 到向量数据库,本质是在速度、容量、持久性、智能性之间寻找平衡。
二、前端表单:从默认提交到 Ajax 进化
让我们回到前端开发的日常场景------表单。
在 index.html 中,我们有一个经典的 form 结构:
html
<form class="add-items">
<input type="text" name="item" placeholder="Add a new tapas">
<input type="submit" value="+ Add Item">
</form>
当用户点击 type="submit" 的按钮时,表单会触发 submit 事件,浏览器会执行默认行为 :向 action 属性指定的地址发送请求,并刷新页面。
这种做法在传统 Web 应用中很常见,但在现代前端开发中,我们更倾向于用 JavaScript 接管提交逻辑,以提供更流畅的用户体验。
javascript
const oForm = document.querySelector('.add-items');
function addItem(e) {
console.log(e);
console.log(this);
// 阻止提交默认行为 ------ 不刷新页面
e.preventDefault();
// 这里可以写 fetch / ajax 逻辑
}
// 监听 submit 事件
oForm.addEventListener('submit', addItem);
通过 e.preventDefault() 阻止默认的页面刷新,然后用 fetch 或 XMLHttpRequest 异步提交数据,这就是 Ajax 的核心思想。页面不再刷新,用户感知到的只有数据的局部更新,体验大大提升。
三、this:函数运行时的"灵魂"
如果说 JavaScript 中哪个概念最让人困惑,this 一定名列前茅。很多人把 this 理解为"函数声明时"确定的,这是错误的。正确的理解是:
this 在函数运行时确定,指向函数的调用者。
我们来看一段代码(来自 2.html):
javascript
var name = "用户A";
let obj = {
name: "用户B",
say: function() {
console.log(this.name);
}
}
obj.say(); // 输出:用户B
这里 say 作为对象 obj 的方法被调用,this 指向 obj,所以输出 "用户B"。
但如果把函数引用赋值给一个变量再调用呢?
javascript
const fn = obj.say;
fn(); // 输出:用户A(在非严格模式下)
这时 fn 作为普通函数被调用,this 指向全局对象 window,所以输出 "用户A"。但如果在 common.js 中我们开启了严格模式:
javascript
"use strict";
// 严格模式下,普通函数的 this 为 undefined
这就是 严格模式 的意义之一:减少隐式的全局污染。
3.1 this 的五种场景
| 场景 | this 指向 |
|---|---|
| 普通函数调用(非严格模式) | window / global |
| 普通函数调用(严格模式) | undefined |
| 对象方法调用 | 调用该方法的对象 |
| 构造函数调用(new) | 新创建的实例对象 |
| 事件处理函数 | 触发事件的 DOM 元素 |
3.2 事件处理函数中的 this
在 common.js 中,我们给链接绑定了点击事件:
javascript
document.querySelector('.lnk')
.addEventListener('click', goBaidu);
function goBaidu(e) {
console.log(this); // 指向 <a class="lnk" href="..."> 元素
e.preventDefault();
}
作为事件处理函数,this 默认指向触发事件的 DOM 元素,这与普通函数的调用方式不同。
四、手动指定 this:call / apply / bind
很多时候,我们并不想让 this 由调用方式决定,而是手动控制 。JavaScript 提供了三个方法:call、apply、bind。
4.1 call:立即调用,逐个传参
javascript
let obj = {
name: "用户C",
speak: function(a, b) {
console.log(a, b);
console.log(this.name);
}
}
let obj2 = { name: "用户D" };
// 使用 call,将 this 指向 obj2
obj.speak.call(obj2, '你好', '我是用户D');
// 输出:你好 我是用户D 用户D
call 的第一个参数是想要绑定的 this 对象,后面的参数依次传递给函数。
4.2 apply:立即调用,数组传参
apply 与 call 几乎相同,区别在于传递参数的方式:
javascript
obj.speak.apply(obj2, ['你好', '我是用户D']);
// 效果与 call 相同,但参数以数组形式传递
当参数数量不确定或来自数组时,apply 会更加方便。
4.3 bind:延迟调用,返回新函数
bind 与前两者最大的区别是:它不立即执行函数,而是返回一个绑定好 this 的新函数。
javascript
const fn2 = obj.speak.bind(obj2);
console.log(fn2); // 返回一个新函数
fn2('你好', '我是用户D'); // 此时 this 已经绑定为 obj2
这在异步场景中非常有用,比如 setTimeout 或事件监听:
javascript
// common.js 中的示例
const addItemBind = addItem.bind(obj);
oForm.addEventListener('submit', addItemBind);
这里用 bind 将 addItem 函数的 this 永久绑定为 obj,这样在事件触发时,this 就不会指向表单元素,而是指向我们指定的对象。
4.4 异步场景中的 this 陷阱
在 2.html 中,有一段被注释掉的代码:
javascript
setTimeout((function() {
console.log(this.name)
}).bind(this), 1000);
为什么要用 bind(this)?因为在 setTimeout 中,回调函数是作为普通函数被调用的,this 会指向 window(非严格模式)。为了让 this 保持在当前上下文中,我们需要手动绑定。
而箭头函数则天然解决了这个问题:
javascript
setTimeout(() => {
console.log(this.name)
}, 1000);
五、箭头函数:没有 this 的"另类"
箭头函数是 ES6 引入的语法,它没有自己的 this ,而是继承外层作用域的 this。
javascript
var name = "用户A";
let obj = {
name: "用户B",
say: function() {
setTimeout(() => {
console.log(this.name); // 继承 say 函数的 this → obj
}, 1000);
}
}
obj.say(); // 1秒后输出:用户B
在 say 方法中,箭头函数的 this 沿用了外层 say 函数的 this(即 obj),所以输出 "用户B"。
如果用普通函数:
javascript
setTimeout(function() {
console.log(this.name); // 普通函数,this 指向 window
}, 1000);
// 输出:用户A(非严格模式)
这就是箭头函数在异步回调中广受欢迎的原因------它帮我们绕过了 this 的动态绑定问题。
但要注意,箭头函数不能作为构造函数(不能使用 new),也不能使用 call、apply、bind 改变其 this(因为根本没有自己的 this)。
六、综合思考:从表单提交到 this 绑定
让我们把这些知识串联起来,看一个完整的场景:
在 common.js 中,我们使用 bind 将 addItem 的 this 绑定为 obj:
javascript
const oForm = document.querySelector('.add-items');
let obj = {
name: "用户C",
// ...
}
const addItemBind = addItem.bind(obj);
oForm.addEventListener('submit', addItemBind);
为什么要这样做?因为事件监听函数默认的 this 指向 DOM 元素(oForm),但我们希望在 addItem 函数中能访问到 obj 的数据或方法。通过 bind,我们既保留了事件对象 e,又让 this 指向了我们想要的对象。
如果使用 call 或 apply,它们会立即执行函数,而 addEventListener 需要的是一个函数引用(在事件触发时被调用),所以 bind 是最合适的选择。
七、总结
| 概念 | 核心要点 |
|---|---|
| 数据存储 | MySQL(持久化)→ Redis(缓存加速)→ LocalStorage(浏览器存储)→ Embedding(AI 存储),各有适用场景 |
| 表单提交 | 默认提交会刷新页面,用 e.preventDefault() + Ajax 实现无刷新提交 |
| this 指向 | 函数运行时确定,取决于调用方式(普通调用、方法调用、构造函数、事件处理) |
| call / apply | 立即执行,手动指定 this,区别在于传参方式(逐个 vs 数组) |
| bind | 返回新函数,延迟执行,适合事件监听和异步回调 |
| 箭头函数 | 没有自己的 this,继承外层作用域,简化代码但使用场景受限 |
最后思考 :理解 this 的关键,不是背诵规则,而是理解"函数被调用时,谁在调用它"。而理解存储的关键,是明白"数据在何时、以何种方式被访问"。这两者看似不同,但都指向同一个核心------对上下文(Context)的深刻理解。
希望这篇文章能帮你理清这些概念,在开发中更加得心应手。如果有什么疑问,欢迎在评论区交流讨论!
本文所有示例代码均可在附带的 HTML 文件中运行验证。