🔍 JavaScript 深入剖析:this 指向全解析

185 阅读4分钟

在 JavaScript 中,this 指向堪称「千古难题」🤯—— 明明是同一个函数,换种调用方式,this 就像脱缰的野马四处乱窜。今天我们结合具体代码,从基础到进阶,掰开揉碎讲透 this 指向的所有细节,保证看完让你豁然开朗!🚀

一、this 指向的核心原则:谁调用,指向谁!

先记住一句话:this 的指向由「函数被调用的方式」决定,而非定义的位置。就是:

函数调用时,自动生成的对象,函数的执行方式决定,指向最后调用它的对象。

二、不同场景下的 this 指向(附代码逐行解析)

2.1 普通函数调用:this 指向全局对象(非严格模式)

代码示例

var name = '小卢'; // 全局变量,挂载在 window 上
function fn() {
  var name = '小朱'; // 函数内部变量,与 this 无关
  console.log(this.name); // 打印 this 指向的对象的 name 属性
}

// 场景1:直接调用函数
fn(); // 输出:'小卢'
// 原理:全局环境下调用 fn,相当于 window.fn(),this 指向 window

// 场景2:通过 window 调用(等价于场景1)
window.fn(); // 输出:'小卢'
// 原理:显式通过 window 调用,this 仍指向 window

💡 细节补充

  • 若开启严格模式("use strict"),全局调用函数时 this 会指向 undefined,而非 window
  • 函数内部的 var name = '小朱' 是局部变量,和 this.name 无关,this 只认「调用对象的属性」。

2.2 对象方法调用:this 指向调用方法的对象

代码示例

let obj = {
  name: '蔡徐坤',
  fn: function() {
    console.log(this.name);
  }
};

// 场景3:通过对象调用方法
obj.fn(); // 输出:'蔡徐坤'
// 原理:obj 是调用者,this 指向 obj

// 场景4:方法被赋值后调用(this 丢失)
const fn2 = obj.fn; // 仅赋值函数本身,脱离 obj 上下文
fn2(); // 输出:'小卢'
// 原理:fn2 相当于全局调用,this 指向 window(同场景1)

😱 注意:一旦函数脱离原对象单独调用,this 会「回归」全局对象(非严格模式),这就是常见的「this 丢失问题」。

2.3 构造函数调用:this 指向新创建的实例对象

代码示例

function Person(name, age) {
  // 当用 new 调用时,this 指向新创建的空对象 {}
  this.name = name; // 给新对象添加 name 属性
  this.age = age; // 给新对象添加 age 属性
}

// 给构造函数的原型添加方法(所有实例共享)
Person.prototype = {
  sayHi: function() {
    console.log(`你好,我是${this.name}`); // this 指向调用者(实例)
  }
};

// 用 new 关键字调用构造函数
const haha = new Person('haha', 18); 
// 此时:haha 就是 Person 内部 this 指向的新对象

console.log(haha.__proto__ === Person.prototype); // 输出:true
// 原理:new 会让实例的 __proto__ 指向构造函数的 prototype

haha.sayHi(); // 输出:'你好,我是haha'
// 原理:sayHi 中的 this 指向调用者 haha(实例对象)

✨ 关键细节

  • new 关键字的作用:创建空对象 → 绑定 this 到空对象 → 执行构造函数 → 返回新对象。
  • 原型方法中的 this 仍遵循「谁调用指向谁」,这里 haha 调用 sayHi,所以 this 指向 haha

2.4 事件处理函数:this 指向触发事件的元素

代码示例

<button id="btn">点击</button>
<script>
  const btn = document.getElementById('btn');
  btn.addEventListener('click', function() {
    console.log(this); // 输出:<button id="btn">点击</button>
  });
</script>

📌 原理:DOM 事件回调函数中,this 默认指向「触发事件的 DOM 元素」(即 btn)。

三、改变 this 指向的三大利器:call/apply/bind

当默认的 this 指向不符合需求时,我们可以用这三个方法强行修改!它们位于函数原型链上(Function.prototype),作用是「手动绑定函数执行时的 this」。

3.1 call:立即执行,参数逐个传递

语法函数.call(新this指向, 参数1, 参数2, ...)

3.2 apply:立即执行,参数以数组传递

语法函数.apply(新this指向, [参数1, 参数2, ...])

3.3 bind:返回新函数(延迟执行),参数预先绑定

