Js中关于this、apply、call、bind的深入探讨
引言
这又是一个面试经典问题~/(ㄒoㄒ)/~~也是 ES5
中众多坑中的一个,在 ES6
中可能会极大避免 this
产生的错误,但是为了一些老代码的维护,最好还是了解一下 this
的指向和 call
、apply
、bind
三者的区别。
this 的指向
在 ES5
中,其实 this
的指向,始终坚持一个原理:this 永远指向最后调用它所在函数的那个对象。
举个栗子:
1 |
|
用上面这个栗子来解释原理:this
在fn
函数中,fn
函数被调用,而调用它的对象是a
,所以this
指向对象a
。
接下来利用几种 JS
中的函数调用来具体说明 this
的指向:
JS 中的函数调用
函数调用的方法一共有 4
种[1]
- 作为一个函数调用
- 函数作为方法调用
- 使用构造函数调用函数
- 作为函数方法调用函数(call、apply)
作为一个函数调用
例 1:
1 |
|
这样一个最简单的函数,不属于任何一个对象,就是一个函数,这样的情况在 JavaScript
的在浏览器中的非严格模式
默认是属于全局对象 window
的,在严格模式,就是 undefined
。
但这是一个全局的函数,很容易产生命名冲突,所以不建议这样使用。
这个时候我们再来看例 2:
例 2:
1 |
|
这里的 innerFunction()
的调用就属于这一种调用方式:作为一个函数调用(它就是作为一个函数调用的,没有挂载在任何对象上,所以对于没有挂载在任何对象上的函数,在非严格模式下 this
就是指向 window
的)
函数作为方法调用
更多的情况是将函数作为对象的方法使用。比如例 3:
1 |
|
这里定义一个对象 a
,对象 a
有一个属性(name
)和一个方法(fn
)。
然后对象 a
通过 .
方法调用了其中的 fn
方法。
然后我们一直记住的那句话“this 永远指向最后调用它所在函数的那个对象”,所以在 fn
中的 this
就是指向 a
的。
我们做一个小小的改动:
例 4:
1 |
|
这里打印 Cherry
的原因也是因为刚刚那句话“this 永远指向最后调用它所在函数的那个对象”,最后调用它的对象仍然是对象 a
。
我们再来看一下这个例子:
例 5:
1 |
|
这里为什么会打印 undefined
呢?这是因为正如刚刚所描述的那样,调用 fn
的是 a
对象,也就是说 fn
的内部的 this
是对象 a
,而对象 a
中并没有对 name
进行定义,所以 log
的 this.name
的值是 undefined
。
这个例子还是说明了:this 永远指向最后调用它所在函数的那个对象,因为最后调用 fn
的对象是 a
,所以就算 a
中没有 name
这个属性,也不会继续向上一个对象寻找 this.name
,而是直接输出 undefined
。
再来看一个比较坑的例子:
例 6:
1 |
|
这里你可能会有疑问,为什么不是 Cherry
,这是因为虽然将 a
对象的 fn
方法赋值给变量 f
了,但是没有调用
,再接着跟我念这一句话:“this 永远指向最后调用它所在函数的那个对象”,由于刚刚的 f
并没有调用,所以 fn()
最后仍然是被 window
调用的。所以 this
指向的也就是 window
。
由以上五个例子我们可以看出,this
的指向并不是在创建的时候就可以确定的,在 es5
中,永远是this 永远指向最后调用它所在函数的那个对象。
使用构造函数调用函数
如果函数调用前使用了
new
关键字, 则是调用了构造函数。这看起来就像创建了新的函数,但实际上JavaScript
函数是重新创建的对象。
1 |
|
这就有要说另一个面试经典问题:new 的过程了,(ಥ_ಥ)
这里就简单的来看一下 new 的过程吧:
伪代码表示:
1 |
|
- 创建一个空对象
obj
; - 将新创建的空对象的隐式原型指向其构造函数的显示原型。
- 使用
call
改变this
的指向 - 如果无返回值或者返回一个非对象值,则将
obj
返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
所以我们可以看到,在new
的过程中,我们是使用call
改变了this
的指向。
作为函数方法调用函数
在
JavaScript
中, 函数是对象。JavaScript
函数有它的属性和方法。call()
和apply()
是预定义的函数方法。 两个方法可用于调用函数,两个方法的第一个参数必须是对象本身。 在JavaScript
严格模式(strict mode)下, 在调用函数时第一个参数会成为this
的值, 即使该参数不是一个对象。在JavaScript
非严格模式(non-strict mode)下, 如果第一个参数的值是null
或undefined
, 它将使用全局对象替代。
例7:
1 |
|
这个简单一点的理解可以理解为“匿名函数的 this 永远指向 window”,你可以这样想,还是那句话this 永远指向最后调用它所在函数的那个对象,那么我们就来找最后调用匿名函数的对象,这就很尴尬了,因为匿名函数名字啊,笑哭,所以我们是没有办法被其他对象调用匿名函数的。所以说 匿名函数的 this 永远指向 window。
如果这个时候你要问,那匿名函数都是怎么定义的,首先,我们通常写的匿名函数都是自执行的,就是在匿名函数后面加 ()
让其自执行。其次就是虽然匿名函数不能被其他对象调用,但是可以被其他函数调用啊,比如例7中的 setTimeout
。
怎么改变 this 的指向
改变 this
的指向我总结有以下几种方法:
- 使用
ES6
的箭头函数 - 在函数内部使用
_this = this
- 使用
apply
、call
、bind
new
实例化一个对象
箭头函数
如例7中所示,在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout
的对象是 window
,但是在 window
中并没有 func1
函数。
我们在改变 this
指向这一节将把这个例子作为 demo
进行改造。
众所周知,ES6
的箭头函数是可以避免 ES5
中使用 this
的坑的。箭头函数的 this
始终指向函数定义时的 this
,而非执行时。箭头函数需要记着这句话:
箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。
例8:
1 |
|
在函数内部使用 _this = this
如果不使用 ES6
,那么这种方式应该是最简单的不会出错的方式了,我们是先将调用这个函数的对象保存在变量 _this
中,然后在函数中都使用这个 _this
,这样 _this
就不会改变了。
1 |
|
这个例子中,在 func2
中,首先设置 var _this = this
;,这里的 this
是调用 func2
的对象 a
,为了防止在 func2
中的 setTimeout
被 window
调用而导致的在 setTimeout
中的 this
为 window
。我们将 this
(指向变量 a
) 赋值给一个变量 _this
,这样,在 func2
中我们使用 _this
就是指向对象 a
了。
使用 apply、call、bind
使用 apply
、call
、bind
函数也是可以改变 this
的指向的,原理稍后再讲,我们先来看一下是怎么实现的:
使用 apply
1 |
|
使用 call
1 |
|
使用 bind
1 |
|
apply、call、bind 区别
刚刚我们已经介绍了 apply
、call
、bind
都是可以改变 this
的指向的,但是这三个函数稍有不同。apply()
方法调用一个函数, 其具有一个指定的this
值,以及作为一个数组
(或类似数组的对象
)提供的参数; apply
和 call
的区别是 call
方法接受的是若干个参数列表,而 apply
接收的是一个包含多个参数的数组。
apply:
1 |
|
call:
1 |
|
我们先来将刚刚的例子使用 bind 试一下:
1 |
|
bind()
方法创建一个新的函数, 当被调用时,将其this
关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。所以我们可以看出,bind
是创建一个新的函数,我们必须要手动去调用:
1 |
|
总结
关于this
、apply
、call
、bind
的知识的掌握还需要经常地去查看熟悉。
参考
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!