前端存储与 this 指向完全指南:从 LocalStorage 实战到 call/apply/bind 深度解析
this 是 JS 中最让人困惑的概念之一。普通函数、对象方法、构造函数、事件处理、箭头函数------每种场景下 this 的指向都不同。本文从一套完整的 LocalStorage 表单项目出发,逐层拆解 this 的 7 种指向规则,深度对比 call/apply/bind 的异同,同时梳理前端存储的全景体系。
前言
当你写 obj.say() 时,this 指向 obj;当你把 obj.say 赋值给变量再调用时,this 却指向了 window------这种"看似相同、实则不同"的行为,让无数前端开发者踩坑。
本文不讲抽象理论,而是从一个完整的 LocalStorage 表单项目出发,在真实的 DOM 操作和事件绑定场景中,彻底搞懂 this 的所有指向规则。同时,我们还会梳理前端存储的完整技术栈,从浏览器本地存储到 Redis 缓存,再到 LLM 的 Embedding 存储。
一、前端存储全景:数据存在哪里?
在深入 this 之前,先建立存储的全局认知。前端开发中,数据可以存储在不同层级的介质中:
arduino
┌─────────────────────────────────────────────────────────────┐
│ 前端存储全景图 │
├──────────────────┬──────────────────────────────────────────┤
│ 层级 │ 技术方案 │
├──────────────────┼──────────────────────────────────────────┤
│ 云端持久化 │ MySQL(关系型数据库) │
│ │ MongoDB(文档数据库) │
│ │ 云盘 / OSS 对象存储 │
├──────────────────┼──────────────────────────────────────────┤
│ 服务端缓存 │ Redis(KV 缓存,减轻 MySQL 压力) │
│ │ Memcached │
├──────────────────┼──────────────────────────────────────────┤
│ 浏览器端存储 │ LocalStorage(持久化,5~10MB) │
│ │ SessionStorage(会话级,关闭标签页即消失) │
│ │ IndexedDB(结构化大数据,几百 MB) │
│ │ Cookie(4KB,常用来存 Token) │
├──────────────────┼──────────────────────────────────────────┤
│ 应用内存 │ 变量 / 闭包 / Vuex / Redux State │
├──────────────────┼──────────────────────────────────────────┤
│ AI 语义存储 │ Embedding 向量(text-embedding-v4 等) │
│ │ 向量数据库(Milvus、pgvector) │
└──────────────────┴──────────────────────────────────────────┘
1.1 Redis 缓存的作用
markdown
用户请求文章列表
│
├── 第一次 → 查 MySQL → 结果存入 Redis → 返回给用户
│
└── 第二次 → 查 Redis → 命中缓存 → 直接返回
(不查 MySQL,性能提升 10~100 倍)
Redis 的核心价值:用内存换时间。MySQL 查询有磁盘 I/O 瓶颈,Redis 直接在内存中查 KV,速度快得多。
1.2 LLM 的 Embedding 存储
这是 AI 时代的新型"存储"------不是存文字,而是存语义向量。将文章、图片转化为高维向量后存入向量数据库,就能实现语义搜索、RAG 检索增强生成等智能应用。
二、LocalStorage 实战:一个完整的表单项目
2.1 项目结构
perl
local-storage-demo/
├── index.html # 页面结构
├── common.css # 样式
└── common.js # 核心逻辑(this + 事件 + 存储)
2.2 HTML 结构
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LocalStorage</title>
<link rel="stylesheet" href="./common.css">
</head>
<body>
<div class="wrapper">
<h2>LOCAL TAPAS</h2>
<ul class="plates">
<li>Loading Tapas...</li>
</ul>
<!-- form 表单:收集用户输入 -->
<form class="add-items">
<input type="text" name="item" placeholder="Add a new tapas">
<input type="submit" value="+ Add Item">
</form>
<a href="https://www.baidu.com" class="lnk">去百度</a>
</div>
<script src="./common.js?v=11"></script>
</body>
</html>
2.3 为什么不用 form 的默认提交?
传统的 form 提交会刷新整个页面,用户体验极差。现代前端的做法是:
less
form 默认提交(不用)
→ 页面刷新 → 体验差
JS 拦截提交(正确做法)
→ addEventListener('submit', handler)
→ e.preventDefault() 阻止默认行为
→ JS 读取 input 值
→ 存入 LocalStorage
→ 动态更新 DOM(无刷新)
三、this 指向的七种规则
核心原则:this 在函数运行时确定,不是声明时确定。
3.1 规则总览
| 场景 | this 指向 | 代码示例 |
|---|---|---|
| 普通函数调用 | window(非严格)/ undefined(严格) |
fn() |
| 对象方法调用 | 调用该方法的对象 | obj.say() |
| 引用式赋值后调用 | window / undefined |
const fn = obj.say; fn() |
| 构造函数调用 | 新创建的实例对象 | new Person() |
| 事件处理函数 | 触发事件的 DOM 元素 | btn.addEventListener('click', fn) |
| call / apply / bind | 手动指定的第一个参数 | fn.call(obj) |
| 箭头函数 | 外层作用域的 this(没有自己的 this) | () => { this.xxx } |
3.2 规则一:普通函数调用
javascript
"use strict"; // 启用严格模式
var name = "王五"; // 严格模式下,var 不会挂载到 window
function say() {
console.log(this); // undefined
}
say(); // 普通函数调用,this → undefined(严格模式)
在 "use strict" 严格模式下:
var声明的全局变量不会自动挂载到 window 对象上- 普通函数调用时,
this指向undefined而非window
| 模式 | var name = 'a' |
普通函数 this |
|---|---|---|
| 非严格模式 | window.name === 'a' |
window |
| 严格模式 | window.name === undefined |
undefined |
3.3 规则二:对象方法调用
javascript
let obj = {
name: "张三",
say: function () {
console.log(this.name); // "张三"
}
};
obj.say(); // this → obj(调用者)
3.4 规则三:引用式赋值的陷阱
这是面试中最爱考的 this 指向题:
javascript
let obj = {
name: "张三",
say: function () {
console.log(this.name);
}
};
obj.say(); // "张三" ✓ 对象方法调用
const fn = obj.say; // 引用式赋值:只复制了函数,丢失了调用上下文
fn(); // undefined(严格模式)/ "王五"(非严格模式)
// this 变成了普通函数调用
关键理解 :obj.say 只是获取了函数的引用,赋值给 fn 后,fn 和 obj 之间没有任何关系了。调用 fn() 时就是普通函数调用。
3.5 规则四:构造函数调用
javascript
function Person(name) {
this.name = name; // this → 新创建的实例
}
const p = new Person("李四");
console.log(p.name); // "李四"
3.6 规则五:事件处理函数
javascript
const oForm = document.querySelector('.add-items');
function addItem(e) {
console.log(this); // this → oForm(触发表单提交的元素)
e.preventDefault(); // 阻止默认刷新行为
}
oForm.addEventListener('submit', addItem);
事件处理函数的 this 默认指向触发事件的 DOM 元素。
3.7 规则六:call / apply / bind 手动指定
这是改变 this 指向的三剑客:
javascript
let obj = {
name: "张三",
say: function () {
console.log(this.name);
},
speak: function (a, b) {
console.log(a, b, this.name);
}
};
let obj2 = { name: "李四" };
// call:立即执行,参数逐个传递
obj.say.call(obj2); // "李四"
obj.speak.call(obj2, '你好', '李四'); // "你好" "李四" "李四"
// apply:立即执行,参数以数组形式传递
obj.speak.apply(obj2, ['你好', '我是李四']); // "你好" "我是李四" "李四"
// bind:不立即执行,返回一个新函数(this 永久绑定)
const fn2 = obj.speak.bind(obj2);
fn2('你好', '我是李四'); // "你好" "我是李四" "李四"
三者对比表:
| 方法 | 执行时机 | 参数形式 | 返回值 | 使用场景 |
|---|---|---|---|---|
call |
立即执行 | 散列:call(obj, a, b) |
函数返回值 | 临时改变 this,参数已知 |
apply |
立即执行 | 数组:apply(obj, [a, b]) |
函数返回值 | 临时改变 this,参数是数组 |
bind |
不执行,返回新函数 | 散列:bind(obj, a, b) |
新函数 | 需要延迟执行、永久绑定 this |
3.8 规则七:箭头函数没有自己的 this
箭头函数是 ES6 引入的语法糖,它没有自己的 this,会捕获外层作用域的 this。
javascript
let obj = {
name: "姆巴佩",
say: function() {
console.log(this.name); // "姆巴佩"
// 普通函数 + setTimeout
setTimeout(function() {
console.log(this.name); // undefined(this 指向 window/undefined)
}, 1000);
// 箭头函数 + setTimeout
setTimeout(() => {
console.log(this.name); // "姆巴佩" ✓ 捕获外层 obj 的 this
}, 1000);
}
};
obj.say();
为什么箭头函数能"记住" this?
javascript
普通函数:
setTimeout(function() {...}, 1000)
→ 回调函数独立执行
→ this 按普通函数规则重新判定 → window/undefined
箭头函数:
setTimeout(() => {...}, 1000)
→ 没有自己的 this
→ 向上查找,找到外层 say() 的 this → obj
→ "记住"了 obj
四、实战:事件绑定中的 this 控制
4.1 问题场景
在表单提交事件中,我们希望 this 指向某个业务对象(如 obj),而不是默认的 DOM 元素:
javascript
"use strict";
let obj = {
name: "张三",
say: function () {
console.log(this.name);
}
};
const oForm = document.querySelector('.add-items');
function addItem(e) {
console.log(this); // 默认指向 oForm,但我们想让它指向 obj
e.preventDefault();
}
// ❌ 默认绑定:this → oForm
oForm.addEventListener('submit', addItem);
// ✅ bind:this → obj(永久绑定,返回新函数)
const addItemBind = addItem.bind(obj);
oForm.addEventListener('submit', addItemBind);
4.2 为什么不用 call/apply?
javascript
// ❌ call 立即执行,事件还没触发呢!
oForm.addEventListener('submit', addItem.call(obj));
// 这行代码执行时,addItem 就立即运行了,而不是等 submit 事件
// ✅ bind 返回新函数,等事件触发时才执行
oForm.addEventListener('submit', addItem.bind(obj));
事件监听器的回调必须是函数引用 ,而 call/apply 会立即执行函数,返回的是执行结果(不是函数)。只有 bind 返回的是新函数,适合作为事件回调。
4.3 完整代码回顾
javascript
"use strict";
const oForm = document.querySelector('.add-items');
let obj = {
name: "张三",
say: function () {
console.log(this);
console.log(`${this.name}`);
},
speak: function (a, b) {
console.log(a, b);
console.log(this);
}
};
// 核心技巧:用 bind 将 this 指向 obj
const addItemBind = addItem.bind(obj);
oForm.addEventListener('submit', addItemBind);
function addItem(e) {
console.log(e); // 事件对象
console.log(this); // obj(bind 绑定后的结果)
e.preventDefault(); // 阻止表单默认刷新
}
// 链接点击事件
function goBaidu(e) {
console.log(this); // 指向 <a> 元素
e.preventDefault(); // 阻止跳转
}
document.querySelector('.lnk').addEventListener('click', goBaidu);
五、this 指向速查表与踩坑清单
5.1 速查表
php
调用方式 this 指向
─────────────────────────────────────────────────────────
fn() window / undefined
obj.fn() obj
const fn = obj.fn; fn() window / undefined
new Fn() 新实例
btn.addEventListener('click',fn) btn(DOM 元素)
fn.call(obj) obj
fn.apply(obj) obj
fn.bind(obj)() obj
箭头函数 ()=>{} 外层作用域的 this
5.2 面试高频踩坑题
题目 1:
javascript
var name = "全局";
let obj = {
name: "对象",
say: () => {
console.log(this.name);
}
};
obj.say(); // 输出什么?
答案 :"全局"(非严格)或 undefined(严格)。因为箭头函数没有自己的 this,会捕获外层的 window。obj.say() 中的 this 不是 obj。
题目 2:
javascript
let obj = {
name: "姆巴佩",
say: function() {
setTimeout(function() {
console.log(this.name);
}, 1000);
}
};
obj.say(); // 输出什么?
答案 :undefined。setTimeout 的回调是普通函数,独立执行时 this 指向 window/undefined。
修正方案:
javascript
// 方案一:bind
setTimeout(function() {
console.log(this.name);
}.bind(this), 1000);
// 方案二:箭头函数(推荐)
setTimeout(() => {
console.log(this.name); // "姆巴佩"
}, 1000);
5.3 严格模式的影响
javascript
// 非严格模式
var name = "王五";
console.log(window.name); // "王五"(var 挂载到 window)
function fn() { console.log(this); }
fn(); // window
// 严格模式
"use strict";
var name = "王五";
console.log(window.name); // undefined(严格模式下 var 不挂载 window)
function fn() { console.log(this); }
fn(); // undefined
知识树
kotlin
前端存储与 this 指向完全指南
├── 前端存储体系
│ ├── 云端:MySQL / MongoDB / 云盘
│ ├── 服务端缓存:Redis(KV,内存换时间)
│ ├── 浏览器端:LocalStorage / SessionStorage / Cookie / IndexedDB
│ └── AI 语义存储:Embedding 向量 + 向量数据库
├── LocalStorage 表单实战
│ ├── form 默认提交(页面刷新,体验差)
│ └── JS 拦截提交(e.preventDefault + 动态更新 DOM)
├── this 指向七规则
│ ├── ① 普通函数 → window / undefined
│ ├── ② 对象方法 → 调用者对象
│ ├── ③ 引用赋值 → window / undefined(丢失上下文)
│ ├── ④ 构造函数 → 新实例
│ ├── ⑤ 事件处理 → 触发元素(DOM)
│ ├── ⑥ call/apply/bind → 手动指定
│ │ ├── call:立即执行,散列参数
│ │ ├── apply:立即执行,数组参数
│ │ └── bind:返回新函数,永久绑定
│ └── ⑦ 箭头函数 → 外层 this(没有自己的 this)
├── 实战:事件绑定中的 this 控制
│ ├── addEventListener 默认 this → DOM 元素
│ ├── 为什么用 bind 不用 call/apply(事件回调需要函数引用)
│ └── 完整表单提交处理流程
└── 踩坑清单
├── 箭头函数作为对象方法(this 不指向对象)
├── setTimeout 回调中的 this(普通函数丢失上下文)
└── 严格模式下 var 不挂载 window
结语
this 指向的规则看似复杂,但核心只有一句话:看函数是怎么被调用的,而不是怎么被定义的。
- 直接
fn()→ 普通函数,this 是 window/undefined obj.fn()→ 方法调用,this 是 objnew Fn()→ 构造函数,this 是新实例btn.onclick = fn→ 事件处理,this 是 btnfn.call(obj)→ 手动指定,this 是 obj() => {}→ 没有自己的 this,捕获外层
理解这些规则后,再去看项目中的代码------bind 绑定事件回调、call 临时借用方法、箭头函数在 setTimeout 中保持 this------每一处都有了清晰的逻辑支撑。
同时,前端存储的知识体系也为我们打开了更广阔的视野:从浏览器端的 LocalStorage 到服务端的 Redis,再到 AI 时代的 Embedding 向量存储,存储的本质始终是让数据在正确的时间、正确的位置被高效地访问。
搞懂 this,就搞懂了 JavaScript 的运行时机制。理解存储,就理解了系统的数据流架构。二者兼备,前端工程能力再上一个大台阶。
参考与拓展阅读:
- 《你不知道的 JavaScript(上卷)》第 2 章 ------ this 全面解析
- MDN:Function.prototype.call / apply / bind
- MDN:箭头函数(Arrow functions)
- MDN:Window.localStorage API
- 《JavaScript 高级程序设计》第 4 章 ------ 变量、作用域和内存
如果本文帮你理清了 this 的所有指向规则,欢迎点赞 + 收藏。面试前翻一遍,this 指向题不再丢分!有任何疑问,欢迎在评论区交流讨论 👇
#JavaScript #this指向 #前端面试 #LocalStorage #callApplyBind #掘金技术社区