彻底搞懂js中的this关键字

225 阅读8分钟

在理解this关键字之前,我们应该先搞清楚一个问题。

为什么要用this

来看看这段代码:

function identify(context) {
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identify(context)
    console.log(greeting);
}
var me = {
    name: "pw"
}
var you = {
    name: "Reader"
}
speak(you);
speak(me);

注:toUpperCase() 是一个字符串(String)对象的方法。它用于将调用它的字符串值转换为全部大写,并返回新的全部为大写的字符串。原字符串不会被改变,因为字符串在 JavaScript 中是不可变的。

执行结果:

Hello, I'm READER
Hello, I'm PW

再来看看使用了this关键字的版本:

function identify() { 
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I'm " + identify.call(this)
    console.log(greeting);
}
var me = {
    name: "pw"
}
var you = {
    name: "Reader"
}
speak.call(you);
speak.call(me);

执行结果同上。

不难看出,如果不使用this,你就必须要显式地传入上下文对象,而this提供了方式让对象引用可以被隐式传递,优雅、简洁、且易于复用。上述代码逻辑简单,你可能还体会不出来,但是随着代码的设计模式越来越复杂,显式传递上下文对象会让代码变的越来越混乱,而this可以让函数自动引用合适的上下文对象,使用this关键字对API的设计是非常有帮助的。

另外,this由于其不同的绑定方式(后面会提到),可以帮我们确定函数是如何被调用的(函数被谁调用以及被调用的位置)。

现在,让我们回到对this关键字概念上的理解。

什么是this


this是函数运行环境的指针,它指向当前执行上下文的对象。

this的值是在运行时基于函数的调用方式动态绑定的,而不是定义时确定的。


不必咬文嚼字,理解了this的底层机制你就能看懂这两句话了。

this的机制

如果你不能轻松判断this的指向,我想告诉你的是:当你深入了解JavaScript中内存管理(包括栈内存和堆内存)的机制时,你就能更好地理解this关键字在不同情况下指向哪里。

现在我需要你从内存的角度思考。 来看看这个例子:

iwEeAqNwbmcDAQTRAcsF0QDcBrCEaX49Cuh_RAc0vZOUWBoAB9IoJU2sCAAJomltCgAL0R53.png_720x720q90.jpg

var obj = {  
    foo:5
}//这是一个对象

iwEcAqNwbmcDAQTRAvkF0QEiBrBkuB1jEPAATwc0vdkNpE8AB9IoJU2sCAAJomltCgAL0W40.png_720x720q90.jpg

  • 当你定义一个对象并为其添加属性时,JavaScript引擎会在内存中为该对象创建相应的内部特性(即属性描述符):

    • [[value]]:属性的值。

    • [[writable]]:是否可以修改属性的值。

    • [[enumerable]]:是否可以在遍历对象时被枚举。

    • [[configurable]]:是否可以删除该属性或修改其描述符(是否可配置)。

var obj = {  
    foo:function(){
        console.log(this);
    }
}//这是一个对象

obj.foo();// 属于对象的方法被调用

var foo = obj.foo;
foo();// 属于普通函数被调用

iwEcAqNwbmcDAQTRAwYF0QFdBrDWNVtKmSf_Pgc0vnB3gYoAB9IoJU2sCAAJomltCgAL0gAAkoM.png_720x720q90.jpg

反常识的地方来了,尽管foo是对象obj的属性并且函数function是属性foo的值,但是在内存中objfunction的联系并不是很强烈,因为foo属性的值实际上是function地址

再来看看两种不同的调用方式。相同点是它们最终都是同一个函数在运行;区别在于它们调用的方式不一样

执行结果:

{ foo: [Function: foo] }

<ref *1> Object [global] {
    省略
}

同一个函数的执行为什么会产生两种不同的结果?

这是因为 每个函数都有一个内置的this关键字 ,foo属性作为对象的方法被调用在内存中指向了它的对象obj, 而foo作为普通函数调用则默认指向了全局对象,现在我们需要了解this的指向的规则了。

this的四种绑定方式

1. 默认绑定

这是最基础的绑定规则,适用于直接调用函数的情况。

  • 非严格模式:如果函数不是作为对象的方法被调用、也不是通过new或显式绑定的方式调用,那么this会默认指向全局对象(在浏览器中是window,在Node.js环境中是global)。
  • 严格模式:在严格模式下,如果没有任何其他规则适用,this将被设置为undefined
function foo() {
  console.log(this);
}
foo(); // 在非严格模式下输出 window 或 global,在严格模式下输出 undefined

2. 隐式绑定

当一个函数作为对象的方法被调用时,this会被隐式地绑定到该对象上。换句话说,如果一个函数是某个对象的一个属性,并且通过该对象来调用这个函数,那么this就会指向这个对象。

