在JavaScript
中,this
的指向是一个很复杂的问题,这篇文章将对this
指向问题进行一个总结。
前置知识:作用域与this
要想彻底明白this
的行为,就要明白作用域和this
的关系。
JavaScript
中采用词法作用域,即在函数声明时,就已经确定了其作用域,而与函数在何处进行调用没有关系;而this
是在运行时确定的,因此对于同一个函数而言,不同的调用方式会导致不同的this
绑定(暂时不考虑箭头函数,下文会单独讲)。
这一点对于理解this
很重要,希望大家谨记。
四种绑定方式
在各种书籍和博客上都会提到this
的绑定方式有四种,分别如下(暂不考虑箭头函数)
- 默认绑定
- 隐式绑定
- 显示绑定
new
绑定
1. 默认绑定
默认绑定是指,当函数独立调用时(即没有调用者),this
指向window
// 代码1.1
function foo() {
console.log(this)
}
foo() // window
而在严格模式下,this为undefined
// 代码1.2
'use strict'
function foo() {
console.log(this)
}
foo() // undefined
这种this
绑定形式很好理解,此外,值得注意的是,**IIFE
**也是函数独立调用的一种,其中的this也会指向window
// 代码1.3
(function () {
console.log(this) // window
})()
下面这种形式也是函数独立调用,this
指向window
// 代码1.4
function bar() {
console.log(this);
}
let obj = {
foo: function () {
bar();
}
}
obj.foo() // window
2. 隐式绑定
隐式绑定与默认绑定相对应,隐式绑定可以大致理解为谁调用,this
就指向谁,即this
指向当前执行上下文。
// 代码2.1
let obj = {
foo: function () {
console.log(this)
}
}
obj.foo() // obj
不止是上面这种形式,如下形式也符合隐式绑定规则
// 代码2.2
function foo() {
console.log(this)
}
let obj = {
foo: foo
}
obj.foo() // obj
上面的代码中,先在全局声明了foo
函数,之后将其赋值给obj
的foo方法。再次强调,this
是执行时确定,与作用域无关。因此,虽然函数foo
是在全局中声明的,但是调用者是obj,也就是执行时上下文是obj
对象,因此其this
指向obj
对象。
是不是感觉自己懂了?别急,再看看下边的代码
// 代码2.3
let obj = {
foo: function () {
console.log(this)
}
};
let bar = obj.foo
bar()
obj.foo()
答案是window
,答对了吗?接下来解释一下。
- 大家不要一看到函数,就开始在脑海中运行,只把它看作一个变量就行,不要考虑函数体里面的代码,等到函数执行时再进行分析。
- obj.foo方法声明部分,只需要理解为在堆内存中开辟了一块空间,并由obj.foo持有这块内存空间的引用,由于函数尚未执行,因此还没有确定
this
- 将
obj.foo
赋值给bar
,也就是将函数的引用拷贝一份给了bar bar
独立调用,因此this指向window
这也就是很多文章和书籍中提到的隐式丢失。
代码2.3中的this
隐式丢失是由于变量赋值导致的,此外,间接引用也会造成隐式丢失
// 代码2.4
function logThis() {
console.log(this)
}
let obj1 = { log: logThis }
let obj2 = {}
obj1.log() // obj1
(obj2.log = obj1.log)() // window
这是由于赋值表达式(obj2.log = obj1.log)
的返回值是函数logThis
的引用,上面的代码可以理解为
let retFn = (obj2.log = obj1.log)
retFn()
可以看出,这相当于函数独立调用,符合默认绑定的规则。
还有一种比较隐蔽的导致隐式丢失的情况:
// 代码2.5
let obj = {
foo: function () {
console.log(this);
}
}
function bar(cb) {
cb()
}
bar(obj.foo); // window
答案同样是window
,这就是由于参数传递导致的this
隐式丢失,接下来是分析过程。
- 声明
obj.foo
,在堆内存中开辟一块空间,并由obj.foo
持有对内存的引用 - 将
obj.foo
作为函数bar
的参数,bar
独立调用,函数执行过程如下所示- 在函数作用域内声明形参
cb
,并将实参obj.foo
赋值给形参cb
cb
独立调用
- 在函数作用域内声明形参
从以上的分析可以看出,参数传递导致的隐式丢失与变量赋值导致的隐式丢失,从本质上来说是一样的。
参数传递导致的this隐式丢失在JavaScript
内置API中也十分常见,比如:Array.prototype.map
,setTimeout
,ES6的Promise
构造函数,node中的readFile
等等。这些函数接受一个回调函数作为参数,并在某一时刻执行它,执行模式与下面的伪代码类似
// 代码2.5
function fn(cb) {
// {...代码...}
cb()
// {...代码...}
}
参数传递导致的隐式丢失比较隐蔽,不容易发现,需要额外留意。
3.显式绑定
最常用的显式绑定就是使用Function.prototype
上的三个方法:call, apply, bind
相信这三个方法大家用的也会比较多了,就不多做介绍了,有一点需要注意,就是通过以上三个方法绑定this
之后,就无法继续通过这三个方法继续改变this
指向了,也就是说,只有第一次绑定有效,后续绑定就会失败。
bind
有一个点需要注意一下,就是返回的bound
没有prototype
属性,但是可以用let ins = new bound()
来创建对象。并且ins instanceof bound
为true
,这一点规范中有提到,如果instanceof
的对象是一个bind
之后的函数,就会找原函数来进行操作。
除了上面提到的三个显式绑定的方法,还有另外两种绑定方式,我也不知道该怎么分类,姑且放到显式绑定里吧:事件的绑定和部分API提供的绑定方式。
第一种是事件处理函数,在事件处理函数中,this
指向绑定事件的元素。第二种是例如Array.prototype.map
支持第二个参数来指定回调函数中的this
指向。值得一提的是,通过bind
可以改变这两种情况中函数的this
指向。
4.new绑定
当使用new
来实例化一个构造函数时,this
指向实例。
优先级
绑定的优先级就比较简单了,这四种方式从上到下权重依次增加:new > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数
前面说了这么多,都是针对普通函数,也就是用function
关键字声明的函数。ES6中引入了箭头函数,都说箭头函数没有this
,但是又能在箭头函数中使用this
,这可能会给很多人造成困惑。其实,弄清楚箭头函数的this
很简单,只需要把箭头函数的this
看做一个普通的变量,由于箭头函数自身没有this
,所以需要沿着作用域链向上进行查找,直到找到this
(普通函数或者window)。也就是说,箭头函数的this
由两方面决定:词法作用域和父级函数的this
。因此,理解了普通函数的this
指向,箭头函数的this
也就很简单了。
那么箭头函数和call
一起使用会发生什么呢?由于箭头函数自身是没有this
的,而call
是改变目标函数的this
指向,因此对箭头函数使用call
是无效的,箭头函数依然会根据词法作用域来查找this
我在github上总结了一些关于this
指向的题目,大家可以看一看。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!