this
是JavaScript世界最让人迷惑的关键字之一,很多人对它一知半解,本文希望详细的把this
说明白。
为什么要有this?
JavaScript 允许在函数体内部,引用当前执行上下文的其他变量。
1 | function func() { |
如上代码,函数func
引用了当前执行上下文的变量a
,问题是这个函数func
可以在任意其他执行上下文中被调用,因此这个a
可能就指向不同了。正因为如此,JS引擎需要有一个机制,可以依靠其
优雅地、准确地指向当前代码运行时所处的上下文环境(context)。
因此便催生了“this”。
何谓”优雅地“?
1 | //假设有个对象名字很长,而且有可能会改名 |
iAmALongLongLongNameObject
的方法func2
使用了this
关键字,是不是优雅多了?然后即使以后对象名字变化,func2
内部的代码也不用改变。func1
这种确实也可以实现与func2
同样的功能,但是就显得丑陋、不灵活了。
何谓“准确地”?
this
可以准确地指向(某个对象)而不会产生歧义。
1 | //全局变量 |
与“Java等高级语言的this
会指向对象的实例本身”不同,JavaScript的this
指向函数的调用位置的对象,也即调用该函数的对象。你需要知道,JavaScript中所有的函数都有属性,就如对象有属性一样。函数执行阶段(也即执行上下文的执行阶段)会获取this
属性的值,此时this
就是一个变量,储存着调用该函数的对象的值。
1 | var a = "coffe"; |
上面代码中,func
的调用者未通过点操作符.
指明,那它的调用者就是默认的全局对象window
,func
函数作为window
的一个方法,其体内的this.a
就是明确指代window
中属性a
,这种指向是准确而清晰的,不会有歧义。this
的这种灵活性在设计API的时候,会变得很方便和容易被复用。
调用位置
调用位置就是函数在代码中被调用的位置,而不是声明的位置。研究调用位置,也即搞清“由谁在哪调用了这个函数”的问题。搞清楚了调用位置,才能准确地找到this
的指向。
要找到调用位置,最重要的是要分析是被谁、在哪调用。
1 | var module = { |
如上代码,要找到函数getX
的调用位置,需要先看哪儿调用了它,很明显,有函数有两处位置调用了函数getX()
,接下来分析是谁调用了它。
- 作为
module
对象的getX
方法被调用。 这种情况被谁调用?很明显是被对象module
调用,this是指向module
。module
对象里面有一个属性x
,它的值是1891
,因此console.log(module.getX())
输出1891
。 - 作为全局函数
getX
被调用。 这种情况是被谁调用?我们都知道全局函数可以看作为window
对象的方法,那么,很明显现在getX
是被当做全局对象window
的一个方法被调用。
我们搞清楚了调用位置之后,接下来就会着手判断this的指向。
先看看很多人对this指向的一些误解
this
既不指向函数自身也不指向函数的作用域,这之前是很多前端工程师容易误解的地方,现在澄清一下。
this
的指向,是在函数被调用的时候确定的,也就是执行上下文被创建时确定的;this
的指向和函数声明的位置没有任何关系,只取决于函数的调用位置(也即由谁、在什么地方调用这个函数);- 正因为在执行上下文的创建阶段
this
的指向就已经被确定了,在执行阶段this
指向不可再被更改。
1 | var obj = { |
this的指向规则
1. 默认指向
独立函数调用(无法应用后面其他指向规则时),this
指向全局对象window
。
1 | function func() { |
对于默认指向来说,决定this
指向对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this
会指向undefined
,否则this
会指向全局对象。
1 | function func() { |
1 | function func() { |
还有一种默认指向,就是在SetTimeout或SetInterval结合使用时。代码示例如下。
1 | var num = 0; |
可以发现在setInterval和setTimeout中传入函数时,函数中的this会指向window对象。
2. 隐式指向
隐式指向是日常开发中最常见的指向。
函数体内this
的指向由调用位置的调用者决定。如果调用者调用的函数,为某以个对象的方法,那么该函数在被调用时,其内部的this
指向该对象。
1 | function func() { |
对象属性引用链中只有最顶层或者说最后一层会影响调用位置,也就是说this
指向最终调用函数的对象。这句话可能说得比较拗口,其实简单通俗地说,this
指向最靠近被调用函数的对象,离得远的不是。举例来说:
1 | function func() { |
再来看看隐式丢失:
1 | function func() { |
3. 显式指向
JavaScript内置对象Function
的三个原型方法call()
、apply()
和bind()
,它们的第一个参数是一个对象,它们会把这个对象绑定到this
,接着在调用函数时让this
指向这个对象。
1 | var a = "makai"; |
另外,使用bind
可以修正SetTimeout和SetInterval的this指向:
1 | var num = 0; |
4. “new”操作符指向
在JavaScript 中,构造函数只是一些使用new
操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能算是一种特殊的类型(class),它们只是被new
操作符调用的普通函数而已。
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(或者说构造)一个全新的对象;
- 将构造函数的作用域赋给新对象(因此
this
就指向了这个新对象); - 执行构造函数中的代码(为这个新对象添加属性、方法等);
- 如果函数没有返回其他对象,那么返回这个新对象。
1 | function func(a) { |
可以将函数的返回值分成三种情况:
- 返回一个对象
- 没有返回值,即默认返回undefined
- 返回基本数据类型
1 | 1、返回一个对象 |
所以使用new绑定时,需要判断函数返回的值是否为一个对象,如果是对象,那么this会绑定到返回的对象上
如何利用规则判断this的指向
this
的指向判断,可以按照下面的优先级顺序来判断函数在某个调用位置应用的是哪条规则
1. 函数是否在new
中被调用(new
操作符指向)?
如果是的话,this
绑定的是新创建的对象。
1 | function func(name) { |
2. 函数是否通过call
、apply
、bind
显式指向?
如果是的话,this
指向的是call、apply、bind三个方法的第一个参数指定的对象。
1 | var obj1 = { |
3. 函数是否被当做某个对象的方法而调用(隐式指向)?
如果是的话,this
指向的是这个对象。
1 | var obj1 = { |
4. 若以上都不是的话,使用默认绑定。
如果在严格模式下,就绑定到undefined
,否则绑定到全局对象。
1 | var a = "coffe"; //为全局对象window添加一个属性a |
几个例外情况
1. 被忽略的this
null
或者undefined
作为this
指向的对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认指向规则。
1 | function func() { |
2. 间接引用
间接引用最容易在赋值时发生;间接引用时,调用这个函数会应用默认指向规则。
1 | function func() { |
3. 箭头函数
箭头函数并不是使用function
关键字定义的,而是使用被称为“胖箭头”的操作符 =>
定 义的。
箭头函数不遵守this
的四种指向规则,而是根据函数定义时的作用域来决定 this
的指向。何谓“定义时的作用域”?就是你定义这个箭头函数的时候,该箭头函数在哪个函数里,那么箭头函数体内的this就是它父函数的this。
看下面代码加深理解:
1 | function func() { |
这个特性甚至被mozilla的MDN称作“没有this”,这种说法很费解。其实应该这么理解:一般而言,this的指向是在函数运行之后才确定的,而箭头函数的this指向在定义时也即调用之前就定死了,在运行之后无法更改,那相当于当成一个固定值的变量,此时this失去了原来作为“指向当前代码运行时所处的上下文环境(context)”的意义,所以MDN说箭头函数没有了this,我觉得翻译成“把this阉割了”更贴切 🤣 。