this指向问题可谓是js中一个老生常谈的话题了,不论是在实际开发还是前端面试中,this指向问题都或多或少让人“头疼”,因为this的指向涉及的规范实在是太繁多了......但这并不是我们放弃的理由(手动狗头),毕竟只有彻底掌握它所涉及的知识点,才能在开发中活学活用嘛!今天我们就从this指向问题展开,看一看它到都有哪些规则:)
一、一句话总结this指向
关于this的指向,有一种被大家所熟知的说法是:**谁调用它,this就指向谁**,即this的指向是在调用的时候确定的,而不是声明的时候。这种说法虽然不是百分百准确,但也概括了绝大部分场景。
二、执行上下文
既然在总结this指向时说到了“调用”,我们就不得不提到js中一个基础的概念————**执行上下文**(execution context)了。
js中的执行上下文指的是当前执行环境中的变量、函数声明、参数、作用域链、this等信息,分为全局执行上下文和函数执行上下文,他们俩的区别在于全局执行上下文只有一个,函数执行上下文在每次调用函数时候会创建一个新的函数执行上下文。执行上下文也有自己的生命周期,分为创建和执行阶段,创建阶段做的事情包括:生成变量对象、创建arguments、扫描函数声明、扫描变量声明、建立作用域链、**确定this的指向**,执行阶段做的事情大致包括变量赋值、函数引用和执行其他代码等。
一段代码的运行可能会产生多个执行上下文,那怎么去管理这多个执行上下文呢?————执行上下文栈(调用栈),我们知道栈是一种先入后出的数据结构,利用这一特点,js代码可以保证代码执行过程中保持执行时所在的执行上下文环境。
上面提到,执行上下文的创建阶段会确定 this 的指向。因此,关于this指向我们可以确定的一点是:this 的指向是在**调用函数**时根据**执行上下文**所**动态确定**的。
### 三、this绑定规则
1、默认绑定————绑定至全局对象window
观察以下代码:
![image.png](http://ttc-tal.oss-cn-beijing.aliyuncs.com/1628490252/image.png)
在代码中,foo()是直接使用**不带任何修饰的函数引用**进行调用的,因此其内部this为默认绑定规则,即**独立函数调用**时其this会指向全局对象,用之前执行上下文的角度来说,独立函数调用时它处于全局执行上下文。需要注意的是,如果使用严格模式(strict mode),不能将全局对象用于默认绑定,因此this会绑定到undefined。
2、隐式绑定
观察以下代码:![image.png](http://ttc-tal.oss-cn-beijing.aliyuncs.com/1628491149/image.png)
可以看到foo是单独声明的,之后被当作引用属性添加到了obj中。从声明方式上来看,foo函数严格来不属于obj对象,然而,obj.foo()这种调用方式会使用obj执行上下文来引用foo函数,可以说函数foo被调用时obj对象“拥有”或者“包含”它。当函数引用有执行上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
为什么称之为隐式绑定呢?因为我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接绑定到这个对象上。
ps: 对象属性引用链中**只有上一层或者说最后一层在调用位置中起作用**,这条规则能使我们在嵌套调用中定位this的指向。举个例子:
![image.png](http://ttc-tal.oss-cn-beijing.aliyuncs.com/1628491663/image.png)
3、隐式丢失
观察以下代码:
![image.png](http://ttc-tal.oss-cn-beijing.aliyuncs.com/1628491804/image.png)
看起来bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,符合默认绑定规则,因此应用了默认绑定。类似情况还有函数作为参数传递,也属于一种隐式赋值。
4、显式绑定
不同于隐式绑定,如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?此时可以使用**函数的**call/apply/bind方法(使用方式:f.call()/f.apply()/f.bind())。
这三个方法是如何工作的呢?它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将this绑定到该对象。因为可以直接指定this的绑定对象,因此我们称之为显式绑定。总结来说,他们都是用来改变相关函数 this 指向的,call/apply 是直接进行相关函数调用;bind 不会执行相关函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this 指向,手动调用即可。
5、new绑定
我们知道,使用new来调用函数时,会自动执行下面的操作:
(1)创建(或者说构造)一个全新的对象;
(2)这个新对象会被执行[[Prototype]]连接;
(3)这个新对象会绑定到函数调用的this;
(4)如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
使用new来调用函数时,会构造一个新对象并把它绑定到函数调用中的this上。
6、优先级
结论:new绑定>显式绑定(call、apply、bind)>隐式绑定>默认绑定
因此可以按照下面的顺序来进行判断:
(1)函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
(2)函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
(3)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
(4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到window。
四、箭头函数
之前介绍的绑定规则已经可以包含所有正常的函数,但是ES6中新增了一种特殊函数类型:箭头函数。箭头函数并不是使用function关键字定义的,而是使用操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么),这其实和ES6之前代码中的self = this机制一样。
五、小结
this 的指向是在调用函数时根据执行上下文所动态确定的。
要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置,可以按照下面的顺序来进行判断:
(1)函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
(2)函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
(3)函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
(4)如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到window。
箭头函数是个例外,它会继承外层函数调用的this绑定(无论this绑定到什么)。
注:参考《你不知道的JavaScript》