SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?

前言:一个意外的 Bug

在使用 TypeScript + Pinia 开发 Vue 3 项目时,我遇到了一个奇怪的问题:

// Pinia Store - 可以解构 ✅
const { sessionActions } = useConversationStore();
sessionActions.setActive("1"); // ✅ 正常工作

// Class 实例 - 解构后报错 ❌
const { saveConversationDB } = messageStorage;
await saveConversationDB(1, {}); // ❌ TypeError: Cannot read property '_currentUserId' of undefined

为什么同样是解构,Pinia Store 可以,Class 实例不行?

这篇文章将深入解析 JavaScript 的 this 绑定机制,以及如何正确处理方法解构。


一、问题复现

1.1 错误代码

// MessageStorageService.ts
export class MessageStorageService {
  private static instance: MessageStorageService | null = null;
  private _currentUserId = ref<string>("");

  static getInstance() {
    if (!this.instance) {
      this.instance = new MessageStorageService();
    }
    return this.instance;
  }

  async saveConversationDB(sessionId: number, data: any) {
    // 这里需要访问 this._currentUserId
    const userId = this._currentUserId.value; // ← 关键

    const updatedData = {
      userId: userId,
      sessionId,
      ...data,
    };

    // 保存到数据库
    await this.save(updatedData);
  }
}

export const messageStorage = MessageStorageService.getInstance();
// useConversation.ts
import { messageStorage } from "../services/MessageStorageService";

export function useConversation() {
  // ❌ 错误:解构实例方法
  const { saveConversationDB } = messageStorage;

  const fetchData = async () => {
    await saveConversationDB(1, { messages: [] });
    // TypeError: Cannot read property '_currentUserId' of undefined
  };
}

1.2 错误原因

// 解构前
messageStorage.saveConversationDB(1, {});
// this 指向 messageStorage 实例 ✅

// 解构后
const { saveConversationDB } = messageStorage;
saveConversationDB(1, {});
// this 变成 undefined(严格模式)或 window(非严格模式)❌

二、JavaScript this 绑定机制详解

2.1 四种 this 绑定规则

JavaScript 的 this 绑定遵循以下优先级(从高到低):

规则 1:new 绑定

function Person(name) {
  this.name = name;
}

const person = new Person("Alice");
// this 指向新创建的对象

规则 2:显式绑定(call/apply/bind)

function greet() {
  console.log(this.name);
}

const obj = { name: "Bob" };
greet.call(obj); // this 指向 obj
greet.apply(obj); // this 指向 obj
const bound = greet.bind(obj);
bound(); // this 指向 obj

规则 3:隐式绑定(对象方法调用)

const obj = {
  name: "Charlie",
  greet() {
    console.log(this.name);
  },
};

obj.greet(); // this 指向 obj ✅

// 但解构后会丢失绑定
const { greet } = obj;
greet(); // this 变成 undefined ❌

规则 4:默认绑定

function test() {
  console.log(this);
}

test();
// 严格模式:undefined
// 非严格模式:window(浏览器)或 global(Node.js)

2.2 为什么解构会丢失 this?

核心原因:解构改变了调用方式

// 原始调用(隐式绑定)
obj.method();
// 相当于:obj.method.call(obj)
// this 指向 obj ✅

// 解构后(默认绑定)
const { method } = obj;
method();
// 相当于:method.call(undefined)
// this 变成 undefined ❌

示例验证:

class Counter {
  count = 0;

  increment() {
    this.count++;
    console.log(this.count);
  }
}

const counter = new Counter();

// ✅ 方式 1:对象方法调用(隐式绑定)
counter.increment(); // 1

// ❌ 方式 2:解构后调用(默认绑定)
const { increment } = counter;
increment(); // TypeError: Cannot read property 'count' of undefined

三、Pinia Store 为什么可以解构?

3.1 Pinia 的魔法:自动绑定

