boxboxs
技术

js中this

浏览 13最近编辑于
js中this

在JavaScript学习过程中,this几乎是每个人都会遇到的“经典难题”

很多人刚开始接触this时,常常会有这样的疑问:

  • 为什么同一个函数,在不同的地方调用时,this的值不一样
  • 为什么对象方法里的this指向对象本身,而定时器里的this却常常“跑偏”
  • 为什么箭头函数又说“没有自己的this”

这些问题的根源在于:JavaScript中this的指向,并不是在函数定义时决定的,而是在函数调用时决定的。

理解这句话,是掌握this的关键。

这篇文章会从最基础的概念开始,逐步讲清楚this的常见绑定规则、实际开发的典型陷阱,以及如何快速判断一个函数中this到底指向谁

this到底是什么

在JavaScript中,this可以理解为函数运行时的上下文对象引用。可以简单的理解为这次函数的执行,到底关联的时谁。

来看一个简单的例子:

function showThis() {
  console.log(this)
}
showThis()

这里的this指向谁,不是由函数showThis写在什么位置决定的,而是由showThis()这种调用方式决定的。

this让人困惑的地方

this难的地方在于:同一个函数,换一种调用方式,this可能就变了

例如:

function sayName() {
  console.log(this.name)
}

const obj1 = {name: "Tom", sayName}
const obj2 = {name: "Jerry", sayName}

obj1.sayName() // "Tom"
obj2.sayName() // "Jerry"

sayName //  undefined或者报错

明明是同一个函数sayName,但因为调用方式不同,this的值也不同

所以,学this不能死记硬背“它指向某个固定对象”,而应该掌握它背后的绑定规则

判断this的核心原则

在JavaScript中,this的指向通常可以根据以下几种规则来判断

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定
  • 箭头函数的词法绑定

接下来我们逐个来看:

  • 当一个函数以普通函数调用时,会触发默认绑定
function foo() {
  console.log(this)
}
foo()

此时分两种情况:严格模式下,this时undefined;非严格模式下,this指向全局对象global,在浏览器中通常时window

  • 隐式绑定:作为对象的方法调用
const obj = {
  name: "Tom" 
  sayName: function() {
    console.log(this.name)
  }
}

obj.sayName() // Tom

当函数通过某个对象来调用时,this通常指向这个调用它的对象。

容易出现误解的地方:很多人认为,只要函数定义在对象里面,this就一定指向这个对象。其实不是。关键还是看调用方式

例如:

const obj = {
  name: "Tom",
  syaName() {
    console.log(this.name)
  }
}
const fn = obj.sayName
fn()

这里虽然fn来自Obj.sayName,但真正调用的时候是:

fn()

它已经不是通过obj调用了,因此不再满足隐式绑定。这也是开发中最常见的this丢失问题之一。

  • 显式绑定:call、apply、bind

JavaScript提供了三种手动指定this的方法: call、apply、bind,它们统称为显示绑定。

call会立即调用函数,并且第一个参数就是要绑定的this

function sayName() {
  console.log(this.name)
}

const person1 = {name: "Tom"}
const person2 = {name: "Alice"} 

sayName.call(person1) // "tom"
sayName.call(person2) // "Alice"

apply的作用和call基本一样,也是立即调用函数,不同点在于参数的传递方式不同。

function introduce(age, city) {
  console.log(this.name, age, city)
}
const person = {name: "Bob"}

introduce.apply(person, [20, "Bei jing"]) // Bob 20 Bei jing

call的参数是以列表形式一个个传,apply参数放在数组传。例如:

introduce.all(person, 20, "Bei jing") // Bob 20 Bei jing
introduce.apply(person, [20, "Bei jing"]) // Bob 20 Bei jing

bind:和前面两个最重要的区别就是:它不是立即执行函数,而是返回一个新的函数。

function sayName() {
  console.log(this.name)
}

const person = {name: "Tom"}
const newFn = sayName.bind(person) 

newFn() // Tom

显示绑定最大的价值是: 当默认绑定和隐式绑定不满足要求时,我们可以手动决定this指向谁,可以选择立即执行,或者保存后在适当时机调用。

  • new绑定: 构造函数中的this

当一个函数通过new调用时,this会指向新创建的对象

function Person(name) {
  this.name = name
}
const p = new Person("Jack")
console.log(p.name) // Jack

这里Person中的this指向new过程中创建出来的实例对象。

当我们执行:new Person("Jack") 时,大致会经历下面几个步骤:

  • 创建一个空的新对象
  • 让这个对象的隐式原型__proto__,指向函数的原型对象prototype
  • 把构造函数中的this绑定到这个新对象
  • 执行构造函数代码
  • 判断构造函数有无显式返回一个对象,如果有则返回这个对象,否则返回新创建的对象。

