this是一个关键字,不是变量,也不是属性名,JavaScript的语法不允许给this赋值。和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this。如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象就是undefined(非严格模式下是全局对象(即window对象),严格模式下是undefined)。很多人误以为调用嵌套函数时,this会指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this的值保存在一个变量中,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this。
记住一点:函数中的this总指向调用它的对象。(金句)
对应函数的四种调用方法(函数调用、方法调用、通过call()和apply()方法的间接调用和构造函数调用),this有四种绑定方式,即:默认绑定、隐式绑定、显式绑定和new绑定。
默认绑定
当一个函数没有明确的调用对象的时候,也就是单纯作为独立函数调用的时候,将对函数的this使用默认绑定:绑定到全局的window对象。
注:以函数形式调用的函数通常不使用this关键字,不过,此时的“this”可以用来判断当前是否是严格模式。
代码说明:
本代码第16行是IIFE(立即执行函数)的写法。
严格模式下,this指向undefined,所以第13行代码打印出false,14行打印出true
非严格模式下(即注释掉第9行代码),this指向window,所以第13行代码打印出true,14行打印出false。
注:除非特别说明,本文的代码默认运行在非严格模式下。
一、简单的函数调用写法
代码说明:
第19-24行代码很容易就能看懂,第24行就是普通的函数调用,a()作为独立函数调用,此时,this指向window或者undefined。
二、嵌套函数调用
此时的代码你看懂了吗?b()是什么方式调用?obj.foo()是什么方式的调用?
代码说明:
第36行,b()依然是独立调用,它只是嵌套在foo函数中。所以它的指向依然是是默认指向(window or undefined),而全局变量a的值是0(从第27行代码可看出来),所以此时34行代码打印出 0.
Q:如果将34行代码复制到36行代码的下面一行呢?会出现什么效果?
运行代码会发现第37行代码打印出1,这是为什么呢?
这就是马上要讲的函数的另一种调用方式-----方法调用,方法调用引发this的隐式绑定。
隐式绑定
首先解释下函数的方法调用
一个方法无非是个保存在一个对象的属性里的JavaScript函数。白话说,就是在一个对象中定义函数,调用该函数就是方法调用,还记得前文提到的金句吗?“函数中的this总指向调用它的对象”,此时的this就指向调用该函数的对象。
还是用上面的代码(第27行---40行)
代码说明:
先说一下第一个this:
第27行定义了一个变量a,很显然,此时的a是全局变量。第28行定义了一个obj对象,在obj对象中,第29行定义了一个变量a,此时,a是obj对象中的局部变量,第30行定义一个foo函数,这里的foo函数作用域也是obj对象,第32行是在foo函数里定义了一个b函数,这里的b函数的作用域是foo函数,第36行是在foo函数中,调用b函数,注意哦,此时的调用是独立调用哦,就是普通的函数调用哦,所以当40行调用foo函数时,b函数被调用,b函数里的this指向全局,所以,打印出 0 。
现在介绍第二个this:
当代码流执行到第40行时,调用obj对象中的foo函数,正如前文所讲,此时的调用就称作函数的方法调用。函数的使用方法调用时,this指向调用该方法的对象,在这里就是this指向obj对象。而obj对象中,a的值在第29行被赋值为1,所以第37行代码打印出 1 。
隐式丢失
前文之所以称作隐式绑定,是因为此时的this隐式绑定在obj对象上。所以this的指向是obj对象。然而,当绑定至上下文对象的函数被赋值给一个新的函数,或者传递给回调函数时,函数中的 this容易丢失掉绑定对象,此时this执行默认绑定规则。
一、赋值丢失(绑定至上下文对象的函数被赋值给一个新的函数)
代码说明:
第56行定义全局变量a,并赋值为1,57行定义obj对象,58行在obj对象中定义局部变量a,并赋值为2,59行在obj对象中定义foo函数。63行很奇怪是吧,为了方便介绍,我们将其等价成a=b,
深度解释“a=b”赋值语句:
第一步,计算表达式a,得到a的地址refa;
第二步,计算表达式b, 得到b的值valueb;
第三步:将valueb赋给refa
第四步:返回valueb
也就是说,obj.foo = obj.foo会返回第二个obj.foo所指向的函数表达式,所以第63行代码就等价于
很显然,64行代码时定义了一个新的函数,并且是匿名函数,而在红宝书(P182)中介绍过,匿名函数的执行环境具有全局性,所以此时的this指向全局,即打印出 1.
再深入理解一下,你知道为什么说第64行是定义了一个新函数吗?
在64行的前面插入代码console.log(obj.foo),你会发现浏览器会打印出一个函数表达式,也就是obj.foo是一个函数表达式,第64行是把该函数表达式赋值给fooo,所以此时的fooo是一个函数
上述代码等价于
注意:此时浏览器打印出 2是因为70行代码将变量重新赋值成2,覆盖了68行的赋值,即此时的a仍然是全局变量。上述代码是一个函数的独立调用。所以此时的this指向全局。
要不要再尝试一下?现在把65行改成var fooo = obj.foo()
可以发现此时第66行报错,这是为什么呢?聪明的同学已经发现,obj.foo()是一个函数的方法调用,此时的返回值是obj中的局部变量 2,此时的fooo是一个被声明的变量,那么它就不再是一个函数的形式了,所以也就不能使用fooo()了。
二、传参丢失
1,语言内置的函数传参,比如使用setTimeout()时
大家都知道,setTimeout()的用法如下
例如:setTimeout(function(){....}, 3000);
在经过3000ms后执行{....}的内容,第83行代码的执行过程是先将obj.foo赋值给setTimeout的第一个参数,问题就出现在这里,请看前文的赋值丢失情况介绍,是不是同一个道理?
2,自定义函数传参时
其实这就是第一种情况的变种,实际上参数传递就是一种隐式赋值
显式绑定
函数的间接调用引发this的显示绑定。
JavaScript中的函数也是对象,和其他JavaScript对象没什么两样,函数对象也可以包含方法,其中的两个方法call()和apply()可以作为任何对象的方法来调用。两个方法都允许显式指定调用所需的this值。
bind()是ES5中新增的方法,当在函数foo()上调动bind()方法并传入一个对象obj作为参数,这个方法将返回一个新的函数,当然此时this也便是方法调用了,所以会指向当前对象obj。
注:普通的显式绑定无法解决隐式丢失问题
硬绑定
硬绑定是显式绑定的一个变种,固定this的指向,上例中的bind()是硬绑定的内置函数
代码说明:
在bar函数内部手动调用foo.call(obj)。因此,无论之后如何调用函数bar,它总会手动在obj上调用foo
new绑定
如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。
构造函数的四个步骤:
第一步:创建一个新对象
第二步:将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
第三步:执行构造函数中的代码(为这个新对象添加属性)
第四步:返回新对象