全面掌握JS中this的指向规则

218 阅读6分钟

今天带大家深入解析一下javascript中的this关键字,首先来看一段代码

let obj = {
    myname:'jesper',
    age:18,
    speak:function(){
        console.log(obj.myname);
    }
}
obj.speak()

很明显这段代码执行函数speak输出obj.myname,但总感觉哪里怪怪的,对象obj在自己内部调用自己的对象前面还要加上obj,就像你一般自称不会用自己的名字而是用“我”对吧,所以这里我们就引用了this

let obj = {
    myname:'jesper',
    age:18,
    speak:function(){
        console.log(this.myname);
    }
}
obj.speak()

虽然输出是一样的,但看起来就是更加优雅

this

在 JavaScript 中,this 关键字是用来引用函数执行时的上下文对象。通过使用 this,我们可以在对象的方法中访问该对象的属性,减少上下文传递,从而实现更清晰、更易维护的代码。

接下来让我们一起来揭开this神秘的面纱。

this 可以在哪些地方使用?

1. 全局环境

在全局环境中使用 this 时,它指向全局对象。在浏览器中,全局对象是 window

var a = 1
console.log(this.a); 

比如上面这段代码在浏览器的控制台上执行就会输出1.或者直接在浏览器中console.log(this),浏览器会输出window对象。但如果你在Node环境执行上述代码,则会发现输出了undefined,那是因为在Node环境中全局并不是window而是global

2. 函数体内

相较于全局,函数中this的指向则更加复杂,比如说如下代码

 function foo(){
     var a = 1
     console.log(this.a);    
 }
 foo()

乍一眼看下去,应该会输出a的值1对吧,但结果却是undefined,因为这里的this指向的是全局,这就涉及到了接下来我们要将的函数内部this的指向问题。

this 的指向规则

1. 默认绑定

先说结论, 当一个函数独立调用时,不带任何修饰符的调用,该函数的this指向全局,即window对象,这就是默认绑定。

接下来我们以下面的代码为例来分析一下

var a = 1
function foo(){
    var a = 2
    function bar(){
        var a = 3
        function baz(){
            console.log(this.a);
        }
        baz()   
    }
    bar()
}
foo()

最终结果是输出1还是2或者3 ?

  • 解读:
  1. 首先是baz独立调用,又可以看到最开始的this.a是在baz中,即this属于baz
  2. this实际的指向其实是baz所处的词法作用域,即bar,所以baz this-->bar this(函数都自带this)
  3. 又因为bar独立调用,所以bar中的this指向它的词法作用域foo,即bar中的this-->foo中的this
  4. 以此类推,这样this最终就指向了全局。

如果你觉得不好理解的话可以直接记住只要函数是直接调用的,那么它的this最终指向都是全局。

2. 隐式绑定

当一个函数被某个对象所拥有,或者函数被某个上下文对象调用时,该函数中的this指向该上下文对象

var obj = {
    a:1,
    foo:foo 
}

function foo(){
    console.log(this.a);
}
obj.foo()

这里的函数fooobj所拥有并调用,所以函数中的this指向obj,所以这里输出的是obj中的属性a的值1.

这里我们稍微改动一下代码

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

大家觉得现在的执行结果是什么?

这下就不是1了,而是undefined,因为按照上述写法对象obj只是拥有了函数的执行结果,至于obj.foo,相当于是一条无用语句,因为函数已经在obj对象中进行了独立调用,也不算无用吧,至少它可能骗到了你不是吗。

3. 隐式丢失

当一个函数被多个对象链式调用时,this指向最近的那个对象

相信大家的初高中英语老师都教过英语中的就近原则吧。

var obj = {
    a:1,
    foo:foo
}
var obj2 = {
    a:2,
    obj:obj
}

function foo(){
    console.log(this.a);
}
obj2.obj.foo()

上面的代码大家可能会认为foo属于objobj又属于obj2,所以最终foo函数中的this指向obj2对吧,但其实并不是这样的,当一个函数被多个对象链式调用时,this指向最近的那个对象,在这里就是obj,所以执行后输出的是1。

4. 显式绑定

通过 callapplybind 方法,可以显式地将 this 绑定到指定的对象。

var obj = {
    a:1
}
function foo(x,y){
    console.log(this.a,x+y);
}
// foo.call(obj,1,2)    
// foo.apply(obj,[2,3])
const bar = foo.bind(obj,1,2)
bar() // 输出1 3

上面的三个方法都是将foo中的this绑定到obj对象上,区别主要是传参的方式和返回值,callapply 的区别在于,call 接受的是参数列表,而 apply 接受的是参数数组。bind 方法返回一个新的函数,并且 this 绑定到指定对象,不会立即执行。

5. new绑定

当使用 new 操作符调用构造函数时,this 会指向新创建的实例对象。

function Person(){
    this.name = 'jesper'
}
let p = new Person()
console.log(p);

image.png

但如果函数本身有返回值呢

function Person(){
    this.name = 'jesper'
    return 'tom'
}
let p = new Person()
console.log(p)

function Person(){
    this.name = 'jesper'
    return []
}
let p = new Person()
console.log(p)

答案是如果返回值是基本类型,如数字、字符串等,则不会覆盖掉,但如果返回值是复杂类型如数组、对象等,就会被返回值覆盖掉,所以上面代码执行结果分别是

image.png

image.png

最后我们要来讲一个特殊的函数——箭头函数

箭头函数中的 this

箭头函数与普通函数不同,它没有自己的 this 绑定机制,而是继承其定义位置外层非箭头函数的 this

var obj = {
    a:1,
    foo:function(){
        const fn = () => {      //箭头函数没有this
            console.log(this.a);
        }
        fn()
    }
}

obj.foo()

上面这个例子中,fn独立调用,函数中的this应该指向全局对吧,但输出结果却是1,就是因为箭头函数不承认自己的this,所以this只能去找外层的非箭头函数即foo,又因为foo属于obj,所以输出的是obj中的属性a

箭头函数的这种行为使其非常适合在需要保持上下文 this 的情况下使用,例如在回调函数中:

function Timer() {
  this.seconds = 0;
  setInterval(() => {
    this.seconds++;
    console.log(this.seconds);
  }, 1000);
}

const timer = new Timer(); // 每秒输出递增的秒数

在这个例子中,箭头函数使得 this 始终指向 Timer 实例,从而正确地更新 seconds 属性。可以达到以下效果。

seconds.gif

结语

通过本文的介绍,我们详细解析了 JavaScript 中 this 关键字的使用场景、指向规则及其在箭头函数中的表现。掌握 this 的指向规则可以帮助我们编写更清晰、更易维护的代码。无论是在全局环境、函数体内,还是在箭头函数中,我们都要理解 this 的行为。希望这篇文章能帮助你更好地理解和运用 this