深入理解 JavaScript 中的 this:定时器、箭头函数与绑定方法全解析(含 call vs bind 核心区别)

38 阅读6分钟

一、基础规则:谁调用,this 就指向谁

js
编辑
var name = "windowName";
var a = {
  name: "Cherry",
  func1: function() {
    console.log(this.name); // 谁调用 func1,this 就是谁
  }
};

a.func1(); // 输出 "Cherry" —— a 调用了 func1

关键点:普通函数作为对象方法被调用时,this 指向该对象。


二、setTimeout 中的 this 陷阱

问题重现

js
编辑
var a = {
  name: "Cherry",
  func2: function() {
    setTimeout(function() {
      console.log(this); // ❌ 输出 window(非严格模式下)
      this.func1();      // ❌ 报错:this.func1 is not a function
    }, 1000);
  }
};
a.func2();

原因分析

  • setTimeout 的回调函数是一个普通函数,它是在全局作用域中被调用的。
  • 因此,this 指向全局对象(浏览器中是 window,Node.js 中是 global)。
  • 即使这个回调写在对象方法内部,也无法自动继承外层的 this

三、三种主流解决方案对比

方案 1️⃣:使用 that = this(闭包保存上下文)

js
编辑
func2: function() {
  var that = this; // 保存当前 this(即 a 对象)
  setTimeout(function() {
    that.func1(); // ✅ 正确调用
  }, 1000);
}
  • 优点:兼容性好,ES3 时代就可用。
  • 缺点:需要额外变量,代码略显冗余。

方案 2️⃣:使用 bind 显式绑定 this

js
编辑
func2: function() {
  setTimeout(function() {
    this.func1();
  }.bind(this), 1000); // ✅ 强制 this 指向 a
}

或者更常见的方式:

js
编辑
const boundFunc = this.func1.bind(this);
setTimeout(boundFunc, 1000);
  • 优点:语义清晰,可复用绑定后的函数。

  • 注意

    • bind 返回一个新函数,不会立即执行。
    • call 和 apply 会立即执行,不适合用于 setTimeout 的延迟场景。

🔍 小知识

js
编辑
console.log(a.func1.call(a)); // 立即执行,输出 "Cherry",返回 undefined
console.log(a.func1.bind(a)); // 返回绑定后的函数,不执行
const func = a.func1.bind(a);
func(); // 手动调用,输出 "Cherry"

方案 3️⃣:使用箭头函数(推荐现代写法)

js
编辑
func2: function() {
  setTimeout(() => {
    console.log(this); // ✅ 指向 a 对象
    this.func1();      // ✅ 正常调用
  }, 1000);
}

为什么箭头函数能解决?

  • 箭头函数没有自己的 this
  • 它的 this 词法绑定(lexical binding)到定义时的外层作用域
  • 在 func2 中定义的箭头函数,其 this 继承自 func2 的 this,即 a 对象。

💡 这是 ES6 最优雅的解决方案,尤其适合事件回调、定时器等场景。


四、深入:call 与 bind 的核心区别

要真正掌握 this 绑定机制,必须厘清 callbind 的本质差异。先记住一句话:

call 是「立即执行」函数并绑定 thisbind 是「返回新函数」(不执行),且永久绑定 this / 参数。

二者的设计目标、执行时机、返回值完全不同。

一、核心差异对比表