// Pinia Store 定义
export const useConversationStore = defineStore("conversation", () => {
  const sessionId = ref<string | null>(null);

  const sessionActions = {
    setActive(id: string) {
      sessionId.value = id; // ← 没有使用 this
    },
    clear() {
      sessionId.value = null;
    },
  };

  return {
    sessionId,
    sessionActions,
  };
});

关键发现:Pinia Store 内部不使用 this!


3.2 Pinia 内部实现原理(简化)

// Pinia 源码简化版
export function defineStore(id, setup) {
  return function useStore() {
    // 执行 setup 函数
    const storeState = setup();

    // 遍历所有返回的属性
    for (const key in storeState) {
      const value = storeState[key];

      // 如果是对象(actions),绑定 this
      if (typeof value === "object" && !isRef(value)) {
        for (const methodKey in value) {
          if (typeof value[methodKey] === "function") {
            // ✅ 关键:自动 bind
            value[methodKey] = value[methodKey].bind(storeState);
          }
        }
      }
    }

    return storeState;
  };
}

Pinia 做了什么?

  1. 检测返回对象中的 actions
  2. 自动调用 .bind() 绑定正确的 this
  3. 所以解构后仍然能正常工作

3.3 验证 Pinia 的自动绑定

const store = useConversationStore();

// 方式 1:直接调用
store.sessionActions.setActive("1"); // ✅

// 方式 2:解构后调用(Pinia 已自动 bind)
const { sessionActions } = store;
sessionActions.setActive("1"); // ✅ 仍然有效

// 方式 3:甚至进一步解构
const { setActive } = store.sessionActions;
setActive("1"); // ✅ 仍然有效(因为 Pinia 已 bind)

四、为什么 Class 实例方法不能解构?

4.1 Class 方法的 this 绑定

class MessageStorageService {
  private _currentUserId = ref<string>("");

  async saveConversationDB(sessionId: number, data: any) {
    // ← 这里使用了 this
    const userId = this._currentUserId.value;
    // ...
  }
}

const messageStorage = new MessageStorageService();

Class 方法依赖 this 访问实例属性!


4.2 解构导致 this 丢失

// ❌ 错误调用流程
const { saveConversationDB } = messageStorage;

await saveConversationDB(1, {});
// ↓ 等价于
await saveConversationDB.call(undefined, 1, {});
// ↓ 方法内部
const userId = undefined._currentUserId.value; // ❌ TypeError

4.3 完整错误流程图

┌─────────────────────────────────────────┐
│ const { saveConversationDB } =          │
│   messageStorage                        │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ saveConversationDB 现在是一个独立函数   │
│ 失去了与 messageStorage 的连接          │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ await saveConversationDB(1, {})         │
│ 调用时没有对象前缀                      │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ 函数内部:                              │
│ const userId = this._currentUserId.value│
│              ↑                           │
│              this = undefined           │
└───────────────┬─────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────┐
│ TypeError: Cannot read property         │
│ '_currentUserId' of undefined           │
└─────────────────────────────────────────┘

五、保持 this 的正确方法

方法 1:不解构,直接使用(推荐)

export function useConversation() {
  // ✅ 推荐:不解构
  const fetchData = async () => {
    await messageStorage.saveConversationDB(1, {});
    const result = await messageStorage.getConversationsByAccountDB(123);
  };
}

优点:

  • ✅ 最简单直接
  • ✅ this 自动绑定
  • ✅ 代码可读性好

方法 2:使用箭头函数包装

export function useConversation() {
  // ✅ 箭头函数包装
  const saveConversationDB = (sessionId: number, data: any) =>
    messageStorage.saveConversationDB(sessionId, data);

  const getConversationsByAccountDB = (accountId: number) =>
    messageStorage.getConversationsByAccountDB(accountId);

  const fetchData = async () => {
    await saveConversationDB(1, {}); // ✅ this 保持
  };
}

优点:

  • ✅ 可以简化调用
  • ✅ this 正确绑定