function foo(){
    console.log(this.a); 
}
var obj1={
    a:2,
    foo:foo
};
obj.foo();

需要注意的是,如果方法被赋值给一个变量后调用,或者是在某些情况下传递给另一个函数时调用,隐式绑定可能会丢失,这时this会根据调用位置应用默认绑定规则。

3. 显式绑定

你可以使用call()apply()bind()方法来显式地指定函数内部this的值。这种方式允许你手动控制this的绑定。

  • call()apply() :立即调用函数,并允许你指定this的值。它们的区别在于参数的传递方式不同:call()接收的是逗号分隔的参数列表,而apply()接收的是一个参数数组。
  • bind() :返回一个新的函数,该函数的this被永久绑定到你提供的对象上,即使之后再使用call()apply()或再次bind()也不会改变其this的绑定。
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello'); // Hello, Alice
greet.apply(person, ['Hi']); // Hi, Alice

const greetPerson = greet.bind(person);
greetPerson('Good morning'); // Good morning, Alice

4. new绑定(构造器绑定)

当使用new关键字调用一个函数时,它被称为构造函数调用。在这种情况下,this会被绑定到新创建的对象实例上。构造函数通常用于创建具有特定属性和方法的新对象。

function Constructor(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, ${this.name}`);
  };
}
const instance = new Constructor('Bob');
instance.sayHello(); // Hello, Bob

构造器调用还涉及到原型链的概念,新创建的对象会继承构造函数的原型上的属性和方法。

现在明白为什么在创建构造函数的时候我们总是要写this在里面了吗?在代码的世界里,绝不存在什么“固定搭配”,每个关键字、每种代码规范都有它的设计初衷,只有深入到底层,你才能明白这些设计的真正用法。

绑定可能有问题

function foo() {
    console.log(this.a);
}
var obj = {
    a: 2,
    foo: foo
};
var bar = obj.foo;

var a = "oops, global";

bar();

输出结果:

oops, global

仔细观察这段代码和隐式绑定的区别,我们可以看到代码中定义了bar变量并将对象obj下的属性foo赋值给了它。 然后在对象外部定义了一个变量a,此时对象内外都存在一个变量a

那么,为什么this选择了外部的a而没有选择内部的a呢?

还记得this的定义么?我来重复一遍吧。

this是函数运行环境的指针,它指向当前执行上下文的对象。 this的值是在运行时基于函数的调用方式动态绑定的,而不是定义时确定的。

请看到代码的最后一行,foo函数被赋给了变量bar,由于JavaScript是弱类型语言,此时bar同样为函数,而bar实际上是以普通函数的方式调用的,因此this默认指向全局,所以拿到了全局执行上下文中的a(严格模式下将被设置为undefined)。像这样隐式绑定的函数丢失其绑定对象的情况我们称之为隐式丢失

this丢失的解决方法

下面这段代码会报错: TypeError: this.func1 is not a function

<script>
    var name = "windowName";
    var a = {
        name:"肖宇泉",
        func1:function() {
            console.log(this.name);
        },
        func2:function() {
            setTimeout((function() {
                this.func1();
            }),1000)
        }
    }
    a.func2();
</script>

解决方法有两种:

1.保存this引用

<script>
    var name = "windowName";
    var a = {
        name:"肖宇泉",
        func1:function() {
            console.log(this.name);
        },
        func2:function() {
        let _this = this;
            setTimeout((function() {
                _this.func1();
            }),1000)
        }
    }
    a.func2();
</script>
  • 将当前的 this 值保存到 _this 变量中。通过保存对原始 this 的引用,可以在回调中正确引用对象 a 的属性和方法。

2.使用箭头函数

<script>
    var name = "windowName";
    var a = {
        name:"肖宇泉",
        func1:function() {
            console.log(this.name);
        },
        func2:function() {
            setTimeout(( ()=> {
                this.func1();
            }),1000)
        }
    }
    a.func2();
</script>
  • 箭头函数是ES6(ECMAScript 2015)引入的一种新的函数定义方式,它简化了函数的语法,并且改变了this的绑定规则。它不拥有自己的this,它继承自外围(词法)作用域,即定义箭头函数的地方。这意味着在箭头函数内部,this的值是在函数被创建时确定的,而不是在函数被调用时确定的。

在一般情况下this的指向应该是在函数被调用时确定的,而上述两种方式使this的指向在函数被创建时就确定了,因此两种方法本质上是相同的。

51330ad45a2842d5af2533cac81140d8~tplv-5jbd59dj06-image.png

希望这篇文章对你有帮助,点个赞再走吧~