特性callbind
执行时机立即调用原函数不执行原函数,返回「新函数」
返回值原函数的执行结果绑定了 this/参数的新函数
this 绑定仅本次调用有效永久绑定,新函数无法被修改
参数传递立即传参(call(this, p1, p2)可「预传参」(柯里化),调用新函数时补传剩余参数

二、结合 setTimeout 场景,用代码讲透

1. 用 call:立即执行,但失去 setTimeout 的延迟效果

js
编辑
var a = {
  name: "cherry",
  func1: function() { console.log(this.name) },
  func2: function() {
    setTimeout(function() {
      this.func1();
    }.call(this), 1000); // ❌ 错误用法!
  }
};
a.func2(); 
// 结果:立即打印 cherry,完全没有 1 秒延迟!

原因
call 的本质是「立即调用函数」。你给 setTimeout 传入的不是「函数引用」,而是 call() 执行后的「函数返回值」(这里 func1 没有返回值,所以传入的是 undefined)。

相当于:

js
编辑
// 引擎实际执行逻辑(伪代码)
var callback = (function(){ this.func1(); }).call(a); // callback = undefined
setTimeout(undefined, 1000); // 1 秒后啥也不做

⚠️ 结论call 不能用于 setTimeout 的回调绑定!


2. 用 bind:返回新函数,保留延迟效果且绑定 this

js
编辑
var a = {
  name: "cherry",
  func1: function() { console.log(this.name) },
  func2: function() {
    setTimeout(function() {
      this.func1();
    }.bind(this), 1000); // ✅ 正确用法
  }
};
a.func2(); 
// 结果:1 秒后打印 cherry,符合预期!

原因
bind 不会立即执行函数,而是返回一个「新的函数对象」—— 这个新函数的 this 被永久绑定为你传入的 a,且会被 setTimeout 保存为回调引用。1 秒后引擎调用这个新函数时,this 已经固定为 a,而非 window


三、更深层:为什么 bind 能「永久绑定」,call 只能「临时」?

  • call / apply:属于「一次性绑定」,仅在调用的那一刻强制修改函数的 this,函数本身的 this 绑定没有被改变;
  • bind:属于「永久性绑定」,会创建一个「新的函数实例」,新函数的内部属性 [[ThisBindingStatus]] 被标记为「已绑定」,后续无论用 call/apply 怎么改,都无法覆盖这个 this(除非用 new 调用)。

验证示例:

js
编辑
function fn() { console.log(this.name); }
var a = { name: "cherry" };
var b = { name: "tom" };

// call 临时修改:本次调用 this 是 a,下次调用恢复
fn.call(a); // 打印 cherry
fn();       // 打印 window.name(或 undefined,严格模式)

// bind 永久修改:新函数的 this 固定为 a
var newFn = fn.bind(a);
newFn();         // 打印 cherry
newFn.call(b);   // 依然打印 cherry(bind 绑定无法被覆盖!)

关键结论bind 创建的新函数,其 this 是“锁死”的。


四、什么时候用 call?什么时候用 bind

场景选 call/apply选 bind
立即执行函数,临时改 this❌(bind 不执行)
延迟执行 / 回调函数(setTimeout、事件监听)❌(call 会立即执行)
函数柯里化(预传参数)✅(bind(this, p1) 可预传 p1

五、箭头函数特性总结(补充)

特性普通函数箭头函数
有 this 吗?✅ 有,动态绑定❌ 没有,继承外层
可以用 new 吗?✅ 可以❌ 不行(无 [[Construct]]
有 arguments 吗?✅ 有❌ 没有(可用 rest 参数替代)
适合做对象方法吗?✅ 适合❌ 不适合(this 不指向对象)

⚠️ 重要提醒:不要把箭头函数用作对象的方法!

js
编辑
const obj = {
  name: "Bad Example",
  getName: () => {
    return this.name; // ❌ this 不是 obj!
  }
};
console.log(obj.getName()); // undefined(this 指向全局)

六、总结要点(速记卡片)

this 指向规则速查

调用方式this 指向
obj.method()obj
func()(独立调用)全局对象(非严格模式) / undefined(严格模式)
setTimeout(fn, ...)全局对象
箭头函数外层作用域的 this
fn.call(obj) / fn.apply(obj)obj(立即执行)
fn.bind(obj)返回新函数,this 永久绑定为 obj

定时器中正确使用 this 的三种方式

  1. const that = this; → 闭包引用
  2. .bind(this) → 显式绑定(✅ 推荐)
  3. 箭头函数 → 自动继承(✅ 现代首选)

call vs bind 一句话总结

call = 绑定 this + 立即执行 → 适合马上调用;
bind = 绑定 this + 返回新函数(不执行)→ 适合延迟/回调场景。


七、拓展思考

Q:如果在类(class)中使用箭头函数定义方法,会怎样?

js
编辑
class MyClass {
  name = "Class Cherry";
  regularMethod() {
    setTimeout(function() { console.log(this.name); }, 100); // ❌ undefined
  }
  arrowMethod = () => {
    setTimeout(() => { console.log(this.name); }, 100); // ✅ "Class Cherry"
  }
}
  • 类中的箭头函数方法会自动绑定实例的 this,非常适合用作事件处理器。
  • 这也是 React 中常用 handleClick = () => {} 的原因。

八、注意事项 & 最佳实践

  • 🚫 避免在对象字面量中使用箭头函数作为方法。
  • ✅ 在回调、定时器、Promise 链中优先考虑箭头函数。
  • 🔁 若需兼容老浏览器(如 IE11),慎用箭头函数,可回退到 bind 或 that
  • 🧪 调试技巧:在不确定 this 时,先 console.log(this) 看实际指向。
  • ⚠️ 永远不要在 setTimeout 中对回调使用 .call(this)  —— 它会破坏延迟逻辑!