缺点:

  • ⚠️ 需要手动包装每个方法
  • ⚠️ 增加代码量

方法 3:使用 bind 绑定

export function useConversation() {
  // ✅ 使用 bind
  const saveConversationDB =
    messageStorage.saveConversationDB.bind(messageStorage);
  const getConversationsByAccountDB =
    messageStorage.getConversationsByAccountDB.bind(messageStorage);

  const fetchData = async () => {
    await saveConversationDB(1, {}); // ✅ this 保持
  };
}

优点:

  • ✅ 标准的 JavaScript 做法
  • ✅ 明确表达意图

缺点:

  • ⚠️ 需要手动 bind 每个方法
  • ⚠️ 代码稍显冗长

方法 4:Class 内部使用箭头函数定义(不推荐)

class MessageStorageService {
  private _currentUserId = ref<string>("");

  // ✅ 使用箭头函数定义
  saveConversationDB = async (sessionId: number, data: any) => {
    const userId = this._currentUserId.value; // this 自动绑定
    // ...
  };
}

// 现在可以解构了
const { saveConversationDB } = messageStorage;
await saveConversationDB(1, {}); // ✅ 正常工作

优点:

  • ✅ 支持解构
  • ✅ this 自动绑定

缺点:

  • ❌ 每个实例都会创建新的函数(内存浪费)
  • ❌ 不符合 TypeScript 类的最佳实践
  • ❌ 无法在子类中被重写

六、对比总结表

方式代码示例this 绑定可解构推荐度
直接调用messageStorage.method()✅ 自动❌⭐⭐⭐⭐⭐
箭头函数包装const fn = (...args) => obj.method(...args)✅ 正确✅⭐⭐⭐⭐
bind 绑定const fn = obj.method.bind(obj)✅ 正确✅⭐⭐⭐⭐
Class 箭头函数method = () => {}✅ 正确✅⭐⭐
直接解构const { method } = obj❌ 丢失✅❌

七、实战案例对比

7.1 错误示例(我的实际 Bug)

// ❌ 错误代码
export function useConversation() {
  const { saveConversationDB, getConversationsByAccountDB } = messageStorage;

  const fetchConversationList = async () => {

    for (const item of response.data) {
      const data = {
        messages: [...],
        tags: [...]
      };

      // ❌ TypeError: Cannot read property '_currentUserId' of undefined
      await saveConversationDB(item.id, data);
    }

    // ❌ TypeError
    const result = await getConversationsByAccountDB(activeAccount.value);
  };
}

7.2 正确修复

// ✅ 正确代码
export function useConversation() {
  // 不解构,直接使用

  const fetchConversationList = async () => {
    for (const item of response.data) {
      const data = {
        messages: [...],
        tags: [...]
      };

      // ✅ 正常工作
      await messageStorage.saveConversationDB(item.id, data);
    }

    // ✅ 正常工作
    const result = await messageStorage.getConversationsByAccountDB(
      activeAccount.value
    );
  };
}

八、Pinia Store vs Class 实例对比

8.1 Pinia Store 的设计哲学

// Pinia Store - 基于闭包,不依赖 this
export const useConversationStore = defineStore("conversation", () => {
  // 状态(闭包变量)
  const sessionId = ref<string | null>(null);

  // Actions(闭包函数)
  const sessionActions = {
    setActive(id: string) {
      sessionId.value = id; // ← 访问闭包变量,不需要 this
    },
  };

  return { sessionId, sessionActions };
});

优点:

  • ✅ 支持解构
  • ✅ 代码简洁
  • ✅ 符合函数式编程风格

8.2 Class 实例的设计哲学

// Class - 基于原型,依赖 this
export class MessageStorageService {
  // 实例属性
  private _currentUserId = ref<string>("");

  // 实例方法
  async saveConversationDB(sessionId: number, data: any) {
    const userId = this._currentUserId.value; // ← 必须通过 this 访问
    // ...
  }
}

