1.4.3 函数的 this 关键字
相比其它语言,JavaScript 中函数的 this
关键字的行为略有不同。此外,它还区分严格模式和非严格模式。
在大多数情况下,this
的值都取决于函数的调用方式(运行时绑定),这就意味着即便是同一个函数,随着它调用方式的不同,函数里面的 this
值就有可能不一样。但有两种特殊情况,分别是:
值得注意的是,在代码执行期间,this
的值是不能通过赋值来设置的。要想在调用函数的时候设置 this
值,可以使用 call()
和 apply()
方法。
1. 执行上下文
this
是执行上下文的一个属性。在严格模式下它可以是任意值(原始值或对象),在非严格模式下它始终是对象的引用。
a property of an execution context
Global context,全局上下文
Function context,函数上下文
Class context,类上下文
Derived classes context,派生类上下文
1.1 全局
在全局执行上下文中,this
指的是全局对象。我们可以使用全局属性 globalThis
来获取全局对象,而不管当前代码的执行上下文。
1.2 函数
在函数内部,this
的值取决于函数的调用方式。
在上面的代码中,fn
是对象方法的一个引用。fn()
是作为一个函数被直接调用的,而不是作为对象的方法,比如第 8 行里的 obj.method()
。
1.3 类
class 中 this
的行为和函数中的类似(毕竟类的底层依然是函数),但是它们之间还是有一些区别的。
在类的构造函数中,this
是一个常规对象,类里面的所有非静态方法都会添加到这个 this
的原型上。而类里面的所有静态方法都不是这个 this
的属性,而是类本身的属性。
有时可能需要手动修改类方法里的 this
值,以确保它始终指向特定类的实例。比如:
1.4 派生类
和基类的构造函数不同,派生类的构造函数没有初始的 this
绑定,所以就需要开发人员在构造函数中手动调用 super()
。比如,执行以下代码会报错:
在构造函数中调用 super()
的时候,会创建一个 this
绑定。这本质上就和执行了语句 this = new Base()
的效果类似,其中 Base
是继承的父类。
当然,如果派生类中根本就没有构造函数,或是构造函数自己就返回了一个对象,那么不写 super()
也是不会报错的。比如,以下代码就可以正常运行:
2. 调用方式
2.1 作为对象方法
当一个函数作为对象的方法被调用时,它的 this
值会被设置成调用该方法的对象。
只要函数是作为对象的方法被调用的,那就符合这样的逻辑:函数的 this
值会被设置成调用该方法的对象,而不管函数是怎么定义的或是在哪里定义的。
2.1.1 先单独定义函数
也可以先定义函数,然后再将其附加到对象的属性上,最终效果都是一样的。
2.1.2 嵌套对象的方法
也可以将函数赋值给嵌套 n 层的对象的属性,此时 this
值的原理也是一样的。
2.1.3 原型链上的方法
同样的逻辑,也适用于定义在对象原型链上的方法。如果一个方法位于对象的原型链上,那么其 this
指向的依然是调用该方法的特定对象(就像该方法就在那个对象上一样)。
虽然分配给变量 p 的对象没有自己的 sum
属性,但它可以从其原型链上继承。
p.sum()
对 sum
的查找是从对象 p 开始的,所以 sum()
函数内部的 this
值会被设置成对象 p 的引用。也就是说因为 sum()
是作为对象 p 的方法被调用的,所以它的 this
指的就是对象 p。这是 JavaScript 原型继承中一个非常有趣的特性。
2.1.4 getter 和 setter
同样,当函数作为 getter 和 setter 被调用时,其内部的 this
会绑定到获取和设置属性的对象上。
2.2 作为构造函数
当一个函数被当作构造函数使用时(使用 new
关键字),它的 this
值会绑定到正在构造的新对象上。
然而,如果函数有自己的 return
语句,且返回的是一个对象,那么这个对象就会被作为 new
表达式的结果。如下:
在上面的例子中,因为函数在构造的过程中返回了一个对象(第 3 行),所以它就作为了 new
表达式的结果,因此第 9 行会输出 40,而不是 34。在这种场景下,Func()
函数里的 this
所指向的新对象并没有被暴露出去,这就意味着 this
对象只能在构造函数里使用,这在一定程度上实现了封装。
需要注意的是,一旦(构造)函数 return
的不是一个对象,那么 new
表达式的结果还依然是初始的 this
值(即正在被构造的新对象)。如下:
2.3 箭头函数
箭头函数没有绑定自己的 this
值,它会直接保留封闭词法上下文的 this
。关于箭头函数的基本介绍详见《箭头函数》,这里只重点介绍一些交叉类信息,同时作为对箭头函数的一个补充。
2.3.1 在函数内嵌套
如果是在其它函数内部创建的箭头函数,它的 this
值依然只和封闭词法上下文的 this
相关。
以上代码的执行结果,如下:
对象 obj
的方法 bar()
返回的是一个箭头函数。因为是箭头函数,所以它的 this
值会永久绑定到其封闭上下文的 this
上——函数 bar()
的 this
值。而函数 bar()
的 this
值又是动态绑定的(和它的调用方式相关),这就反过来影响到了返回的箭头函数的 this
值,也就是说这里的箭头函数的 this
值看起来也是“动态”的了。
第 7 行 fn = obj.bar()
的执行过程大约是:执行函数 bar()
,因为它是作为对象 obj
的方法被调用的,所以 bar()
函数里的 this
值会被设置成 obj
对象。函数 bar()
执行完之后,会返回箭头函数的引用 x
,并将其赋值给变量 fn
。此时,fn
指向箭头函数,里面的 this
值是 obj
对象,所以第 8 行代码会输出 obj
对象。
第 10 行 fn2 = obj.bar
只是引用了 obj
的 bar
方法并没有调用它,所以此时的变量 fn2
是一个指向了 bar
函数的引用。所以,当在第 11 行执行 fn2()()
时,第一个 ()
相当于执行了 bar()
函数且是在全局作用域里调用的,所以 bar()
里的 this
会被设置成全局对象,而第二个括号 ()
相当于是执行了返回的箭头函数,所以最终会输出全局对象 window
。
2.3.2 无法设置的 this
this
如果是通过 bind()
, call()
或 apply()
调用箭头函数,那么传过来的 thisArg
参数会直接被箭头函数忽略,所以通常会直接传个 null
。
所以,我们通常会说箭头函数不适用于 call()
, apply()
和 bind()
这些实例方法。
2.4 事件处理器
当一个函数用作事件处理程序时,它的 this
会被设置成监听的那个 DOM 元素。
3. 主要参考
Last updated