语法const 新函数 = 函数.bind(新this指向, 参数1, 参数2, ...)
(调用新函数时才执行,this 固定为绑定的值)

代码示例

var a = {
  name: '小心心',
  fn: function(a, b) {
    console.log(this.name); // 依赖 this 指向
    console.log(a, b); // 接收参数
  }
};

const b = a.fn; // b 是脱离 a 的函数,默认 this 指向 window

// 用 call 改变 this 并执行
b.call(a, 1, 2); 
// 输出:'小心心' 、1 2
// 解析:this 被改为 a,参数 1、2 逐个传递

// 用 apply 改变 this 并执行
b.apply(a, [1, 2]); 
// 输出:'小心心' 、1 2
// 解析:this 被改为 a,参数以数组 [1,2] 传递

// 用 bind 改变 this 并返回新函数
const func2 = b.bind(a, 1, 2); 
func2(); // 调用新函数才执行
// 输出:'小心心' 、1 2
// 解析:bind 不立即执行,而是返回一个绑定了 this 和参数的新函数

🔍 区别总结:

方法执行时机参数传递方式返回值
call立即执行逗号分隔函数执行结果
apply立即执行数组或类数组函数执行结果
bind延迟执行逗号分隔(可预填)新函数

四、实战中的 this 丢失问题及解决

在实际开发中,最容易踩坑的就是「this 丢失」,比如在事件回调、定时器中。我们以 button.js 为例,看看如何解决!

问题场景:事件回调中 this 指向错误

原始问题代码

function Button(id) {
  this.element = document.querySelector(`#${id}`);
  this.bindEvent();
}

Button.prototype.bindEvent = function() {
  // 给按钮绑定点击事件
  this.element.addEventListener('click', function() {
    // 这里的 this 指向触发事件的按钮(DOM元素)
    this.element.style.background = 'red'; // 报错!因为按钮没有 element 属性
  });
};

❌ 报错原因:回调函数中的 this 是按钮元素,此时 this =>  this.element ,而我们需要 this 指向 Button 实例(才能访问 this.element)。

解决方法 1:用 bind 强行绑定 this

原来this => this.element通过 bind  =>  this 

Button.prototype.bindEvent = function() {
  // 用 bind(this) 将回调中的 this 绑定为 Button 实例
  this.element.addEventListener('click', this.setBgColor.bind(this));
};

Button.prototype.setBgColor = function() {
  this.element.style.background = '#1abc9c'; // 正确!this 指向 Button 实例
};

解决方法 2:用箭头函数(继承外层 this

箭头函数没有自己的 this,会继承外层作用域的 this

Button.prototype.bindEvent = function() {
  // 箭头函数的 this 继承自 bindEvent 中的 this(即 Button 实例)
  this.element.addEventListener('click', () => {
    this.element.style.background = '#1abc9c'; // 正确!
  });
};

解决方法 3:直接不要element

由于this => this.element ,不要element直接解决

Button.prototype.bindEvent = function() {
  this.element.addEventListener('click', this.setBgColor);
};

Button.prototype.setBgColor = function() {
  this.style.background = '#1abc9c'; // this指向this.element
};

五、箭头函数与 this:特殊的存在

箭头函数是个「异类」—— 它没有自己的 this,永远继承外层作用域的 this,且无法通过 call/apply/bind 修改!

(代码示例)

var a = {
  name: 'JJJ',
  func1: function() { console.log(this.name); },
  func2: function() {
    // 箭头函数继承 func2 中的 this(即 a)
    setTimeout(() => {
      this.func1(); // 输出:'JJJ'(this 指向 a)
    }, 1000);
  }
};
a.func2();

⚠️ 注意:箭头函数不适合做构造函数(不能用 new 调用),也没有 arguments 对象。

六、总结:this 指向速查表 📝

函数调用方式this 指向示例代码
普通函数调用全局对象(非严格模式)/ undefinedfn()
对象方法调用调用方法的对象obj.fn()
构造函数调用新创建的实例对象new Person()
事件处理函数触发事件的 DOM 元素btn.onclick = function(){}
call/apply 调用第一个参数指定的对象fn.call(obj)
bind 返回的函数绑定的对象(固定不变)const newFn = fn.bind(obj)
箭头函数继承外层作用域的 this() => { ... }

掌握 this 指向的关键,在于「关注函数被谁调用」。遇到复杂场景时,不妨在函数中打印 console.log(this) 调试,多写多练自然就熟啦!💪