优点:

  • ✅ 封装性强
  • ✅ 支持继承
  • ✅ 符合面向对象编程风格

缺点:

  • ❌ 方法依赖 this
  • ❌ 不能直接解构

九、最佳实践建议

9.1 使用 Pinia Store 时

// ✅ 推荐:解构 state,直接使用 actions
const store = useConversationStore();
const { sessionId } = storeToRefs(store); // state 用 storeToRefs
const { sessionActions } = store; // actions 直接解构

// 使用
sessionActions.setActive("1"); // ✅

9.2 使用 Class 实例时

// ✅ 推荐:不解构,直接使用
import { messageStorage } from "../services/MessageStorageService";

export function useConversation() {
  const fetchData = async () => {
    await messageStorage.saveConversationDB(1, {}); // ✅
    const result = await messageStorage.getConversationsByAccountDB(123); // ✅
  };
}

9.3 如果一定要解构

// 方案 1:bind(推荐)
const saveConversationDB =
  messageStorage.saveConversationDB.bind(messageStorage);

// 方案 2:箭头函数包装
const saveConversationDB = (...args) =>
  messageStorage.saveConversationDB(...args);

// 方案 3:通用工具函数
function bindMethods<T extends object>(obj: T, methods: (keyof T)[]): any {
  const result: any = {};
  methods.forEach((method) => {
    result[method] = (obj[method] as any).bind(obj);
  });
  return result;
}

// 使用
const { saveConversationDB, getConversationsByAccountDB } = bindMethods(
  messageStorage,
  ["saveConversationDB", "getConversationsByAccountDB"]
);

十、调试技巧

10.1 如何检测 this 丢失问题

// 在方法内部添加日志
class MessageStorageService {
  async saveConversationDB(sessionId: number, data: any) {
    console.log("this:", this); // ← 检查 this 是否正确
    console.log("this._currentUserId:", this._currentUserId);

    if (!this || !this._currentUserId) {
      throw new Error("this 绑定丢失!请检查调用方式");
    }

    // 正常逻辑
  }
}

10.2 TypeScript 类型检查

// 使用 ThisType 辅助类型
interface MessageStorageActions {
  saveConversationDB(sessionId: number, data: any): Promise<boolean>;
}

class MessageStorageService implements MessageStorageActions {
  // TypeScript 会检查方法签名
  async saveConversationDB(sessionId: number, data: any): Promise<boolean> {
    // ...
  }
}

十一、总结

核心要点

  1. this 绑定取决于调用方式

    • obj.method() → this 指向 obj
    • const { method } = obj; method() → this 丢失
  2. Pinia Store 可以解构的原因

    • 内部不使用 this(基于闭包)
    • Pinia 自动 bind 所有 actions
  3. Class 实例方法不能解构的原因

    • 方法依赖 this 访问实例属性
    • 解构后 this 变成 undefined
  4. 保持 this 的方法

    • 方法 1:不解构(推荐)
    • 方法 2:箭头函数包装
    • 方法 3:bind 绑定

最佳实践

// ✅ Pinia Store - 可以解构
const { sessionActions } = useConversationStore();
sessionActions.setActive("1");

// ✅ Class 实例 - 不要解构
messageStorage.saveConversationDB(1, {});

// ✅ 如果一定要解构 - 使用 bind
const saveConversationDB =
  messageStorage.saveConversationDB.bind(messageStorage);

十二、参考资料

  • MDN: this
  • MDN: Function.prototype.bind()
  • You Don't Know JS: this & Object Prototypes
  • Pinia 官方文档
  • TypeScript Class

作者注: 这个问题困扰了我很久,希望这篇文章能帮助到遇到同样问题的开发者。记住:Pinia Store 可以解构,Class 实例方法不要解构!

Happy Coding! 🎉

最后更新时间:
贡献者: 何风顺
上一页
HTTP流式接口详解与封装实践