前端存储与 this 指向完全指南:从 LocalStorage 实战到 call/apply/bind 深度解析

前端存储与 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 后,fnobj 之间没有任何关系了。调用 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,会捕获外层的 windowobj.say() 中的 this 不是 obj

题目 2

javascript 复制代码
let obj = {
    name: "姆巴佩",
    say: function() {
        setTimeout(function() {
            console.log(this.name);
        }, 1000);
    }
};
obj.say(); // 输出什么?

答案undefinedsetTimeout 的回调是普通函数,独立执行时 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 是 obj
  • new Fn() → 构造函数,this 是新实例
  • btn.onclick = fn → 事件处理,this 是 btn
  • fn.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 #掘金技术社区

相关推荐
sugar__salt1 小时前
手撕字符串算法:反转、回文、验证回文 Ⅱ 完整拆解
javascript·算法·面试·职场和发展
wei1986211 小时前
.net添加web引用和添加服务引用有什么区别?
java·前端·.net
To_OC1 小时前
从一行报错开始,把字符串反转、回文算法连带着包装类一起捋明白
javascript·算法·api
蜡台2 小时前
Node 安装 awesome-qr 失败解决
javascript·vue·qrcode·awesome-qr
格子软件3 小时前
2026年GEO优化系统源码级状态机与多模型调度拆解
java·前端·vue.js·人工智能·vue·geo
HUMHSX3 小时前
Vue 项目启动全流程解析:从入口文件到全局指令注册与页面渲染
前端·javascript·vue.js
有颜有货4 小时前
PMC生产排产的4种算法,一次讲清
java·服务器·前端
小虎牙0074 小时前
Android kotlin图片库Coil源码详解
android·前端
随风一样自由4 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架