全面解析JavaScript中的this
在写这篇文章时,我不知道还有多少人还没有理解JavaScript中的this。this究竟指向哪里?这个问题一直困扰着我。
对this的误解
指向自身:this并不像我们所想的那样指向函数本身
通过下面代码,我们想要记录一下foo被调用的次数
1 | function foo(num) { |
从上述代码结果中得出:foo(…)确实被调用了4次,但foo.count仍然是0,这又是为什么呢?
在执行foo.count = 0
时,的确向函数对象foo添加了一个count属性,但在函数内部代码this.count++
中的this并不是指向那个函数对象,在这里我可以明确的告诉你,this.count++
这行代码应该这样理解:在无意中创建了一个全局变量count,然后将一个没初始化的count变量进行运算,所以得到的结果就是Nan
。如果你想深入探究的话,继续看下去,你自己就能得出答案。
指向函数的作用域:一般来说,this在任何情况下都不指向函数的词法作用域
通过下面代码,使用this来隐式引用函数的词法作用域
1 | function foo() { |
首先,这段代码试图通过this.bar()
来引用bar()函数,如果说this指向foo函数的作用域,而在foo函数作用域内并不存在bar函数,那么执行this.bar()
就应该报错了,但是没有报错,代码继续往下执行,则证明this不指向foo函数的作用域。
此外,在执行this.bar()
时,控制台输出的this.a
结果为undefined,更加证明了this并不指向foo函数的作用域,因为this指向的是foo函数的作用域,那么输出的this.a
结果应该为2
this到底是什么
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。
学习this的第一步是明白this既不指向函数自身也不指向函数的词法作用域,this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用、怎么被调用。
this绑定规则
要搞清除this是什么,你必须找到函数调用位置,然后判断需要应用下面四条规则中的哪一条。我们首先会分别解释这四条规则,然后解释多条规则都可用时它们的优先级如何排列。
默认绑定
这最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
思考一下下面的代码:
1
2
3
4
5function foo(){
console.log(this.a)
}
var a = 2;
foo(); //2代码的执行结果是在控制台输出2,也就是说调用foo()时,this.a被解析成了全局变量a。为什么?因为在本例中,函数调用时应用了this的默认绑定,因此this指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()是如何调用的。在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
隐式绑定
这条规则是要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
思考下面代码:
1
2
3
4
5
6
7
8
9
10function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2首先需要注意的是foo()的声明方式,及其之后是如何被当作引用属性添加到obj中的。但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象。然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象“拥有”或者“包含”它。无论你如何称呼这个模式,当foo()被调用时,它的前面确实加上了对obj的引用。
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因为调用foo()时this被绑定到obj,因此this.a和obj.a是一样的。
值得注意:对象属性引用链中只有上一层或者说最后在调用位置中起作用,举例来说:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42显示绑定
我们可以固定this,使用apply,call方法指定this。这两个方法是如何工作的呢?它们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。
思考下面的代码:
1
2
3
4
5
6
7
8
9function foo() {
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj); // 2通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。
使用bind方法实现硬绑定,它的用法如下:
1
2
3
4
5
6
7
8
9
10
11
12function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5bind(..)会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文并调用原始函数。
new 绑定
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建一个全新的对象
- 这个新对象会被执行[[prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
我们现在关心的是第1步、第3步、第4步,暂时忽略第2步
思考下面代码:
1
2
3
4
5
6function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。new是最后一种可以影响函数调用时this绑定行为的方法,我们称之为new绑定。
绑定规则的优先级
现在我们已经了解了函数调用中this绑定的四条规则,你需要做的就是找到函数的调用位置并判断应当应用哪条规则。但是,如果某个调用位置可以应用多条规则该怎么办?为了解决这个问题就必须给这些规则设定优先级,这就是我们接下来要介绍的内容。
毫无疑问,默认绑定的优先级是四条规则中最低的,所以我们可以先不考虑它。
隐式绑定和显式绑定哪个优先级更高?我们来测试一下:
1 | function foo() { |
可以看到,显式绑定优先级更高,也就是说在判断时应当先考虑是否可以存在显式绑定。
现在我们需要搞清楚new绑定和隐式绑定的优先级谁高谁低:
1 | function foo(something) { |
可以看到new绑定比隐式绑定优先级高。但是new绑定和显式绑定谁的优先级更高呢?
看一下下面代码:
1 | function foo(something) { |
出乎意料!bar被硬绑定到obj1上,但是new bar(3)并没有像我们预计的那样把obj1.a修改为3。相反,new修改了硬绑定(到obj1的)调用bar(..)中的this。因为使用了new绑定,我们得到了一个名字为baz的新对象,并且baz.a的值是3。
判断this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:
- 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
1 | var bar = new foo() |
- 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
1 | var bar = foo.call(obj2) |
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
1 | var bar = obj1.foo() |
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
1 | var bar = foo() |
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。不过……凡事总有例外。
例如:call、apply指定的对象为null
,箭头函数。
如果你把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则:
1 | function foo() { |
箭头函数中的this
箭头函数并不是使用function关键字定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
看看箭头函数绑定的this
1 | function foo() { |
foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1, bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)
最后,上面代码大多借鉴《你不知道的JavaScript上卷》,里面关于this指向问题说的很清楚,大家可以去看一下。