一个定时器,理清 JavaScript 里的 this

本文将从最基础的对象方法中this的指向说起,深入剖析定时器中this"不听话" 的原因,再逐一讲解几种常见的 "救回this" 的方法,包括经典的var that = this、灵活的call/apply、实用的bind,以及 ES6 中更优雅的箭头函数。通过清晰的案例对比和原理分析,帮你彻底理清this的绑定规律,从此不再被this的指向问题困扰。

一、从最普通的对象方法说起:this 指向当前对象

先从最正常的场景看起:一个对象,里面有个方法,方法里打印

this 和 this.name

js 复制代码
var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this);
    console.log(this.name);
  }
};
obj.func1();

在这种通过"对象.方法()"调用的场景下:

  • this 指向的是 obj 本身
  • this.name 就是 "Cherry"

也就是说,只要是"谁点出来的函数,

this 一般就指向谁"。

这一点很多人都懂,真正乱的是下面这种情况。

二、一进定时器,this 就不听话了

把上面的对象稍微改一下:再加一个方法,里面开个定时器。

js 复制代码
var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    console.log(this);   // 这里的 this 还是 obj
    setTimeout(function () {
      console.log(this); // 这里的 this 是谁?
      this.func1();      // 这里很多人第一反应是"调用不到"
    }, 3000);
  }
};
obj.func2();

运行之后你会发现:

  • func2 里面第一行 console.log(this) 打印的是 obj
  • 但是定时器回调里的 console.log(this),却不再是 obj,而是全局对象(浏览器里是 window,严格模式下甚至可能是 undefined

原因是:谁调用这个函数,

this 就指向谁

  • obj.func2() 是"对象.方法调用",所以 this === obj
  • setTimeout 回调是"普通函数调用",真正执行时类似 window.callback(),所以 this 又回到了全局

于是

this.func1() 就出现了典型错误:

你以为是调用 obj.func1,实际上是在全局环境下找 func1。

三、三种常见的"救回 this"姿势

为了在定时器里还能拿到"外层的那个对象",常见有三种写法。

1. 老派写法:var that = this

最早接触到的方案一般是这个:

js 复制代码
var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    var that = this;   // 先把外层的 this 存起来
    setTimeout(function () {
      console.log(this);      // 这里还是全局对象
      that.func1();           // 用 that 调用
    }, 3000);
  }
};
obj.func2();

思路很直白:

  • 外层 this 是我们想要的对象
  • 回调内部再用一个变量 that 把它"闭包"住
  • 不再依赖回调里的 this,而是用 that 去调用

优点:

  • 所有环境都支持,ES5 就可以用
    缺点:
  • 可读性一般,多层回调时会出现 that = this / self = this 满天飞

2. call / apply:立即执行并指定 this

第二种方案是利用 Function.prototype.call / apply,它们有两个关键点:

  • 都是立即调用函数
  • 第一个参数是要绑定的 this

例如:

js 复制代码
function show() {
  console.log(this.name);
}
var obj = { name: 'Cherry' };
show();             // this => window / undefined
show.call(obj);     // this => obj
show.apply(obj);    // this => obj

callapply 的区别只在于传参方式

  • call(fnThis, arg1, arg2, ...)
  • apply(fnThis, [arg1, arg2, ...])

在和定时器结合时,有一种稍微"绕"一点的写法,会先用 call 执行一次,然后把返回的函数交给定时器:

js 复制代码
setTimeout(function () {
  console.log(this);  // 这里的 this 被 call 成 obj
  this.func1();
  
  return function () {
    console.log(this);  // 这个函数真正被 setTimeout 调用时,this 又回到全局
  };
}.call(obj), 2000);

分析一下这个写法的流程:

  • .call(obj)立刻执行 这段函数,里面的 this 是 obj
  • 这个函数里 this.func1() 能正常调用到 obj.func1
  • return 的那个内部函数才是真正交给 setTimeout
  • 这个内部函数在将来执行时,又是一次"普通函数调用",于是 this 再次回到全局

这种写法属于"利用 call 硬拉一次

this 过来",但在实际项目里,更常见的做法不是这样用 call,而是第三种:bind

3. bind:先订婚,后结婚

bindcall/apply 很容易混:

  • call/apply马上执行,并临时指定一次 this
  • bind不执行,而是返回一个"this 永远被绑死"的新函数

用一个简单对比看差异:

js 复制代码
var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  }
};
console.log(obj.func1.bind(obj)); // 打印的是一个新函数
console.log(obj.func1.call(obj)); // 打印的是 func1 的返回值(这里是 undefined)
const f = obj.func1.bind(obj);
f(); // 始终以 obj 作为 this 调用

套用一个比较形象的说法:

  • call/apply闪婚,当场拍板,函数当场执行完事
  • bind先订婚,先约定好将来的 this,真正结婚(执行)是以后

因此在定时器这种"将来才会执行"的场景,bind 非常自然:

js 复制代码
var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this.name);
  },
  func2: function () {
    setTimeout(this.func1.bind(this), 3000);
  }
};
obj.func2();
  • this.func1.bind(this) 立即返回了一个新函数
  • 这个新函数里 this 被固定成当前对象
  • setTimeout 三秒后再执行它时,this 依然是那个对象

相较于 that = this 和"花里胡哨的 call 写法",bind 在这种场景下是最容易读懂的一种。

四、箭头函数:不再创建自己的 this

还有一种办法,是直接 **放弃回调自己的 **

this,而是用外层的。

这就是箭头函数的做法:箭头函数不会创建自己的执行上下文,它的 this 完全继承自外层作用域

箭头函数的核心是没有自己的 this ,它的 this词法绑定 (定义时继承外层作用域的 this),而非动态绑定。

把前面的定时器改成箭头函数版本:

js 复制代码
var obj = {
  name: 'Cherry',
  func1: function () {
    console.log(this);       // obj
    console.log(this.name);  // Cherry
  },
  func2: function () {
    console.log(this);       // obj
    setTimeout(() => {
      console.log(this);     // 依然是 obj
      this.func1();          // 也能正常调用
    }, 3000);
  }
};
obj.func2();
obj.func1();

这里的关键点是:

  • func2 里的 this 是对象本身
  • 箭头函数的 this 直接沿用 func2 的 this
  • 所以在箭头函数里,this 没有发生"跳变",始终是那个对象

再结合一个简单的对比例子,看得更清楚。

1. 普通函数和箭头函数的 this 对比

js 复制代码
// 普通函数
function normal() {
  console.log(this);
}
// 箭头函数
const arrow = () => {
  console.log(this);
};
normal(); // 非严格模式下 this => window
arrow();  // this 继承自定义它时所在的作用域(全局里一般也是 window / undefined)

再注意一个常被问到的问题:

  • 箭头函数不能作为构造函数使用 ,也就是说不能 new 一个箭头函数
    在实际代码里,如果你尝试 new func()(func 是箭头函数),会直接报错

2. 顶层箭头函数的 this

在普通脚本里,如果写一个顶层箭头函数:

js 复制代码
const func = () => {
  console.log(this);
};
func();

这里的

this 继承自顶层作用域:

  • 浏览器非模块脚本中,一般是 window
  • 严格模式 / ES 模块中,顶层 this 往往是 undefined

这也再次说明:箭头函数的

this 完全取决于它被定义时所处的环境,而不是被谁调用。

3. 继承外层作用域的 this

js 复制代码
const obj = {
  name: 'Cherry',
  func: function () {          // 普通函数,this === obj
    console.log('外层 this:', this);
    setTimeout(() => {         // 箭头函数,this 继承外层
      console.log('箭头函数 this:', this);
      console.log('name:', this.name);
    }, 1000);
  }
};
obj.func();

把 setTimeout 里的箭头函数换成普通函数,this 会丢失

setTimeout 中的回调函数是被 JavaScript 引擎 "独立调用" 的,而非作为某个对象的方法调用

五、小结 & 使用建议

把上面的内容串一下,可以得到这样一份"速记表":


this 的基础规律

  • 谁调用,指向谁obj.method()this === obj

  • 普通函数直接调用:fn() 里 this 是全局对象 / undefined(严格模式)

  • setTimeout / 回调里的

    this

    • 回调是普通函数调用,this 默认指向全局
    • 所以在对象方法里直接写 setTimeout(function () { this.xxx }),往往拿不到我们想要的对象
  • 三种修复方式的对比

    • var that = this

      • 利用闭包保存外层 this
      • 兼容最好,但代码略显"老派"
    • call/apply

      • 立即执行函数
      • 第一个参数用来指定 this
      • 适合"当场就要执行一次"的场景
    • bind

      • 返回"this 被锁死"的新函数,而不是立即执行
      • 非常适合"定时器、事件监听、回调"这些"稍后再执行"的情形
  • 箭头函数

    • 不创建自己的 this,只继承外层
    • 在对象方法中配合回调使用,能有效避免 this 跳来跳去
    • 不适合作为构造函数(不能 new
相关推荐
代码小学僧2 小时前
从 Arco Table 迁移到 VTable:VTable使用经验分享
前端·react.js·开源
微笑的曙光2 小时前
Vue3 环境搭建 5 步走(零基础友好)
前端
不知名用户来了2 小时前
基于vue3 封装的antdv/element-Plus 快速生成增删改查页面
前端
明川2 小时前
Android Gradle - ASM + AsmClassVisitorFactory插桩使用
android·前端·gradle
San302 小时前
深度驱动:React Hooks 核心之 `useState` 与 `useEffect` 实战详解
javascript·react.js·响应式编程
布列瑟农的星空2 小时前
webpack迁移rsbuild——配置深度对比
前端
前端小黑屋2 小时前
查看项目中无引用到的文件、函数
前端
前端小黑屋2 小时前
小程序直播挂件Pendant问题
前端·微信小程序·直播