别再背八股了!从 5 个真实场景彻底搞懂 JavaScript 的 this

你以为你懂 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>

这样做有什么弊端?

  1. 用户体验断裂:页面刷新会导致当前状态丢失(比如滚动位置、未保存的临时数据),用户会感到"闪一下"的割裂感。
  2. 无法进行前端校验:即使用户输入了非法格式,也会先提交到后端,再由后端返回错误,浪费网络资源和时间。
  3. 数据丢失风险:如果网络慢,刷新过程中用户可能以为没反应,反复点击提交,造成重复数据。
  4. 无法局部更新:现代 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 来打造流畅的用户体验。

这些知识点组合起来,就是一个完整的前端数据持久化 + 交互闭环。

背八股只能过面试,理解本质才能写好代码。

相关推荐
东风破_1 小时前
JavaScript 面试常考的字符串算法:从反转字符串到回文判断
前端·javascript
巴勒个啦1 小时前
D3.js 入门实战:用力导向图可视化项目依赖关系
javascript
不好听6132 小时前
JavaScript 的 this 到底指向谁?
javascript·面试
触底反弹2 小时前
🔥 2026 年爆火的 Harness Engineering 到底是什么?从原理到实战一文讲透
javascript·人工智能·程序员
mONESY2 小时前
一文搞定JavaScript不同场景中 this 的指向问题
javascript
用户298698530142 小时前
在 React 中使用 JavaScript 合并 Excel 文件
前端·javascript·react.js
大流星2 小时前
LangChainJs之基础模型(一)
javascript·langchain
橘子星2 小时前
JavaScript this 指向全解实战指南
前端·javascript
weedsfly3 小时前
JS垃圾回收:从原理到项目实战,彻底根治内存泄漏
前端·javascript·面试