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

    • html1
    • html2
  • CSS

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

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的`this`指向问题详解
    • SessionStorage踩坑记录:它真的能"只设置一次"吗?
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
  • es6

    • Promise
    • es6 模块导出方式全解析
  • 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 3 深度选择器:deep()完全指南
  • Electron

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

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

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • 使用 pnpm 构建 Monorepo 实战指南
    • pnpm 修改依赖源码打包报错
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧

JavaScript 中的this指向问题详解

问题背景

在 JavaScript 事件处理中,this的指向经常会导致令人困惑的问题。本文通过BuriedLogic类中的实际问题,分析常见的this指向问题并提供解决方案。

问题代码示例

在BuriedLogic类中,我们遇到了这样的问题:

// 问题代码
document.getElementById("jumpWB").addEventListener("click", BuriedLogic.instance.jumpWB);

// jumpWB方法(普通函数定义)

class BuriedLogic {
  static instance = null;

  constructor() {
    if (BuriedLogic.instance) {
      return BuriedLogic.instance;
    }

    jumpWB() {
        const ua = navigator.userAgent.toLowerCase();
        const time = ua.indexOf("baiduboxapp/") > -1 ? 500 : 100;

        setTimeout(() => {
            this.handleJumpLogic(); // 报错:this.handleJumpLogic is not a function
        }, time);
    }
}

点击按钮时报错:"handleJumpLogic is not a function",而下面的写法却可以正常工作:

// 可行的代码
document
  .getElementById("jumpWB")
  .addEventListener("click", () => BuriedLogic.instance.jumpWB());

深入理解 JavaScript 中的this

普通函数与箭头函数的根本区别

  1. 普通函数:

    • this值取决于函数如何被调用,而非定义在哪里

    • 作为事件监听器时,this指向触发事件的 DOM 元素

    • 函数调用上下文可以被改变(通过 call, apply, bind)

  2. 箭头函数:

    • 没有自己的this绑定,它使用定义时所在环境的this值

    • this值在定义时就确定,不会因调用方式而改变

    • 不能通过 call、apply、bind 改变其this指向

为什么会出现问题?

当我们使用addEventListener("click", BuriedLogic.instance.jumpWB)这种方式时:

  1. 传递的是函数引用,原始的对象上下文(BuriedLogic.instance)在传递过程中丢失

  2. 当事件触发时,浏览器调用此函数,并将this设置为触发事件的 DOM 元素

  3. jumpWB方法内部的this指向了 DOM 元素,而不是BuriedLogic实例

  4. DOM 元素上没有handleJumpLogic方法,因此报错

内部执行逻辑相当于:

const clickHandler = BuriedLogic.instance.jumpWB; // 提取函数引用,丢失了this上下文
element.onclick = function (event) {
  // 浏览器将this设为DOM元素
  clickHandler.call(element, event);
};

两种主要解决方案

方案 1:使用箭头函数包装调用,实现方法为普通函数

document
  .getElementById("jumpWB")
  .addEventListener("click", () => BuriedLogic.instance.jumpWB());

class BuriedLogic {
  // ...其他代码...

  // 普通函数定义方法
  jumpWB() {
    const ua = navigator.userAgent.toLowerCase();
    const time = ua.indexOf("baiduboxapp/") > -1 ? 500 : 100;

    setTimeout(() => {
      this.handleJumpLogic(); // this指向BuriedLogic实例
    }, time);
  }
}

为什么有效?

  • 外层箭头函数充当"包装器"

  • 明确指定了调用对象是BuriedLogic.instance

  • 在执行jumpWB()时保持了正确的对象上下文

工作原理:

  1. 事件触发时,执行外层箭头函数

  2. 箭头函数内部显式指定了BuriedLogic.instance作为调用jumpWB的对象

  3. 这确保了方法在正确的对象上下文中执行

方案 2:将实现方法定义为箭头函数,使用时为函数引用

class BuriedLogic {
  // ...其他代码...

  // 使用箭头函数定义方法
  jumpWB = () => {
    const ua = navigator.userAgent.toLowerCase();
    const time = ua.indexOf("baiduboxapp/") > -1 ? 500 : 100;

    setTimeout(() => {
      this.handleJumpLogic(); // this指向BuriedLogic实例
    }, time);
  };
}

// 使用方式不变
document
  .getElementById("jumpWB")
  .addEventListener("click", BuriedLogic.instance.jumpWB);

为什么有效?

  • 箭头函数的this在定义时就被"锁定",指向定义时的上下文(即 BuriedLogic 实例)

  • 箭头函数在类的实例创建阶段被求值,此时this指向新创建的实例

  • 这个this值会被"固定"在箭头函数中,不会因为后续的调用方式而改变

工作原理:

  1. 当类实例化时,箭头函数属性被初始化,捕获了当时的this(指向 BuriedLogic 实例)

  2. 当事件触发时,即使函数是作为事件处理程序被调用,其内部的this仍然维持原始值

  3. 这确保了this.handleJumpLogic()可以正确找到方法

实例对比展示

通过完整代码展示两种方式的区别:

class BuriedLogic {
  static instance = null;

  constructor() {
    if (BuriedLogic.instance) {
      return BuriedLogic.instance;
    }

    BuriedLogic.instance = this;
    this.urlKey = "示例值";
  }

  // 初始化方法
  initialize() {
    console.log("--- 测试不同绑定方式 ---");

    // 问题方式:this会指向DOM元素
    console.log("1. 直接传递方法引用:");
    document
      .getElementById("test1")
      .addEventListener("click", BuriedLogic.instance.regularMethod);

    // 解决方案1:箭头函数包装调用
    console.log("2. 箭头函数包装:");
    document
      .getElementById("test2")
      .addEventListener("click", () => BuriedLogic.instance.regularMethod());

    // 解决方案2:使用箭头函数定义方法
    console.log("3. 箭头函数方法:");
    document
      .getElementById("test3")
      .addEventListener("click", BuriedLogic.instance.arrowMethod);
  }

  // 普通方法定义
  regularMethod() {
    console.log("普通方法中this是:", this);
    console.log("能否访问urlKey:", this.urlKey);
    try {
      this.helper();
    } catch (e) {
      console.error("调用helper失败:", e.message);
    }
  }

  // 箭头函数方法定义
  arrowMethod = () => {
    console.log("箭头函数方法中this是:", this);
    console.log("能否访问urlKey:", this.urlKey);
    this.helper(); // 总是可以正常调用
  };

  helper() {
    console.log("辅助方法被调用,urlKey =", this.urlKey);
  }
}

// 初始化
BuriedLogic.instance = new BuriedLogic();
BuriedLogic.instance.initialize();

最佳实践

  1. 理解函数类型的区别:明确普通函数与箭头函数的this绑定差异

  2. 事件处理程序选择:

    • 当需要在多处重用同一事件处理程序时,优先将方法定义为箭头函数

    • 对于一次性使用的事件处理,箭头函数包装是简洁有效的选择

  3. 避免混合使用:在项目中保持一致的事件绑定风格,避免混合不同的绑定方式

  4. 性能考量:

    • 箭头函数作为类属性会占用每个实例的内存

    • 对于可能创建多个实例的类,考虑使用bind方法或箭头函数包装

理解 JavaScript 中的this机制是编写可靠代码的关键,特别是在处理事件和回调函数时。选择合适的函数定义和绑定方式,可以避免大多数常见的this指向问题。

最后更新时间:
贡献者: 何风顺
上一页
JavaScript 中的 script 标签问题详解
下一页
SessionStorage踩坑记录:它真的能"只设置一次"吗?