你以为你懂 this,其实你只是背下了答案
楔子:从一个"存数据"的日常说起
那天我正在给项目加本地存储功能,产品经理跑过来问:"咱们这个 Tapas 清单,刷新页面数据还在吗?"
"在,用了 localStorage。"我随口答道。
他点点头走了,我却愣在原地------localStorage 存的是字符串,为什么我直接用 JSON.stringify 存数组,取出来却要用 JSON.parse? 这背后是 JS 的引用类型和基本类型区别。
但这不是今天的重点。重点是当我写完这段存储逻辑,加了个表单提交事件,this 指向却出了问题------明明想更新数据,结果 this 指向了 window,数据死活不对。
这就是我们今天要聊的主题:this。
别急着划走,我知道你看过无数篇 this 的文章。但这次不一样,我会用真实可运行的代码 和5 个实际开发场景,带你从"背答案"进化到"真理解"。
对了,细心的你可能会看到我的 HTML 里引用了 common.js?v=11------这个 v=11 是版本号,用于强制浏览器加载最新的 JS 文件,而不是使用缓存中的旧版本 。每次更新代码后,我都会顺手改一下版本号(比如 v=12),这样用户就能立即体验到最新逻辑,不需要手动清除浏览器缓存。这是前端工程化里最朴素也最有效的缓存控制手段。
一、this 的本质:一句话说透
this 是在函数运行时确定的,而不是在函数声明时确定的。
说白了,谁调用我,我就指向谁(箭头函数除外,这个我们后面单独说)。
这句话值 80 分,剩下的 20 分来自各种边界情况。咱们一个个过。
二、五条规则,搞定 90% 的场景
规则一:普通函数调用 → this 指向全局对象
scss
function showThis() {
console.log(this);
}
showThis(); // window (浏览器) / global (Node.js)
但是! 如果你启用了严格模式 ('use strict'),情况就变了:
javascript
'use strict';
function showThis() {
console.log(this);
}
showThis(); // undefined
这就是为什么越来越多的项目默认开启严格模式------避免无意中污染全局对象。
实战踩坑:
javascript
var name = '全局的梅西';
function sayName() {
console.log(this.name);
}
sayName(); // '全局的梅西' (var 声明的变量挂载在 window 上)
如果改用 let 声明:
javascript
let name = '全局的梅西'; // let 不会挂载到 window
function sayName() {
console.log(this.name);
}
sayName(); // undefined
金句 :
var是 window 的"亲儿子",let/const是"养子"------前者继承家产,后者独立门户。
规则二:作为对象方法调用 → this 指向调用对象
这是最直观的场景:
javascript
let obj = {
name: '姆巴佩',
say: function() {
console.log(this.name);
}
};
obj.say(); // '姆巴佩' ✅ this 指向 obj
但这里有个巨坑:引用式赋值
php
const fn = obj.say; // 把方法"拿出来"单独用
fn(); // undefined(严格模式)或 window.name(非严格模式)
这就是经典陷阱:方法脱离对象,this 就丢了上下文。
一句话记住 :
.左边是谁,this 就是谁。没有.,就是普通函数调用。
规则三:作为构造函数调用 → this 指向实例对象
ini
function Person(name) {
this.name = name;
this.say = function() {
console.log(this.name);
};
}
const p = new Person('内马尔');
p.say(); // '内马尔' ✅
new 做了四件事,其中关键一步就是把 this 绑定到新创建的实例上。
规则四:作为事件处理函数 → this 指向触发事件的元素
这是前端开发最常遇到的场景:
bash
<button id="btn">点我</button>
javascript
document.getElementById('btn').addEventListener('click', function() {
console.log(this); // <button id="btn">点我</button> ✅
console.log(this.id); // 'btn'
});
但是,如果你用箭头函数:
javascript
document.getElementById('btn').addEventListener('click', () => {
console.log(this); // window ❌
});
箭头函数没有自己的 this,它会从上层作用域继承。
规则五:箭头函数 → 没有自己的 this
箭头函数是 ES6 的"异类"------它不绑定 this ,this 由外层作用域决定。
javascript
let obj = {
name: '梅西',
say: function() {
setTimeout(function() {
console.log(this.name); // undefined (this 指向 window)
}, 1000);
}
};
obj.say();
经典修复方案一: 使用 bind
javascript
let obj = {
name: '梅西',
say: function() {
setTimeout((function() {
console.log(this.name);
}).bind(this), 1000);
}
};
obj.say(); // '梅西' ✅
经典修复方案二: 使用箭头函数(更优雅)
javascript
let obj = {
name: '梅西',
say: function() {
setTimeout(() => {
console.log(this.name); // 箭头函数从上层(say函数)继承 this
}, 1000);
}
};
obj.say(); // '梅西' ✅
金句:箭头函数没有自己的 this,所以它永远不会被 call/apply/bind 改变指向------"我命由天不由我"。
三、手动指定 this:call、apply、bind
有时候,我们想强行让函数使用某个对象作为 this,那就需要三兄弟出场了。
call:立即执行,参数逐个传递
javascript
let obj1 = {
name: 'lqq'
};
let obj2 = {
name: 't总'
};
function speak(a, b) {
console.log(a, b);
console.log(this.name);
}
speak.call(obj2, '你好', '我是t总');
// '你好' '我是t总'
// 't总' ✅
apply:立即执行,参数以数组传递
arduino
speak.apply(obj2, ['你好', '我是t总']);
// 结果同上
区别就一个 :call 是散装参数,apply 是整包参数。
bind:返回新函数,不立即执行
scss
const boundSpeak = speak.bind(obj2);
boundSpeak('你好', '我是t总');
// 结果同上
bind 最适合事件监听这种"未来才执行"的场景:
javascript
const oForm = document.querySelector('.add-items');
function addItem(e) {
e.preventDefault();
console.log(this); // 我希望 this 指向某个特定对象
}
// ❌ 错误:this 会指向 oForm
oForm.addEventListener('submit', addItem);
// ✅ 正确:用 bind 固定 this
const addItemBind = addItem.bind(obj2);
oForm.addEventListener('submit', addItemBind);
// 现在 addItem 里的 this 永远指向 obj2
四、综合实战:一个完整的 Tapas 清单
结合上面的所有知识,来看一个完整的实战例子,顺便把 localStorage 存储、表单提交的两种方式,以及不同存储方案的选型都讲透。
🗂️ 先聊聊存储:前端有哪些"仓库"可选?
在实际开发中,数据要存哪里,取决于生命周期 和容量:
| 存储方式 | 生命周期 | 容量 | 特点 |
|---|---|---|---|
| localStorage | 永久(除非手动清除) | ~5-10MB | 同源共享,适合持久化配置、用户偏好 |
| sessionStorage | 标签页关闭即消失 | ~5MB | 适合单次会话的临时数据(如表单草稿) |
| Cookie | 可设置过期时间 | ~4KB | 每次请求自动携带,适合身份认证(但不宜存大量数据) |
| IndexedDB | 永久 | 几百MB以上 | 异步、支持索引查询,适合大型结构化数据(如离线缓存) |
| 后端数据库(MySQL/Redis等) | 由服务端控制 | 无限(理论上) | 多端共享、持久化,需要网络请求 |
在我们的 Tapas 清单里,数据不需要跨设备同步,也不涉及敏感信息,所以 localStorage 是最轻量、最合适的选择。
📝 表单提交:默认行为 vs JS 拦截
HTML 里 <form> 标签的默认行为是:点击 submit 按钮后,会向 action 地址发送 GET/POST 请求,并且整个页面会刷新。
xml
<!-- 默认提交:点击按钮后页面会刷新 -->
<form action="/submit" method="POST">
<input name="item">
<input type="submit">
</form>
这样做有什么弊端?
- 用户体验断裂:页面刷新会导致当前状态丢失(比如滚动位置、未保存的临时数据),用户会感到"闪一下"的割裂感。
- 无法进行前端校验:即使用户输入了非法格式,也会先提交到后端,再由后端返回错误,浪费网络资源和时间。
- 数据丢失风险:如果网络慢,刷新过程中用户可能以为没反应,反复点击提交,造成重复数据。
- 无法局部更新:现代 Web 应用追求"单页体验",只更新需要变化的部分,而默认提交是全量刷新,显然不符合。
所以现代开发中,几乎都会用 e.preventDefault() 拦截默认行为,改用 fetch / axios 异步提交,再配合前端状态管理(如 React state、Vue data)来局部渲染。
💻 完整代码(含存储 + 表单拦截)
xml
<!-- index.html -->
<div class="wrapper">
<h2>LOCAL TAPAS</h2>
<ul class="plates">
<li>loading Tapas...</li>
</ul>
<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="link">去百度</a>
</div>
<!-- 注意这里的 v=11,每次更新JS后改一下版本号,强制刷新缓存 -->
<script src="./common.js?v=11"></script>
ini
// common.js
'use strict';
const oForm = document.querySelector('.add-items');
const oList = document.querySelector('.plates');
const oLink = document.querySelector('.link');
// ----- 数据与存储 -----
const STORAGE_KEY = 'tapas_items';
let items = [];
// 从 localStorage 读取数据
function loadFromStorage() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
items = JSON.parse(stored); // 字符串 → 数组
} catch (e) {
items = [];
}
} else {
items = ['默认 Tapas 1', '默认 Tapas 2']; // 初始数据
}
renderList();
}
// 保存到 localStorage
function saveToStorage() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); // 数组 → 字符串
}
// 渲染列表
function renderList() {
if (items.length === 0) {
oList.innerHTML = '<li>还没有 Tapas,添加一个吧</li>';
return;
}
oList.innerHTML = items.map(item => `<li>${item}</li>`).join('');
}
// ----- 表单提交:拦截默认行为,用 JS 处理 -----
function addItem(e) {
e.preventDefault(); // 阻止页面刷新 ------ 这是用户体验的关键!
const input = oForm.querySelector('[name="item"]');
const newItem = input.value.trim();
if (!newItem) return;
items.push(newItem);
saveToStorage(); // 存到 localStorage
renderList(); // 仅更新列表,页面不刷新 ✅
input.value = ''; // 清空输入框
// 模拟异步提交到后端(实际项目用 fetch 或 axios)
// fetch('/api/tapas', { method: 'POST', body: JSON.stringify({ item: newItem }) })
// .then(res => res.json())
// .then(data => console.log('提交成功', data))
// .catch(err => console.error('提交失败', err));
console.log(`已添加: ${newItem},数据已存到 localStorage`);
}
// 手动指定 this 为某个对象(比如统计对象),同时保留功能
const appData = { name: 'Tapas 清单' };
const addItemBound = addItem.bind(appData);
oForm.addEventListener('submit', addItemBound);
// 链接跳转拦截(演示 this 指向)
function goBaidu(e) {
e.preventDefault();
console.log(this); // 指向 <a> 元素本身
alert('你可以在这里做埋点或校验,再决定是否跳转');
// 如果校验通过,可以手动跳转:window.location.href = this.href;
}
oLink.addEventListener('click', goBaidu);
// ----- 初始化加载数据 -----
loadFromStorage();
关键点拆解:
- 存储选型 :我们用了 localStorage,因为数据量小、永久留存、简单易用。如果是临时表单草稿,可以改用
sessionStorage;如果需要存储大量图片或文档,则考虑IndexedDB。 - 表单拦截 :
e.preventDefault()是救命稻草,它让页面不再刷新,从而实现无感提交 + 局部更新,用户体验瞬间提升一个档次。 - 版本号 :
common.js?v=11保证用户永远拿到最新 JS,避免旧缓存干扰新功能。
五、一张图总结
| 调用方式 | this 指向 |
|---|---|
| 普通函数调用(非严格模式) | window / global |
| 普通函数调用(严格模式) | undefined |
| 对象方法调用 | 调用对象 |
| 构造函数调用(new) | 新创建的实例 |
| 事件处理函数 | 触发事件的元素 |
| 箭头函数 | 上层作用域的 this |
| call / apply / bind | 手动指定的对象 |
六、面试官最爱问的 3 个 this 题
题目一:混合场景
ini
var name = 'window';
const obj = {
name: 'obj',
say: function() {
console.log(this.name);
}
};
const fn = obj.say;
fn(); // ?
obj.say(); // ?
答案 :'window' 和 'obj'
题目二:setTimeout + this
javascript
var name = 'window';
const obj = {
name: 'obj',
say: function() {
setTimeout(function() {
console.log(this.name);
}, 100);
}
};
obj.say(); // ?
答案 :'window'(非严格模式),因为 setTimeout 的回调是普通函数调用
题目三:箭头函数 + setTimeout
javascript
var name = 'window';
const obj = {
name: 'obj',
say: function() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
obj.say(); // ?
答案 :'obj',箭头函数继承了 say 函数的 this
写在最后
this 是 JavaScript 最让人头疼的概念之一,但它的核心其实只有一句话:运行时决定,谁调用指向谁。
剩下的,就是记住那 5 条规则,加上 call/apply/bind 三个手动挡工具。
而今天我们顺带还聊了 localStorage / sessionStorage / Cookie / IndexedDB 的选型对比,以及表单默认提交的四大弊端 (刷新页面、体验割裂、无法前端校验、数据丢失风险),并演示了如何用 preventDefault + 异步 JS 来打造流畅的用户体验。
这些知识点组合起来,就是一个完整的前端数据持久化 + 交互闭环。
背八股只能过面试,理解本质才能写好代码。