this是JavaScript语法中的一个关键字,为什么要用this呢?它能做什么呢?为什么很多前辈和一些书籍,文章中都会告诉我:
this是个重点,稍有不慎就会搞错。
真的吗?好吧,那我也学学看~
01. 我遇到的问题
我见到过的this,一般出现在函数
中。这让我很容易见文会意
.它是不是表示:当前这个函数
。this嘛?翻译过来不就是这个
的意思,所以它就表示当前的这个函数。
这是我最初的理解。哎呀,这有什么难的?看来他们说的危言耸听了吧。
接下来,我自己写一些测试代码,再加深一下对this
的认识。
var a = 'global'
var b = 0
function test1(paramA, paramB) {
var a = 'inner'
var b = 1
console.log(this.a)
console.log(this.b)
}
test1()
像上面的代码,在最外层和test1函数中都声明了相同名称的变量:a
、b
代码最后一行调用函数时,执行里面的代码,要打印的东西分别是this.a
,this.b
.此时是在函数内部,那么this毫无疑问应该指向test1函数作用域的。
自然得到的结果是:inner和1
然而,让我大跌眼镜的是,居然输出的global和0
啊!为什么呀?跟外面的a,b
有什么关系?
就这样由一个不能理解的问题入手,我开始了this的学习之旅。
02. this的理解
我是一名coder,在学了很多东西后,自然的形成了一种认识: 每一个技术,都不会凭空(无缘无故)的产生,它的出现理应是为了解决一些问题的,并且有自己的适用范围。
因此,关于this我也是这么考虑的。
首先,我其实到现在都很疑惑,到底JavaScript算是一门面向对象的编程语言呢?还是,有观点也认为它其实是面向函数的语言。甚至说它其实两者都不是。
如此种种,算了我也不再细究。可能是这门语言,不是那么的纯粹。所以,导致如果我们从不同的视角来看,它都有可能符合其中的一些特性。
而this
我认为则是从面向对象这个视角下,存在的一种机制。
为什么要有this
既然现在是把JS当成一门面向对象的语言,那么在设计代码解决问题时,自然会把所有东西都看成是一个个的对象。
基于对象,我们会对其进行静态的属性描述,以及动态的行为上的设置。而这些行为,我们又一般会通过一个个的函数来呈现。
// 定义一个猫咪的对象
var cat = {
name: '猫咪', // 名称
callDesc: '喵~喵~', // 猫咪是怎么叫的
call: function() { // 描述猫咪叫的行为
console.log(`${cat.name}的叫声是:${cat.callDesc}`)
}
}
// 定义一个小狗的对象
var dog = {
name: '小狗',
callDesc: '汪~汪~',
call: function() {
console.log(`${dog.name}的叫声是:${dog.callDesc}`)
}
}
上面的代码,我们分别定义了一个cat、dog
的对象,其内部都有名称,以及叫声的描述和行为。哪怕是现在只有两个对象,也能看到call
这个方法,我们完全可以抽出去形成一个单独的函数,这样后续哪怕再添加其他对象,就都可以复用了。
// 可复用的“动物叫”这个行为的函数
function animalCall(context) {
console.log(`${context.name}的叫声是:${context.callDesc}`)
}
// 这样的话,上面的cat、dog对象,就可以变成这样了
var cat = {
name: '猫咪', // 名称
callDesc: '喵~喵~', // 猫咪是怎么叫的
call: animalCall(cat)
}
var dog = {
name: '小狗',
callDesc: '汪~汪~',
call: animalCall(dog)
}
cat.call() // 输出,猫咪是怎么叫的
dog.call() // 输出,小狗是怎么叫的
// 可复用的“动物叫”这个行为的函数
function animalCall(context) {
console.log(`${context.name}的叫声是:${context.callDesc}`)
}
// 这样的话,上面的cat、dog对象,就可以变成这样了
var cat = {
name: '猫咪', // 名称
callDesc: '喵~喵~', // 猫咪是怎么叫的
call: animalCall(cat)
}
var dog = {
name: '小狗',
callDesc: '汪~汪~',
call: animalCall(dog)
}
cat.call() // 输出,猫咪是怎么叫的
dog.call() // 输出,小狗是怎么叫的
但是上面的代码还是存在一些问题,比如我们在使用animalCall
这个函数的时候,还需要给它传当前的对象。这样一是繁琐,二是在后面如果要给对象重起个名字,这块还得改。
所以,就有了一种更优雅的方式。也就是使用this。
// 使用this,定义可复用的函数
function animalCall() {
console.log(`${this.name}的叫声是:${this.callDesc}`)
}
// 这样的话,使用animalCall函数时,就可以什么都不用传了。
// 哪怕后面cat、dog名称修改了,也不需要再修改其他的东西了
var cat = {
name: '猫咪', // 名称
callDesc: '喵~喵~', // 猫咪是怎么叫的
call: animalCall()
}
var dog = {
name: '小狗',
callDesc: '汪~汪~',
call: animalCall()
}
cat.call() // 输出,猫咪是怎么叫的
dog.call() // 输出,小狗是怎么叫的
当使用了this后,使得代码会更加的简洁和优雅。
这时候,我就不禁有这样的思考:
之前对this的误解,是不是我只看到了它出现在函数中,刻板的把它理解成了函数的某种形式。因此,它的表示逃离不出函数的范围:比如误解成this指向函数自身;指向函数的作用域。
然而,并非如此。this它是基于对象下的对其行为描述的一种设计机制。
那么它其实就跟使用它的对象有关。
03. this的工作方式
平时,我们定义的函数,一般会被放到堆内存
中。当要调用它时,会把它压到一个执行栈中去运行,与此同时会创建一个执行记录,来协助函数的执行。里面会包含一些现场信息,而this此时也才会被创建,并包含在其中。
所以,我们看到this
是在运行时被创建的,它的具体指向是与调用位置有关。
对,没错。就是调用位置!
而绝不是声明位置!不是!不是!
绑定规则
那么根据调用位置的不同,this是有如下四种的绑定规则:
- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
下面,就对其一一进行讲解:
默认绑定
它是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。 ——来源自《你不知道的JavaScript上卷》
什么样的调用算是独立函数
调用呢?
function test2() {
console.log(this.a)
}
var a = 1
test2() // 1
如上面的代码所示,就是调用test2函数时,没有任何的修饰。此时在函数中应用的就是this的默认绑定。
注意
在应用默认绑定时,this的具体指向还会跟代码是否处于严格模式下有关联。
简单的说,就是:
- 严格模式下,不能将全局对象用于默认绑定,此时this会绑定到
undefined
- 非严格模式下,this就绑定到全局对象下
隐式绑定
当函数调用时,看它是否被包含在某个对象下。如果是的话,那么此时this就会应用隐式绑定,把它绑定到那个对象下。
比如:
var test03Obj = {
a: 1,
b: test03Fn
}
function test03Fn() {
console.log(this.a)
}
test03Obj.b() // 1
test03Fn这个函数是作为test03Obj这个对象下b属性的值,当我们用test03Obj.b()
方式调用时,就使用了隐式绑定,此刻this指向的就是test03Obj
这个对象。
注意
在这些情况下,隐式绑定会丢失,此时变成了默认绑定
- 函数别名
- 作为函数的参数进行传递
var a = 'global'
function test04() {
console.log(this.a)
}
var test04Obj = {
a: 'inner',
b: test04
}
// 把函数赋值给一个变量(相当于给函数起了个别名)
var newNameFn = test04Obj.b
newNameFn() // 'global'
在13行代码中,我们看到是把test04Obj.b
这个函数赋值给了newNameFn,它现在是test04Obj.b
的一个引用。但实际上,它指向的还是test04这个函数本身。
而我们在15行去调用这个newNameFn
是完全不带任何的修饰,此时它就变成了默认绑定。输出的结果自然就是global。
var a = 'global'
function test05() {
console.log(this.a)
}
var test05Obj = {
a: 'inner',
b: test05
}
function handlerTest05(callback) {
callback()
}
handlerTest05(test05Obj.b) // 'global'
在第16行时,test05Obj.b是作为一个参数被传递到了handlerTest05这个函数中,并且在这个函数内部,也会执行这个作为回调的,也就是test05这个函数。
然而,我们看到的结果,依然是输出了全局对象下的变量a的值。
所以,通过以上两个例子,依然是更能说明:this的绑定,是只跟调用位置有关。
哪怕这个过程中涉及到一些隐式绑定的使用方式,但它们都只是作为一个中间环节,服务于最终要调用的那个函数。
而那个最终的函数的调用位置是什么,就决定着this最终的指向。
显式绑定
有些时候,如果我们希望this能确定的绑定到某个对象下,那么就可以使用显式绑定。
到目前为止,可以使用函数自身提供的方法:
- call()
- apply()
- 以及bind()
来强制的把this绑定到某个对象下。
var a = 'global'
function test06() {
console.log(this.a)
}
var test06Obj = {
a: 'inner',
b: test06
}
test06.call(test06Obj) // 'inner'
// 或者是
test06.apply(test06Obj) // 'inner'
// 使用bind
var bindOfTest06 = test06.bind(test06Obj)
bindOfTest06() // 'inner'
new绑定
在了解new绑定之前,需要先明确一点。它和其他的面向对象编程语言中的使用new关键字,来实例化一个类是完全不同的。
在JavaScript中,它其实很简单。甚至可以说相当简陋和朴素。
它只是被new操作符调用的普通函数而已。
不信我们看,在使用new操作符来调用函数时,会自动执行下面的动作:
- 创建一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function test07(a) {
this.a = a
}
var newObj = new test07('new')
console.log(newObj.a) // 'new'
上面的代码可以理解为,当使用new操作符来调用函数时,会返回一个新的对象给变量newObj
,而这个新对象中包含了一个值为new
的a属性。
绑定规则的优先级
this的绑定规则是有上面的四种,但是当某个调用位置可以使用多种调用规则时,this又会指向什么呢?
关于这个问题,就不得不考虑绑定规则之间的优先级了。
todo:示例代码后续补充……
结论则是:
new绑定 > 显式绑定(apply、call、bind)> 隐式绑定 > 默认绑定
所以,当我们判断this的绑定时,可以按照如下顺序进行判断:
- 函数是否在new中调用?如果是的话,this绑定的就是新创建的对象。
- 函数是否通过call、apply或者bind调用?如果是的话,this绑定的就是指定的对象
- 函数是否在某个上下文对象中调用?如果是的话,this绑定的就是那个上下文对象
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
绑定的例外情况(后续补充)
this的词法(箭头函数)
在ES6中,我们新推出了一种定义函数的方式:就是箭头函数(它不是使用function关键字来声明的,而是用了被称为“胖箭头”的操作符的方式)
可能在此之前,开发者们苦this的复杂易错的绑定机制,久矣!
于是,JavaScript新的标准,从语言层面上就帮我们避开了这些问题。
箭头函数并不会应用上面讲到的四种绑定规则,而是根据当前的词法作用域来决定this。具体来说,就是会继承外层函数调用的this绑定。 —— 来自《你不知道的JavaScript上卷》