让我们来看看,通过阅读本篇文章你可以学习到:
- this的默认绑定
- 隐式绑定
- 隐式绑定的隐式丢失问题
- 显式绑定
- 显式绑定的其它用法
- new绑定
- 箭头函数绑定
- 综合题
- 几道手写题
前期准备
在正式阅读之前,你需要知道this的5种绑定方式:
- 默认绑定(非严格模式下
this
指向全局对象, 严格模式下this
会绑定到undefined
) - 隐式绑定(当函数引用有上下文对象时, 如
obj.foo()
的调用方式,foo
内的this指向obj)
- 显示绑定(通过
call()
或者apply()
方法直接指定this
的绑定对象, 如foo.call(obj))
new
绑定- 箭头函数绑定(
this
的指向由外层作用域决定的)
1. 默认绑定
先介绍一种最简单的绑定方式吧:默认绑定。
也就是我们常说的:在非严格模式下
this指向的是全局对象window,而在严格模式下
会绑定到undefined。
1.1 题目一
老规矩,来看个最基本的案例:
代码解读
var a = 10;
function foo () {
console.log(this.a)
}
foo();
我们知道在使用var
创建变量的时候(不在函数里),会把创建的变量绑定到window
上,所以此时a
是window
下的属性。
而函数foo
也是window
下的属性。
因此上面的代码其实就相当于是这样:
window.a = 10;
function foo() {
console.log(this.a)
}
window.foo();
在这里,调用foo()
函数的是window
对象,且又是在非严格模式下,所以foo()
中this
的指向是window
对象,因此this.a
会输出10
。
答案:
10
1.2 题目二
改造下题目一,看看在严格模式下。
(想要开启严格模式,只要在需要开启的地方写上"use strict"
)
"use strict";
var a = 10;
function foo () {
console.log('this1', this)
console.log(window.a)
console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo();
需要注意的点:
开启了严格模式
,只是说使得函数内的this
指向undefined
,它并不会改变全局中this
的指向。因此this1
中打印的是undefined
,而this2
还是window
对象。
另外,它也不会阻止a
被绑定到window
对象上。
所以最后的执行结果:
f foo() {...}
'this2' Window{...}
'this1' undefined
10
Uncaught TypeError: Cannot read property 'a' of undefined
1.3 题目三
let a = 10
const b = 20
function foo () {
console.log(this.a)
console.log(this.b)
}
foo();
console.log(window.a)
如果把var改成了let 或者 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined
。
答案:
undefined
undefined
undefined
1.4 题目四
var a = 1
function foo () {
var a = 2
console.log(this)
console.log(this.a)
}
foo()
这里我们很容易就知道,foo()
函数内的this
指向的是window
,因为是window
调用的foo
。
但是打印出的this.a
呢?注意,是this.a
,不是a
,因此是window
下的a
。
并且由于函数作用域的原因我们知道window
下的a
还是1
。
因此答案为:
Window{...}
1
1.5 题目五
把题目1.4改造一下 😁。
var a = 1
function foo () {
var a = 2
function inner () {
console.log(this.a)
}
inner()
}
foo()
其实这里和1.4很像,不过一看到函数内的函数,就很容易让人联想到闭包 😂,然后… 然后就脱口而出,答案是2啊,这还不简单。
小伙伴们,审题可得仔细啊,这里问你的是this.a,而在inner中,this指向的还是window。
答案:
1
(我知道有的小伙伴不满这种题目,这吖的不就是和人玩文字游戏吗?有什么技术含量,但是现实就是这样,很多面试官会喜欢问这种细节题来考察你细心不细心。在没能力改变这种情况的前提下,你只能试着接受它…)
2. 隐式绑定
OK👌,介绍完了默认绑定之后,让我们来看看第二种隐式绑定。
其实有一个简单的规则,this 永远指向最后调用它的那个对象。
谁最后调用的函数,函数内的this指向的就是谁(不考虑箭头函数)。
(了解上下文对象的小伙伴应该都知道,这与上下文对象以及作用域有关,
下面你可以用这个规则来做题啦 😁。
2.1 题目一
function foo () {
console.log(this.a)
}
var obj = {
a: 1, foo }
var a = 2
obj.foo()
(var obj = { foo }
就相当于是var obj = { foo: foo }
,这个大家应该都知道吧)
在这道题中,函数foo()
虽然是定义在window
下,但是我在obj
对象中引用了它,并将它重新赋值到obj.foo
上。
且调用它的是obj
对象,因此打印出来的this.a
应该是obj
中的a
。
换个角度想,上面👆这段代码是不是就相当于是这样:
var obj = {
a: 1,
foo: function () {
console.log(this.a)
}
}
var a = 2
obj.foo()
在这里foo
函数内的this
指向的就是obj
,和题目效果一样。
答案都是:
1
有小伙伴就会有有疑问了,obj.foo()
不就相当于是window.obj.foo()
吗?
那foo()
内的this
到底是算window
呢,还是obj
呢?
请注意我前面说的,是最后调用函数的对象,显然,obj
要比window
更后面一点。
3. 隐式绑定的隐式丢失问题
隐式绑定的基本概念大家应该都清楚了,不过其实有一个关于隐式绑定的常用考点,那就是隐式丢失问题。
隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。
有两种情况容易发生隐式丢失问题:
- 使用另一个变量来给函数取别名
- 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定
我们一种一种来看哈。
3.1 题目一
使用另一个变量来给函数取别名会发生隐式丢失。
function foo () {
console.log(this.a)
};
var obj = {
a: 1, foo };
var a = 2;
var foo2 = obj.foo;
obj.foo();
foo2();
执行这段代码会打印出啥呢 🤔️?
在这里我们已经知道了,obj.foo()
中this
的指向是为obj
的(可以看第二部分隐式绑定),所以obj.foo()
执行的时候,打印出来的是obj
对象中的a
,也就是1
。
但是foo2
它不也是obj.foo
吗?我只不过是用了一个变量foo2
来盛放了它而已。所以你是不是认为它打印的也是1
呢?
额 😅,其实这里不是的,它打印出的是window
下的a
。
答案:
1
2
这是因为虽然foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window。
其实也就相当于是window.foo2(),如果你不相信的话,可以看下面一题👇。
3.2 题目二
让我们在一个新的变量obj2
中也定义一个foo2
看看:
function foo () {
console.log(this.a)
};
var obj = {
a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = {
a: 3, foo2: obj.foo }
obj.foo();
foo2();
obj2.foo2();
这三种不同的foo()
打印出来的分别是什么呢?
答案:
1
2
3
obj.foo()
中的this
指向调用者obj
foo2()
发生了隐式丢失,调用者是window
,使得foo()
中的this
指向window
foo2()
发生了隐式丢失
,调用者是obj2
,使得foo()中的this指向obj2
3.3 题目三
再就是如果你把一个函数当成参数传递时,也会被隐式赋值,发生意想不到的问题。
来看看这道题目:
function foo () {
console.log(this.a)
}
function doFoo (fn) {
console.log(this)
fn()
}
var obj = {
a: 1, foo }
var a = 2
doFoo(obj.foo)
这里我们将obj.foo
当成参数传递到doFoo
函数中,在传递的过程中,obj.foo()函数内的this发生了改变,指向了window。
因此结果为:
Window{...}
2
注意,我这里说的是obj.foo()
函数,而不是说doFoo()
。doFoo()函数内的this本来就是指向window的,因为这里是window
调用的它。
但是你不要以为是doFoo()
函数内的this影响了obj.foo()
,不信你看下一题。
3.4 题目四
现在我们不用window调用doFoo,而是放在对象obj2里,用obj2调用:
function foo () {
console.log(this.a)
}
function doFoo (fn) {
console.log(this)
fn()
}
var obj = {
a: 1, foo }
var a = 2
var obj2 = {
a: 3, doFoo }
obj2.doFoo(obj.foo)
现在调用obj2.doFoo()
函数,里面的this指向的应该是obj2,因为是obj2调用的它。
但是obj.foo()
打印出来的a
依然是2
,也就是window
下的。
执行结果为:
{ a:3, doFoo: f }
2
所以说,
- 使用另一个变量来给函数取别名,会发生隐式丢失的问题,this指向window
- 如果你把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的this指向无关。
- 在非严格模式下,会把该函数的this绑定到window上,严格模式下绑定到undefined。
一样的代码,试试严格模式下:
"use strict"
function foo () {
console.log(this.a)
}
function doFoo (fn) {
console.log(this)
fn()
}
var obj = {
a: 1, foo }
var a = 2
var obj2 = {
a: 3, doFoo }
obj2.doFoo(obj.foo)
执行结果:
{ a:3, doFoo: f }
Uncaught TypeError: Cannot read property 'a' of undefined
4. 显式绑定
功能如其名,就是强行使用某些方法,改变函数内this的指向。
通过call()、apply()或者bind()
方法直接指定this的绑定对象, 如foo.call(obj)。
这里有几个知识点需要注意:
- 使用
.call()
或者.apply()
的函数是会直接执行的 bind()
是创建一个新的函数,需要手动调用才会执行.call()
和.apply()
用法基本类似,不过call接收若干个参数,而apply接收的是一个数组
(其实这应该是经常听到的知识点了吧 😅)
来看看题目一。
4.1 题目一
function foo () {
console.log(this.a)
}
var obj = {
a: 1 }
var a = 2
foo()
foo.call(obj)
foo.apply(obj)
foo.bind(obj)
第一个foo() 都很好理解,这不就是默认绑定吗?😁
而第二个和第三个foo都使用了call或apply来改变this的指向,并且是立即执行的。
第四个foo,仅仅是使用bind创建了一个新的函数,且这个新函数也没用别的变量接收并调用,因此并不会执行
。
答案:
2
1
1
这里想要提一嘴,如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数
。
例如🌰:
function foo () {
console.log(this.a)
}
var a = 2
foo.call()
foo.call(null)
foo.call(undefined)
输出的是:
2
2
2
4.2 题目二
了解了显式绑定的基本使用之后,让我们来看看它的妙用。
首先,是这个例子🌰:
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
foo1: function () {
console.log(this.a)
},
foo2: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}, 0)
}
}
var a = 3
obj2.foo1()
obj2.foo2()
对于obj2.foo1()
,我们很清楚,它就是打印出2
。
但是对于obj2.foo2
呢?在这个函数里,设置了一个定时器,并要求我们打印出this
和this.a
。
想想我前面说过的话,谁调用的函数,函数内的this指向的就是谁。
而对于setTimeout
中的函数,这里存在隐式绑定的隐式丢失,也就是当我们将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定,因此这时候setTimeout中的函数内的this是指向window的
。
所以最终的结果是:
2
Window{...}
3
4.3 题目三
面对上面👆这种情况我们就可以使用call、apply 或者bind来改变函数中this的指向,使它绑定到obj1上,从而打印出1。
ar obj1 = {
a: 1
}
var obj2 = {
a: 2,
foo1: function () {
console.log(this.a)
},
foo2: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}.call(obj1), 0)
}
}
var a = 3
obj2.foo1()
obj2.foo2()
现在的执行结果就是:
2
{ a: 1 }
1
但是看看我这里的写法,我是将.call
运用到setTimeout
里的回调函数上,并不是运用到obj2.foo2()
上。
所以有小伙伴就会问了,我下面的这种写法不可以吗?
obj2.foo2.call(obj1)
注意⚠️:如果是这种写法的话,我改变的就是foo2函数内的this的指向了,但是我们知道,foo2函数内this的指向和setTimeout里函数的this是没有关系的,因为调用定时器的始终是window。
并且这里使用.bind()也是可以的,因为定时器里的函数在时间到了之后本就是会自动执行的。
4.4 题目四
OK👌,我们不用定时器,把它干掉,换成一个函数:
var obj1 = {
a