理解JavaScript中的this、call、apply、bind
this作为JavaScript中最难理解的知识点之一,掌握了this的用法就相当于JavaScript入门,所以掌握this的指向对我们的开发很有好处。
this的指向
this允许在复用不同的函数时调用不同的函数上下文,所以我们在判断this的指向时首先应该看得就是函数在哪儿被调用,在ES5中,this的指向始终坚持一个原理:this永远指向最后调用函数的那个对象,其实当你理解了这句话,基本上就已经理解了this的使用。
在《JavaScript权威指南》一书中,对this有明确的定义
this是一个关键字,不是变量,也不是属性名,JavaScript的语法不允许给this赋值,和变量不同的是,this没有作用域的限制,嵌套的函数不会从调用它的函数中继承this,如果嵌套函数作为方法调用,其this的值指向调用它的对象。
为了能更好的理解this的指向,下面我们来看几个例子:
例1:
1 | var a = 1; |
在上面的例子中,我们看到最后调用函数b()的前面没有调用对象,那么调用对象就是全局对象(此时为window), 相当于window.b(), 而this又指向最后调用它的那个对象,所以最后输出1;
注意: 非严格模式下全局对象为window, 在严格模式下全局对象就是undefined, 此时调用控制台报错: Uncaught TypeError: Cannot read property ‘name’ of undefined
例2:
1 | var name = java; |
通过以上两个例子的对比, 相信你很快就能理解this的指向问题了, 上面的例子中最后调用函数fn()的是对象b, 所以对象理所当然的指向b, 最后输出name的值即为JavaScript。
下面我们在做一个小小的改动
例3:
1 | var name = java; |
这里打印的结果不变的原因还是因为this永远指向最后调用它的那个对象,所以this还是指向b。
下面我们再来看一个比较容易出错的例子
例4:
1 | var name = java; |
看到这儿你也许会疑惑, 为什么此时输出不是java? 这里涉及到另外一个知识点了–this的指向永远是在运行时确定的。所以我们再看一下,由于刚定义的时候函数并没有被调用,当调用函数的时候,函数还是被window对象调用,所以this的指向也就是window。
相信看到这儿,你应该已经知道如何来判断this的指向了。下面我们来看看如何改变this的指向。
怎么改变this的指向
改变this的指向总结了下有以下几种方法:
- 在函数内部将this的值保存在一个变量中
- 使用es6 箭头函数
- 使用new 关键字
- 使用call、apply、bind
在函数内部将this的值保存在一个变量中
这种方法应该是最容易理解的了,我们只需要将this的值保存在一个变量_this中,然后在函数中都是用这个_this, 这样不管怎么调用函数_this都不会变
例5:
1 | var o = { |
在这个例子中,首先函数内部的this指向对象o,并且我们将this的值保存在变量_this中,所以console.log(this === o)打印true,但是在函数f()内部,并没有明确的对象调用函数f(),按照我们上面的说法,此时调用f()的就是全局对象window(this指向window), 所以console.log(this === o) 打印结果为 false, 在函数f()内部, _this的值却还是外部函数this的值并没有发生改变仍为对象o, 所以console.log(_this === o) 的结果为true。
使用es6的箭头函数
关于es6的箭头函数, 所涉及到的知识点太多, 另开一篇博客讲解…
使用new 关键字
JavaScript的new关键字是众多运算符其中的一个, 让我们来看看下面一个例子:
例6:
1 | function Person () { |
每当用new调用函数时, JavaScript解释器都会在底层创建一个全新的对象并把这个对象当做this。如果用new调用一个函数, this会自然地引用解释器创建的新对象。
使用call、apply、bind
下面先来看一个例子
例7:
1 | function fn () { |
使用call
如果fn()不是user的函数,知识一个独立的函数,那我们怎么才能fn()函数调用的this指向user对象呢?当然我们不能像以前那样简单的使用user.fn(),因为此时fn()不是user的函数。这个时候call出现了
“call” 是每个函数都有的一个方法,它允许你在调用函数时为函数指定上下文。
对于例7,我们可以这样调用
1 | fn.call(user) |
强调一下,call是每个函数都有的一个属性。并且call通常可以传递多个参数,默认第一个参数会作为函数调用时的上下文, 换句话说, this将会指向传递给call的第一个参数。
例8:
1 | function fn (age, language) { |
虽然call()能够有效的改变函数执行上下文,但你可能也注意到了, 如果有多个参数,就需要一个一个的传递, 这样显得比较麻烦。所以这个时候apply()方法就出来了。
使用apply
call方法和apply方法作用是一样的, 都是在特定的作用域中调用函数, 等于设置了函数体内this对象的值,以扩充函数赖以运行的作用域。
下面还是来看一个例子:
例9:
1 | function fn (age, language) { |
可以看到,例9就是简单的将例8进行了一个改造,但是两个函数的输出结果却完全一样。
所以我们可以总结下call和apply两个方法:
- 每个函数都有这两个非继承而来的方法。
- 都可以改变函数的运行作用域
- call 可以接受多个参数,其中第一个参数是函数的运行作用域,而后每一个需要传入的参数都必须列举出来; apply方法接受两个参数,第一个同call方法,第二个参数接受参数数组,如果第二个参数不是一个有效数组或不是arguments对象,那么将导致一个TypeError,如果没有提供任何一个参数,那么全局对象将用作this的指向。
下面我们再来看看bind方法
使用bind
bind和call、apply类似,都可以改变函数的运行作用域,下面我们先来看一个例子
例10:
1 | var user = { |
当运行上面这个例子,我们会发现什么都没有打印,这时就开始疑惑了,难道代码写错了???其实不是,因为bind方法返回的是一个修改后的函数,所以我们来稍作修改:
1 | var user = { |
将bind方法稍作修改,代码结果就和我们预想的一样。同样bind也可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。这里就不做具体的举例了。
再说说call、apply和bind的区别
call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加,这就是他们之间的区别。
本文完
PS: 好久没写博客了,反思了下自己,好习惯不能落下,以后每周写一篇博客