上面的步骤也是关于构造函数的一个比较经典的面试题

  • 箭头函数中的this

箭头函数是this判断中最容易混淆的地方,准确来说:this函数没有自己的this。它的this不是在调用时决定的,而是在定义时从外层作用域继承的

这也叫做:词法绑定

先看看普通函数

const obj = {
  name: "Tom"
  sayName:() {
    console.log(this.name)
  }
}

obj.sayName() // Tom

再看看箭头函数

const obj = {
  name: "Tom"
  sayName: () => {
    console.log(this.name)
  }
}
obj.sayName() //

很多人以为这里会输出Tom,其实通常不会。

原因是:箭头函数没有自己的this,他会去外层作用域找this。而不会因为通过obj.sayName() 这种调用方式就把this绑定到obj。

箭头函数适合什么场景

箭头函数最适合的一个场景,是需要保留外层this的回调函数中。

例如:

function Person() {
  this.name = "Alice"

  setTimeout(() => {
    console.log(this.name)
  }, 1000)
}

new Person() // 1秒后输入Alice

这里的箭头函数中的this继承外层Person函数,而Person又是通过new调用的,所以外层this指向实例对象。因此回调也能正常访问到实例的name

如果换成普通函数:

function Person() {
  this.name = "Alice"

  setTimeout(function() {
    console.log(this.name)
  }, 1000)
}

这里的 this 就不是外层实例对象了,因为传给 setTimeout 的是一个普通函数,它执行时会按照自己的调用规则决定 this。结果通常拿不到预期的 name。

实际开发中常见的 this 陷阱

理解规则还不够,真正写代码时,this 的问题通常出现在一些具体场景中。

1. 方法赋值后 this 丢失

const obj = {
  name: "Tom",
  sayName() {
    console.log(this.name);
  }
};

const fn = obj.sayName;
fn();

这里 this 丢失的原因是:调用时已经不是 obj.sayName(),而是 fn()。

解决方式

const fn = obj.sayName.bind(obj);
fn();

或者:

const fn = () => obj.sayName();
fn();

2. 定时器中的 this

const obj = {
  name: "Tom",
  sayName() {
    setTimeout(function () {
      console.log(this.name);
    }, 1000);
  }
};

obj.sayName();

这里回调函数中的 this 通常不会指向 obj。

解决方式一:使用箭头函数

const obj = {
  name: "Tom",
  sayName() {
    setTimeout(() => {
      console.log(this.name);
    }, 1000);
  }
};

obj.sayName();

解决方式二:提前保存 this

const obj = {
  name: "Tom",
  sayName() {
    const self = this;
    setTimeout(function () {
      console.log(self.name);
    }, 1000);
  }
};

obj.sayName();

解决方式三:使用 bind

const obj = {
  name: "Tom",
  sayName() {
    setTimeout(function () {
      console.log(this.name);
    }.bind(this), 1000);
  }
};

obj.sayName();

3. 事件回调中的 this

在浏览器中,事件处理函数里的 this 通常指向触发事件的 DOM 元素。

button.addEventListener("click", function () {
  console.log(this); // button
});

如果改成箭头函数:

button.addEventListener("click", () => {
  console.log(this);
});

这里的 this 就不再是按钮本身,而是继承自外层作用域。

这也是为什么很多时候,DOM 事件回调中更适合用普通函数,而不是箭头函数。

4. 类方法单独传递时 this 丢失

class Person {
  constructor(name) {
    this.name = name;
  }

  sayName() {
    console.log(this.name);
  }
}

const p = new Person("Lucy");
const fn = p.sayName;
fn();

这里同样会发生 this 丢失。

解决方法

const fn = p.sayName.bind(p);
fn();

或者在构造函数中提前绑定。

如何快速判断 this 指向谁

学了这么多规则之后,我们可以总结出一个实用的判断顺序。

当你看到一个函数中的 this 时,可以按照下面的步骤来判断:

第一步:看是不是箭头函数

如果是箭头函数,直接看它外层作用域的 this,它没有自己的 this。

第二步:看是不是通过 new 调用

如果是 new Fn(),那么 this 指向新创建的对象。

第三步:看是不是通过 call、apply、bind 显式绑定

如果用了这些方法,this 通常就是你手动指定的对象。

第四步:看是不是通过对象调用

如果调用形式是 obj.fn(),那么 this 通常指向 obj。

第五步:否则就是默认绑定

普通函数独立调用,就走默认绑定:

  • 非严格模式下通常是全局对象
  • 严格模式下是 undefined

这个顺序在分析绝大多数 this 问题时都很好用。

评论 (0)

为了减少垃圾评论,请先使用 GitHub 登录后再发表评论。

使用 GitHub 登录评论

暂无评论,成为第一个评论者!