this到底指向谁?
相信很多小伙伴曾几何时都被这个问题问倒过吧? 特别是对于初学者而言,this的指向问题确实是一个令人头疼的问题。而对于要参加面试的同学来说,this的指向无疑是一个必考题。
之前我也是对this的指向感觉很迷惑,于是就看了好几篇有关this指向的优秀文章,最后也算是有了一个较为清晰的认识,现在就将其总结一下吧!
在开始之前,建议可以复习一下作用域的相关知识,可以看一下我之前的一篇文章:详解JavaScript作用域和作用域链🔗
OK,接下来就开始吧!
📍判断this的基本流程
首先,先给出我对于this指向判断的一个基本流程:
1、普通函数的this
- 通过
new
操作符绑定(创建一个实例对象,构造函数中的this指向这个实例对象) - 通过显式绑定(通过
call
、apply
、bind
这三个函数指定this的绑定对象) - 通过隐式绑定(指向最后调用它的那个对象,如
obj1.obj2.foo()
的调用方式,foo
内的this
指向obj2
) - 默认绑定(非严格模式下
this
指向全局对象, 严格模式下this
会绑定到undefined
)
2、箭头函数的this
- 指向定义该函数时的上一级作用域
接下来,我们就对上述每个步骤进行详细描述。
🖐普通函数的this
1、通过new绑定
当我们使用new
操作符来调用构造函数时,会自动执行下面的操作:
- 创建一个空对象,构造函数中的
this
指向这个空对象 - 这个空对象内部的
[[Prototype]]
特性被赋值为构造函数的prototype
属性。 - 执行构造函数方法,属性和方法被添加到
this
引用的对象中 - 如果构造函数中没有返回其它对象,那么返回
this
,即创建的这个的新对象,否则,返回构造函数中返回的对象。
关于new一个构造函数的过程,又是一个高频面试题,但不是本文的主要内容,所以不作过多描述。接下来,我们看一个例子:
function SayHi (name) {
this.name = name
}
var Hi = new SayHi('Rocky')
console.log('Hello,', Hi.name)
输出结果为 Hello, Rocky
因为在var Hi = new SayHi('Rocky')
这一步,会将SayHi
中的this
绑定到Hi
对象上。
此外,使用new
操作符创建的对象和字面量形式
创建出来的对象的结果基本没什么区别,看一下下面这个例子:
var name = 'window'
function Person (name) {
this.name = name
this.foo = function () {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var person2 = {
name: 'person2',
foo: function() {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
person1.foo()()
person2.foo()()
首先,person1.foo()
会打印this.name
,这里的this
肯定是指向person1
的,所以会打印person1
,然后它会返回一个函数并且在全局下调用了:person1.foo()()
,所以输出的是全局下的name
,所以为window
。person2
的情况跟person1
是一样的,因为foo()
是被person2
调用的,属于隐式绑定(下文会解释),所以this
是指向person2
的。
所以,输出结果为:
'person1'
'window'
'person2'
'window'
new
过程遇到return
一个对象,此时this
指向为返回的对象
function fn() {
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined
如果返回一个简单类型的时候,则this
指向实例对象
function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx
注意的是null
虽然也是对象,但是此时new
仍然指向实例对象
function fn() {
this.user = 'xxx';
return null;
}
var a = new fn;
console.log(a.user); //xxx
2、默认绑定
由于后面 显式绑定 需要用到 默认绑定 和 隐式绑定 的相关知识,所以这里把顺序颠倒一下。
默认绑定,是在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
对于默认绑定,只需记住:非严格模式下this
指向全局对象, 严格模式下this
会绑定到undefined
。
下面看一个例子:
function sayHi () {
console.log('Hello,', this.name)
}
var name = 'Rocky'
sayHi()
输出:Hello,Rocky
在调用sayHi()
时,应用了默认绑定,this
指向全局对象(非严格模式下),严格模式下,this
指向undefined
,undefined
上没有this
对象,会抛出错误。
因为这里的sayHi()
是在全局环境中调用的,所以也可以看成window.sayHi()
,两者是等价的,这样的话this的指向就很直观了,指向的是window
。
上面的代码,如果在浏览器环境中运行,那么结果就是 Hello,Rocky
但是如果在node
环境中运行,结果就是Hello,undefined
,这是因为node
中name
并不是挂在全局对象上的。
本文中,如不特殊说明,默认为浏览器环境执行结果。
再看一个例子:
var name = "windowsName"
function fn() {
var name = 'Cherry'
innerFunction()
function innerFunction() {
console.log(this.name) // windowsName
}
}
fn()
这里的innerFunction
作为一个没有挂载在任何对象上的函数调用,使用的是默认绑定,在非严格模式下 this 就是指向 window
的
注意,如果变量不是用var
声明的,而是用let
、const
声明的,那么它不会被绑定到window
对象上:
let a = 10
const b = 20
function foo () {
console.log(this.a)
console.log(this.b)
}
foo()
console.log(window.a)
输出结果为
undefined
undefined
undefined
3、隐式绑定
函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun()
。我们来看一段代码:
function sayHi(){
console.log('Hello,', this.name)
}
var person = {
name: 'Rocky',
sayHi: sayHi
}
var name = 'will'
person.sayHi()
打印的结果是 Hello,Rocky
sayHi
函数声明在外部,严格来说并不属于person
,但是在调用sayHi
时,调用位置会使用person
的上下文来引用函数,隐式绑定会把函数调用中的this
(即此例sayHi
函数中的this
)绑定到这个上下文对象(即此例中的person
)
需要注意的是:对象属性链中只有最后一层会影响到调用位置。来看下面的例子:
function sayHi(){
console.log('Hello,', this.name)
}
var person2 = {
name: 'Rocky',
sayHi: sayHi
}
var person1 = {
name: 'will',
friend: person2
}
person1.friend.sayHi()
结果是:Hello, Rocky
因为只有最后一层会确定this
指向的是什么,不管有多少层,在判断this
的时候,我们只关注最后一层,即此处的friend
。
隐式绑定有一种情况,就是发生隐式绑定丢失情况。隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。
有两种情况容易发生隐式丢失问题:
- 使用另一个变量来给函数取别名
- 将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定
接下来,我们来看一下具体的例子:
function sayHi(){
console.log('Hello,', this.name)
}
var person = {
name: 'Rocky',
sayHi: sayHi
}
var name = 'will'
var Hi = person.sayHi
Hi()
结果是: Hello,will
因为person
直接把sayHi
方法赋给了Hi
,但是没有调用。由于this指向最后调用它的那个对象,最后Hi
是在全局window
下调用的,所以sayHi
最终还是在window
下调用的。也就是说,sayHi
跟person
就没有任何半毛钱的关系。 this 指向的也就是window
。
针对此类问题,我们只需牢牢记住这个格式:XXX.fn()
,fn()
前如果什么都没有,那么肯定不是隐式绑定(或隐式绑定丢失)。上面的代码中,Hi()
前面没有任何对象去调用它,所以就不是隐式绑定(或隐式绑定丢失)
如果你把一个函数当成参数传递时,也会发生隐式绑定丢失:
function foo () {
console.log(this.a)
}
function doFoo (fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
输出为:window 2
这里将obj.foo
当成参数传递到doFoo
函数中,在传递的过程中,obj.foo()
函数内的this
发生了改变,指向了window
。所以输出的a是全局下的a。
这里有一个坑,就是很多人以为obj.foo()
函数内的this
指向window
,是因为它是在doFoo
里面调用的,doFoo
的this
就是window
,但其实不是这样的,我们再来看一题:
现在我们不用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)
结果是:
{ a:3, doFoo: f }
2
现在调用obj2.doFoo()
函数,里面的this
指向的应该是obj2
,因为是obj2
调用的它。
但是obj.foo()
打印出来的a
依然是2
,也就是window
下的a。
对于这个问题,只需记住以下这个结论即可:
如果把一个函数当成参数传递到另一个函数的时候,也会发生隐式丢失的问题,且与包裹着它的函数的this指向无关(比如上面的doFoo()
的this
)。在非严格模式下,会把该函数的this绑定到window上,严格模式下绑定到undefined。
下面再来看一个不同的例子:
function foo() {
setTimeout(function () {
console.log('id:', this.id)
}, 100);
}
var id = 21
foo() // 21
在上面的例子中,function () { console.log('id:', this.id) }
这个匿名函数被当成参数传递给setTimeout
,发生了隐式绑定丢失,所以this
应该指向全局对象window
,所以输出的是全局的变量id。我们也可以记住一个结论:匿名函数的this
永远指向window
接下来我们看一个综合一点的小例子:
function sayHi(){
console.log('Hello,', this.name)
}
var person1 = {
name: 'Rocky',
sayHi: function(){
setTimeout(function(){
console.log('Hello',this.name)
})
}
}
var person2 = {
name: 'will',
sayHi: sayHi
}
var name='skumion'
person1.sayHi()
setTimeout(person2.sayHi,100)
setTimeout(function(){
person2.sayHi()
},200)
结果为:
Hello, skumion
Hello, skumion
Hello, will
-
第一条输出很容易理解,刚刚才讲过,是隐式绑定丢失的情况,
this
执指向window
。 -
第二条输出也是隐式绑定丢失的情况,
setTimeout (fn,delay) { fn() }
相当于是将person2.sayHi
赋值给了一个变量fn
,最后执行了变量,这个时候,sayHi
中的this
显然和person2
就没有关系了。 -
第三条虽然也是在
setTimeout
的回调中,但是我们可以看出,这是执行的是person2.sayHi()
使用的是隐式绑定,因此这是this
指向的是person2
,跟当前的作用域没有任何关系。
4、显式绑定
显式绑定就是通过call
,apply
,bind
的方式,显式的指定this
所指向的对象。
call
,apply
和bind
的第一个参数,就是对应函数的this
所指向的对象。call
和apply
的作用一样,只是传参方式不同。call()
方法接受的是一个参数列表,而 apply()
方法接受的是一个包含多个参数的数组。
- call的语法:
function.call(thisArg, arg1, arg2, ...)
- apply的语法:
function.apply(thisArg , [ argsArray])
- bind的语法:
function.bind(thisArg, arg1, arg2, ...)
与call()
和apply()
不一样的是,bind()
方法创建一个新的函数,必须手动调用它才会被执行。
var name = 'Rocky', age = 23
var obj = {
name: 'will',
objAge: this.age,
myFun: function () {
console.log(this.name + '年龄' + this.age)
}
}
var anotherObj = {
name: 'skumion',
age: 18
}
输出结果如下
obj.myFun.call(anotherObj) // skumion年龄 18
obj.myFun.apply(anotherObj) // skumion年龄 18
obj.myFun.bind(anotherObj)() // skumion年龄 18
再来看一个例子
function sayHi(){
console.log('Hello,', this.name)
}
var person = {
name: 'Rocky',
sayHi: sayHi
}
var name = 'will'
var Hi = person.sayHi
Hi.call(person) // 等于Hi.apply(person)
输出的结果为: Hello, Rocky
因为使用显式绑定明确将this
绑定在了person
上。
如果通过call
,apply
或bind
将this
绑定到null
或undefined
身上时,会怎么样呢?
var foo = {
name: 'will'
}
var name = 'Rocky'
function bar() {
console.log(this.name)
}
bar.call(null); //Rocky
从上例中可以看出,将this
绑定到null
或undefined
身上时会被忽略,实际应用的是默认绑定规则。
此外,使用了显式绑定,也会出现隐式绑定所遇到的绑定丢失的情况。
function sayHi(){
console.log('Hello,', this.name)
}
var person = {
name: 'Rocky',
sayHi: sayHi
}
var name = 'will'
var Hi = function(fn) {
fn()
}
Hi.call(person, person.sayHi)
输出的结果是 Hello, will
原因很简单,Hi.call(person, person.sayHi)
的确是将this
指向person
了。但是在执行fn
的时候,相当于直接调用了sayHi
方法(记住: person.sayHi
已经被赋值给fn
了,隐式绑定也丢了),没有指定this
的值,对应的是默认绑定。
如果我们希望绑定不会丢失,要怎么做?很简单,调用fn
的时候,再给它显式绑定。
function sayHi(){
console.log('Hello,', this.name)
}
var person = {
name: 'Rocky',
sayHi: sayHi
}
var name = 'will'
var Hi = function(fn) {
fn.call(this)
}
Hi.call(person, person.sayHi)
此时,输出的结果为: Hello, Rocky
因为person
被绑定到Hi
函数中的this
上,fn
又使用了call(this)
将这个对象绑定给了sayHi
的函数。这时,sayHi
中的this
指向的就是person
对象。
OK,再来看一个例子:
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)
}.call(obj1), 0)
}
}
var a = 3
obj2.foo1()
obj2.foo2()
结果是:
2
{ a: 1 }
1
相信这题对于现在的大家来说根本没有难度,但是使用这个例子是为了引出一个细节问题:
既然call能改变this指向,那我这样写也行吧? obj2.foo2.call(obj1)
注意:这是错误的!如果是这种写法的话,改变的就是foo2
函数内的this
的指向了,但是我们知道,foo2
函数内this
的指向和setTimeout
里函数的this
是没有关系的,因为调用定时器的始终是window
。
接下来再来一个容易被忽视的细节问题:
function foo () {
console.log(this.a)
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
foo()
会正常打印出window
下的a
,也就是2
foo.call(obj)
由于显式绑定了this
,所以会打印出obj
下的a
,也就是1
foo().call(obj)
开始会执行foo()
函数,打印出2
,但是会对foo()
函数的返回值执行.call(obj)
操作,可是我们可以看到foo()
函数的返回值是undefined
,因此就会报错了。
输出结果为:
2
1
2
Uncaught TypeError: Cannot read property 'call' of undefined
所以我们可以看到foo.call()
和foo().call()
的区别了,一个是针对于函数,一个是针对于函数的返回值。
刚刚是因为函数没有返回值才报的错,那现在给它加上返回值看看:
function foo () {
console.log(this.a)
return function () {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo()
foo.call(obj)
foo().call(obj)
- 第一个数字
2
自然是foo()
输出的,虽然foo()
函数也返回了一个匿名函数,但是并没有调用它呀,只有写成foo()()
,这样才算是调用匿名函数。 - 第二个数字
1
是foo.call(obj)
输出的,由于.call()
是紧跟着foo
的,所以改变的是foo()
内this
的指向,并且.call()
是会使函数立即执行的,因此打印出1
,同理,它也没有调用返回的函数。 - 第三个数字
2
是foo().call(obj)
先执行foo()
时打印出来的,此时foo()
内this
还是指向window
。 - 在执行完
foo()
之后,会返回一个匿名函数,并且后面使用了.call(obj)
来改变这个匿名函数的this
指向并调用了它,所以输出了1
。
输出结果为:
2
1
2
1
5、优先级
到此,已经介绍完了this
的四种绑定规则,如果同时应用了多种规则,这四种绑定的优先级为:
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
🖖箭头函数的this
箭头函数有几个使用注意点:
- 箭头函数没有自己的
this
对象(详见下文),所以不能用call()、apply()、bind()这些方法去改变this的指向. - 不可以当作构造函数,也就是说,不可以对箭头函数使用
new
命令,否则会抛出一个错误。 - 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 - 不可以使用
yield
命令,因此箭头函数不能用作 Generator 函数。
上面四点中,最重要的是第一点。对于普通函数来说,内部的this
指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this
对象,内部的this
就是定义时上层作用域中的this
。也就是说,箭头函数内部的this
指向是固定的,相比之下,普通函数的this
指向是可变的。
1、this指向
function foo() {
setTimeout(() => {
// 定义时,this 绑定的是 foo 中的 this 对象
console.log('id:', this.id)
}, 100);
}
var id = 21
foo.call({ id: 42 }) // id: 42
上面代码中,setTimeout()
的参数是一个箭头函数,这个箭头函数的定义生效是在foo
函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this
应该指向全局对象window
,这时应该输出21
。但是,箭头函数导致this
总是指向函数定义生效时所在的对象(本例是{id: 42}
),所以打印出来的是42
。
下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的this
指向。
function Timer() {
this.s1 = 0
this.s2 = 0
// 箭头函数
setInterval(() => this.s1++, 1000)
// 普通函数
setInterval(function () {
this.s2++
}, 1000)
}
var timer = new Timer()
setTimeout(() => console.log('s1: ', timer.s1), 3100)
setTimeout(() => console.log('s2: ', timer.s2), 3100)
// s1: 3
// s2: 0
上面代码中,Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this
绑定定义时所在的作用域(即Timer
函数),后者的this
指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1
被更新了 3 次,而timer.s2
一次都没更新。
箭头函数实际上可以让this
指向固定化,绑定this
使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false)
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id)
}
}
上面代码的init()
方法中,使用了箭头函数,这导致这个箭头函数里面的this
,总是指向handler
对象。如果回调函数是普通函数,那么运行this.doSomething()
这一行会报错,因为此时this
指向window
对象(匿名函数的this指向window
)。
下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this
的指向。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id)
}, 100);
}
// ES5
function foo() {
var _this = this
setTimeout(function () {
console.log('id:', _this.id)
}, 100)
}
上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this
,而是引用外层的this
。
另外,由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
]
}).call({ x: 'outer' })
// ['outer']
上面代码中,箭头函数没有自己的this
,所以bind
方法无效,内部的this
指向外部的this
。
2、不适用场合
由于箭头函数使得this
从“动态”变成“静态”,下面四个场合不应该使用箭头函数。
第一个场合是定义对象的方法,且该方法内部包括this
。
const cat = {
lives: 9,
jumps: () => {
this.lives--
}
}
上面代码中,cat.jumps()
方法是一个箭头函数,这是错误的。调用cat.jumps()
时,如果是普通函数,该方法内部的this
指向cat
;如果写成上面那样的箭头函数,使得this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域。
globalThis.s = 21;
const obj = {
s: 42,
m: () => console.log(this.s)
};
obj.m() // 21
上面例子中,obj.m()
使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m
,这导致箭头函数内部的this
指向全局对象,所以obj.m()
输出的是全局空间的21
,而不是对象内部的42
。上面的代码实际上等同于下面的代码。
globalThis.s = 21;
globalThis.m = () => console.log(this.s);
const obj = {
s: 42,
m: globalThis.m
};
obj.m() // 21
由于上面这个原因,对象的属性建议使用传统的写法定义,不要用箭头函数定义。
第二个场合是需要动态this
的时候,也不应使用箭头函数。
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
上面代码运行时,点击按钮会报错,因为button
的监听函数是一个箭头函数,导致里面的this
就是全局对象。如果改成普通函数,this
就会动态指向被点击的按钮对象。
第三个场合是定义原型方法时,也不应该使用箭头函数
function Foo (value) {
this.value = value
}
Foo.prototype.getValue = () => console.log(this.value)
const foo1 = new Foo(1)
foo1.getValue() // undefined
第四个场合是构造函数也不应该使用箭头函数
const Foo = (value) => {
this.value = value;
}
const foo1 = new Foo(1)
// 事实上直接就报错了 Uncaught TypeError: Foo is not a constructor
console.log(foo1)
3、Vue中的this
所有被Vue管理的函数,最好写成普通函数,这样this的指向才是vm
或 组件实例对象
。如:
computed: {
foo () {
console(this) // vm
}
},
methods: {
bar () {
console(this) // vm
}
}
所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数等),最好写成箭头函数,这样this的指向才是vm
或 组件实例对象
。 如:
methods: {
foo () {
setTimeOut (() => {
console.log(this) // vm
}, 1000)
},
bar () {
return new Promise ((resolve, reject) => {
resolve()
console.log(this) // vm
})
}
}
📒练习题
第一题
"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
第二题
var name = "windowsName"
var a = {
name : "Rocky",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100)
}
};
a.func2()
匿名函数function () { this.func1() }
被当成参数传递给setTimeout
,发生了隐式绑定丢失,所以this指向全局window对象,但是在 window
中并没有 func1
函数。
所以输出结果会报错:this.func1 is not a function
第三题
function foo () {
console.log(this.a)
return function () {
console.log(this.a)
}
}
var obj = { a: 1 }
var a = 2
foo.call(obj)()
首先foo.call(obj)
打印obj
里的1
,然后foo
又返回了一个匿名函数。foo()
函数内的this
虽然指定了是为obj
,但是调用最后调用匿名函数的却是window
。因为foo.call(obj)()
是在全局下调用的。
输出结果为:
1
2
第四题
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()
obj.foo()
中,foo
是被obj
调用的,所以foo
里面的this指向obj
,输出1
var foo2 = obj.foo
是隐式绑定丢失的情况,所以 foo2()
是直接在window
下调用了foo()
,所以输出2
var obj2 = { a: 3, foo2: obj.foo }
同样也是隐式绑定丢失情况,obj.foo
直接赋给了foo2
,然后foo2
被obj2
调用,所以输出3
所以输出结果为:
1
2
3
第五题
请问下面的代码之中,this
的指向有几个?
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id)
}
}
}
}
var f = foo.call({id: 1})
var t1 = f.call({id: 2})()()
var t2 = f().call({id: 3})()
var t3 = f()().call({id: 4})
答案是this
的指向只有一个,就是函数foo
的this
,最终输出三个id: 1
,这是因为所有的内层函数都是箭头函数,都没有自己的this
,它们的this
其实都是最外层foo
函数的this
。所以不管怎么嵌套,t1
、t2
、t3
都输出同样的结果。如果这个例子的所有内层函数都写成普通函数,那么每个函数的this
都指向运行时所在的不同对象。
输出结果为:
id: 1
id: 1
id: 1
第六题
var name = 'window'
function Person (name) {
this.name = name
this.foo = function () {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo.call(person2)()
person1.foo().call(person2)
这是一道new绑定结合显式绑定的题目,具体分析如下
person1.foo.call(person2)()
将foo()
函数内的this
指向了person2
,所以打印出person2
,而内部返回的匿名函数是由window
调用的,所以打印出window
。person1.foo().call(person2)
是将匿名函数的this
显式绑定到了person2
上,所以打印出来的会是person2
。
输出结果如下:
'person2'
'window'
'person1'
'person2'
第七题
var obj = {
a: 1,
foo: function (b) {
b = b || this.a
return function (c) {
console.log(this.a + b + c)
}
}
}
var a = 2
var obj2 = { a: 3 }
obj.foo(a).call(obj2, 1)
obj.foo.call(obj2)(1)
- 开始调用
obj.foo(a)
将2
传入foo
函数并赋值给型参b
,并且由于闭包的原因,使得匿名函数内能访问到b
,之后调用匿名函数的时候,用call()
改变了this
的指向,使得匿名函数内this.a
为3
,并传入最后一个参数1
,所以第一行输出的应该是3 + 2 + 1
,也就是6
。 - 而第二行,
obj.foo.call(obj2)
这里是将foo
函数内的this
指向了obj2
,同时并没有传递任何参数,所以b
开始是undefined
的,但是又因为有一句b = b || this.a
,使得b
变为了3
;同时最后一段代码(1)
,是在调用匿名函数,且和这个匿名函数内的this
应该是指向window
的,因此输出为2 + 3 + 1
,为6
。
输出结果为:
6
6
第八题
const obj = {
aaa() {
setTimeout(function() { // 1.普通函数的setTimeout
setTimeout(function() {
console.log(this)
})
setTimeout(() => {
console.log(this)
})
})
setTimeout(() => { // 2.箭头函数的setTimeout
setTimeout(function(){
console.log(this)
})
setTimeout(() => {
console.log(this)
})
})
}
}
obj.aaa()
第一个普通函数的setTimeout
:this指向window
- 普通函数的
子setTimeout
:默认绑定,this指向全局对象window
- 箭头函数的
子setTimeout
:指向上层作用域的this,上层作用域的this就是window
第二个箭头函数的setTimeout
:往上层作用域aaa
找this,因为aaa
是被obj
调用的,aaa
方法的this就是指向obj
,所以this指向obj
- 普通函数的
子setTimeout
:默认绑定,this指向全局对象window
- 箭头函数的
子setTimeout
:指向上层作用域的this,上层作用域的this就是obj
所以最终依次输出:
window
window
window
obj
第九题
var name = 'window'
var obj1 = {
name: 'obj1',
foo1: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
},
foo2: () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2'
}
obj1.foo1.call(obj2)()
obj1.foo1().call(obj2)
obj1.foo2.call(obj2)()
obj1.foo2().call(obj2)
箭头函数的this
无法通过bind、call、apply
来直接修改,但是可以通过改变作用域中this
的指向来间接修改。接下来具体分析一下:
obj1.foo1.call(obj2)()
第一层为普通函数,并且通过.call
改变了this
指向为obj2
,所以会打印出obj2
,第二层为箭头函数,它的this
和外层作用域中的this
相同,因此也是obj2
。obj1.foo().call(obj2)
第一层打印出obj1
,第二层为箭头函数,使用了.call
想要修改this
的指向,但是并不能成功,因此.call(obj2)
对箭头函数无效,还是打印出obj1
。obj1.foo2.call(obj2)()
第一层为箭头函数,并且想要通过.call(obj2)
改变this
指向,但是无效,且它的外层作用域是window
,所以会打印出window
,第二层为普通函数,this
是最后调用者window
,所以也会打印出window
。obj1.foo2().call(obj2)
第一层为箭头函数,外层作用域是window
,打印出window
,第二层为普通函数,且使用了.call(obj2)
来改变this
指向,所以打印出了obj2
。
输出结果如下:
'obj2' 'obj2'
'obj1' 'obj1'
'window' 'window'
'window' 'obj2'
第十题
var obj = {
hi: function(){
console.log(this)
return ()=>{
console.log(this)
}
},
sayHi: function(){
return function() {
console.log(this)
return ()=>{
console.log(this)
}
}
},
say: ()=>{
console.log(this)
}
}
let hi = obj.hi() // 1
hi() // 2
let sayHi = obj.sayHi()
let fun1 = sayHi() // 3
fun1() // 4
obj.say() // 5
obj.hi()
对应了this的隐式绑定规则,this绑定在obj
上,所以输出obj
。hi()
这一步执行的就是箭头函数,箭头函数会往上一层作用域找this,刚刚我们得出上一层的this是obj
,所以输出obj
sayHi()
属于隐式绑定丢失的情况,这个时候this执行的是默认绑定,this指向的是全局对象window
fun1()
这一步执行的是箭头函数,箭头函数会往上一层作用域找this,this指向的是window
,因此输出结果是window
obj.say()
执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window
最终从1-5依次输出为:
obj
obj
window
window
window
OK,以上就是本篇文章的全部内容啦!
如果你看到了这里,首先感谢你能够花时间阅读我的文章,如果文章有哪里不当的地方,欢迎批评指正~如果你觉得文章对你有一点帮助的话,也不要吝啬你的点赞哦!
参考
阮一峰老师的ES6教程
嗨,你真的懂this吗?
this、call、apply、bind
【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)
web前端面试-面试官系列
推荐阅读
图解script的defer / async与DOMContentLoaded / Load
详解JavaScript作用域和作用域链
彻底搞懂作用域、执行上下文、词法环境
两个项目实例+常用语法解析带你掌握Mock.js✨
靠做题📝来掌握Promise/async/await
用大白话🙌带你掌握闭包
图文并茂🌈聊聊原型与原型链