彻底搞懂JavaScript中的this指向:四大规则与一个流程图

43 阅读4分钟

欢迎使用我的小程序👇👇👇👇

small.png

这可能是你学this最轻松的一次

如果你曾被JavaScript中的this搞得晕头转向,别担心——几乎所有JavaScript开发者都经历过这个过程。this是JavaScript中最强大也最令人困惑的概念之一。今天,我将用最简单的方式帮你理清思路。

常见误解:this到底是什么?

误解1this指向函数本身 ❌ 误解2this指向函数的作用域 ❌

真相this是在函数被调用时才被绑定的,它的指向完全取决于函数的调用位置,而不是声明位置。

可以把this想象成电话通话中的"我":

  • 当我说"我是小明","我"指的是小明
  • 当你说"我是小红","我"指的是小红
  • "我"这个代词的具体指代,取决于谁在说话

四大绑定规则

让我们通过一个简单的比喻来理解:想象this是一个快递包裹,而不同的绑定规则是包裹上的地址标签。

规则一:默认绑定(收件人:全局或undefined)

当函数被独立调用时,this会默认绑定到全局对象(浏览器中是window,Node.js中是global)。严格模式下是undefined

function showPackage() {
  console.log(this.address); // this指向全局对象
}

address = "全局地址"; // 注意:这是全局变量

showPackage(); // 输出:"全局地址"

严格模式下的不同

function strictShowPackage() {
  "use strict";
  console.log(this); // undefined
}

strictShowPackage(); // 输出:undefined

规则二:隐式绑定(收件人:调用者)

当函数作为对象的方法被调用时,this绑定到那个对象。

const person = {
  name: "小明",
  sayHi: function() {
    console.log(`你好,我是${this.name}`);
  }
};

person.sayHi(); // 输出:"你好,我是小明"
// 这里this指向person对象

隐式丢失的坑

const person = {
  name: "小明",
  sayHi: function() {
    console.log(`你好,我是${this.name}`);
  }
};

const sayHiFunc = person.sayHi; // 只是复制函数,不是调用

sayHiFunc(); // 输出:"你好,我是undefined"
// 此时是独立调用,this指向全局对象

规则三:显式绑定(指定收件人)

使用callapplybind方法,明确告诉函数this应该指向什么。

function introduce(lang, level) {
  console.log(`我会${lang},水平${level},我叫${this.name}`);
}

const person1 = { name: "小红" };
const person2 = { name: "小刚" };

// 使用call - 立即执行,参数逐个传递
introduce.call(person1, "JavaScript", "熟练");
// 输出:"我会JavaScript,水平熟练,我叫小红"

// 使用apply - 立即执行,参数数组传递
introduce.apply(person2, ["Python", "精通"]);
// 输出:"我会Python,水平精通,我叫小刚"

// 使用bind - 返回新函数,稍后执行
const introPerson1 = introduce.bind(person1, "Java", "一般");
introPerson1(); // 输出:"我会Java,水平一般,我叫小红"

硬绑定(不会丢失的绑定)

// 使用bind创建硬绑定函数
const person = {
  name: "小明",
  sayHi: function() {
    console.log(`你好,我是${this.name}`);
  }
};

// 硬绑定:无论怎么调用,this都固定指向person
const boundSayHi = person.sayHi.bind(person);

boundSayHi(); // 输出:"你好,我是小明"

// 即使作为回调函数,this也不会丢失
setTimeout(boundSayHi, 100); // 输出:"你好,我是小明"

规则四:new绑定(新建收件人)

使用new关键字调用构造函数时,this会绑定到新创建的对象。

function Person(name) {
  // new调用时,this指向新创建的空对象
  this.name = name;
  this.sayHi = function() {
    console.log(`你好,我是${this.name}`);
  };
  // 不需要return,构造函数会自动返回this
}

const xiaoming = new Person("小明");
xiaoming.sayHi(); // 输出:"你好,我是小明"

new操作符做了四件事:

  1. 创建一个全新的空对象
  2. 将这个新对象的[[Prototype]]链接到构造函数的prototype
  3. this绑定到这个新对象
  4. 如果函数没有返回其他对象,则自动返回这个新对象

特殊场景与优先级

箭头函数:没有自己的this

箭头函数不遵循上述规则,它的this由外层作用域决定。

const obj = {
  name: "小明",
  regularFunc: function() {
    console.log(this.name); // 指向obj
  },
  arrowFunc: () => {
    console.log(this.name); // 指向外层作用域的this
  }
};

obj.regularFunc(); // 输出:"小明"
obj.arrowFunc();   // 输出:undefined(如果外层是全局)

优先级总结

当多个规则同时适用时,优先级如下:

  1. new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
  2. 箭头函数this由外层决定,不受这些规则影响
function showThis() {
  console.log(this.name);
}

const obj1 = { name: "obj1", showThis };
const obj2 = { name: "obj2" };

// 隐式绑定
obj1.showThis(); // 输出:"obj1"

// 显式绑定优先级更高
obj1.showThis.call(obj2); // 输出:"obj2"

// new绑定优先级最高
const newObj = new obj1.showThis(); // 输出:undefined(新对象没有name属性)

实用技巧与常见坑

1. 回调函数中的this丢失

// 常见问题
const button = {
  name: "提交按钮",
  onClick: function() {
    console.log(this.name); // 期望:"提交按钮"
  }
};

// this丢失了!
document.querySelector("button").addEventListener("click", button.onClick);
// 点击后输出:undefined

// 解决方案1:使用bind
document.querySelector("button").addEventListener("click", button.onClick.bind(button));

// 解决方案2:使用箭头函数
const button2 = {
  name: "提交按钮",
  onClick: function() {
    console.log(this.name);
  },
  init: function() {
    document.querySelector("button").addEventListener("click", () => {
      this.onClick(); // 箭头函数的this指向button2
    });
  }
};

2. 链式调用

const calculator = {
  value: 0,
  add: function(num) {
    this.value += num;
    return this; // 返回this实现链式调用
  },
  multiply: function(num) {
    this.value *= num;
    return this;
  },
  getValue: function() {
    return this.value;
  }
};

const result = calculator.add(5).multiply(2).add(10).getValue();
console.log(result); // 输出:20

快速决策流程图

遇到this问题,按这个流程判断:

graph TD
    A[判断this指向] --> B{函数调用方式?};
    
    B --> C[new调用];
    C --> D[this指向新创建的对象];
    
    B --> E[call/apply/bind调用];
    E --> F[this指向指定的第一个参数];
    
    B --> G[对象方法调用 obj.func ];
    G --> H[this指向obj];
    
    B --> I[独立调用 func ];
    I --> J{是否是箭头函数?};
    J --> K[是];
    K --> L[this指向外层作用域的this];
    
    J --> M[否];
    M --> N{是否严格模式?};
    N --> O[是];
    O --> P[this为undefined];
    
    N --> Q[否];
    Q --> R[this指向全局对象];

总结

理解this的关键是记住:this是在函数被调用时确定的,而不是定义时。四大规则为你提供了判断依据:

  1. 默认绑定:独立调用 → 全局/undefined
  2. 隐式绑定:对象方法调用 → 该对象
  3. 显式绑定:call/apply/bind调用 → 指定的对象
  4. new绑定:构造函数调用 → 新创建的对象

加上箭头函数的例外规则,你就能应对绝大多数this相关的问题了。

最好的学习方式是实践。打开浏览器的开发者工具,多写几个例子,观察不同情况下this的表现。当你真正理解this的运作机制后,你会发现它不再是障碍,而是编写灵活、强大JavaScript代码的有力工具。