面试常考题,很多初学者会被JavaScript中众多不同情况的this
指向搞迷糊。本质上,this
始终指向一个对象。
0 太长不看版
只要弄明白,this
在函数中被使用,在定义时不确定指向,调用时才能确定指向(箭头函数除外),并始终指向一个对象,可以简单分为以下6种情况:
- 普通函数调用,指向全局对象
- 对象方法调用,谁调用指向谁,只看调用不看引用
- 构造函数调用,指向新构造的实例,尽管可能不被构造函数返回
- 遇
apply
、call
、bind
,改变指向 - 匿名函数调用,一般指向全局对象
- 箭头函数调用,指向定义箭头函数作用域的
this
1 普通函数调用
普通函数调用时,this
指向全局对象,在浏览器中,全局对象为window
。看示例:
1.1 var
定义变量
1 | var name = "var"; |
fn
作为普通函数被调用,this
指向全局对象window
。
var
定义的全局变量视为全局对象的属性,因此this.name
输出全局对象window
的name
属性,即"var"
。
1.2 let
定义变量
1 | let name = "let"; |
let
定义的变量不会成为全局对象的属性,因此this.name
即全局对象window
的name
属性不存在,输出undefined
。
1.3 全局对象属性
1 | window.name = "win"; |
毫无疑问,this.name
输出全局对象window
的name
属性,即"win"
。
2 对象方法调用
函数作为对象方法被调用时,哪个对象调用,this
就指向谁。
如果函数变量被赋值给其他变量,依然是谁调用指向谁,而不考虑函数来源(只看调用,不看引用)。
2.1 对象直接调用自有方法
1 | var obj = { |
obj
对象直接调用自有方法fn
,fn
中的this
就指向调用它的obj
对象。
2.2 对象多层调用自有方法
1 | var obj = { |
尽管方法fn
是被最外层对象obj
间接调用,但fn
中的this
指向最近的上一层对象inner
(即直接调用它的对象)。
2.3 函数变量被赋值给其他对象的属性
1 | var obj = { |
尽管方法fn2
来源于对象obj
的方法fn
,但依然是“谁调用指向谁”,方法被obj2
调用,this
就指向obj2
。
2.4 原型方法
1 | var obj = { |
这个就很好理解了~虽然getAge
是原型对象Object.prototype
的方法,但实例obj
和obj2
进行了调用,方法中的this
就指向它们。
实际上,这就是原型方法的特点之一:全体实例的可复用方法。
2.5 函数变量被赋值给普通变量
1 | var name = "var"; |
这时,fn1
和fn2
实际上是作为普通函数调用,根据“谁调用指向谁”的原则,它们当中的this
指向全局对象window
,输出全局对象的属性值"var"
和123
。
总结:无论嵌套几层,无论如何赋值,无论函数来源,谁调用了函数/方法,this
就指向谁。
3 构造函数调用
一般情况下,this
指向构造函数new
出来的对象。
3.1 一般情况
1 | function A() { |
这也是最常见的过程,类似于定义对象的自有属性。
面试常考题:通过
new
创建构造函数的一个实例时,发生了什么?以
var a = new A()
为例:
- 创建一个空对象:
var obj = {}
- 让构造器函数
A
的this
指向对象obj
,并执行A
中的函数体:var result = A.call(obj)
- 设置原型链,让对象
obj
的__proto__
属性指向构造器函数A
的原型对象:obj.__proto__ = A.prototype
- 判断构造器函数
A
的返回类型,如果是值类型,结果返回obj
,如果是引用类型(null
除外),返回引用类型的对象:a = result && (typeof result === "object") ? result : obj
如果要手写一个
new
函数,可能是这样的:
1
2
3
4
5
6 Function.prototype.new = function () {
var obj = {};
var result = this.call(obj);
obj.__proto__ = this.prototype;
return result && (typeof result === "object") ? result : obj;
}
3.2 构造函数当作普通函数使用
毫无疑问,this
指向全局对象window
。这种做法在程序中显然是不推荐的,但做题时要知道this
指向谁。
1 | var name = "var"; |
3.3 构造函数返回值的情况
一般情况下,构造函数是不需要显式返回值的,通过new
执行构造函数的返回值是新生成的实例。
但是,如果构造函数返回一个对象(包括空对象),new
执行构造函数的返回值不再是新生成的实例,而是该对象。
构造函数执行过程中,this
依然试图指向构造函数的实例,虽然这个实例无法返回。
1 | function A() { |
如果构造函数返回的是基本类型、undefined
、null
时,new
执行构造函数的返回值依然是新生成的实例。
1 | function A() { |
4 apply
、call
、bind
调用
三者均可改变this
的指向给第一个参数。区别在于:
apply
的第二个参数为可迭代对象call
的参数不固定bind
除返回一个函数外,与call
并无不同,它需要调用才可以执行,其他二者直接执行函数
1 | var obj = { |
注意以上示例12、13行的区别,第13行[5, 6]
被当做一个参数传给形参x
,形参y
得到undefined
。
5 匿名函数调用
匿名函数不属于任何对象,它的this
一般指向全局对象。
5.1 一般情况
1 | var obj = { |
无论谁调用、无论如何调用,这里的匿名函数中的this
指向全局对象window
。
5.2 定时器回调
定时器回调函数中,this
指向全局对象。因此如果需要改变定时器回调函数中this
的指向,需要使用变量提前存储this
的指向。
1 | var age = 123; |
上述代码中,this
指向全局对象window
,因此输出全局变量age
的值123
。
1 | var age = 123; |
上述代码中,使用that
提前存储了fn
中this
的指向,而根据第2节的内容,调用obj.fn()
时fn
中的this
指向obj
,因此定时器回调函数中能输出obj
的age
属性值15
。
5.3 事件绑定
注意,事件绑定时,this
指向事件源。
1 | const btn = document.getElementById("btn"); |
1 | const btn = document.getElementById("btn"); |
如果要清除绑定事件,第一种方法可以通过btn.onclick = null
实现,第二种方法可以调用removeEventListener
方法。
但是!如果下面这么做,移除是失败的。
1 | const btn = document.getElementById("btn"); |
创建的两个匿名函数是独立的,彼此没有关系,自然无法被移除。因此,使用addEventListener
绑定事件时,建议传入具名的函数变量,而避免使用匿名函数。
1 | const btn = document.getElementById("btn"); |
6 箭头函数
箭头函数非常特殊,它没有自己的this
,而是会寻找定义箭头函数的作用域中this
的指向。
箭头函数中的this
在其定义时就已确定,与谁调用无关!
1 | setTimeout(() => console.log(this), 1000); // window对象 |
这个没什么问题。
1 | var obj = { |
this
会寻找定义箭头函数的作用域(即return
的匿名函数)中this
的指向,因此指向window
。
1 | var obj = { |
this
会寻找定义箭头函数的作用域(即return
的箭头函数)中this
的指向,由于外层仍是箭头函数,继续向上寻找,直到找到fn
作用域中的this
且指向为obj
。
1 | let obj = { |
- 第8行,
fn
中的两个this
都指向全局对象window
,输出两次window
对象。 - 第9行,
fn
中的this
被call
改变指向,此时this
指向obj
,调用一次fn
函数,输出obj
即{ name: 'me' }
- 第10行,
fn2
是一个箭头函数,在第9行中,它的this
被改变指向为obj
,因此此处输出obj
即{ name: 'me' }
为什么第10行不是输出window
对象?执行第10行时,定义箭头函数的fn
作用域中的this
应该指向window
对象啊。
箭头函数只看它定义时,当前作用域的this
指向什么。第6行,箭头函数是在fn
函数体内被定义的,因此第9行执行后,在定义箭头函数时,先是把内部的this
更改为obj
,再返回给fn2
。这样fn2
调用时(无论如何调用),都输出obj
对象。
7 面试题解析
7.1 字节跳动面试题
解释以下代码的输出:
1 | var name = "bytedance"; |
输出为:
1 | undefined |
- 第13行,
new
一个A
的实例,程序开始执行A()
- 第3行,由于第5行的变量声明
var age
被提升至函数顶端,因此此处输出undefined
- 第6行,判断
this
指向的是a
实例,此处输出定义的this.name
即123 - 第7行,由于
this
指向的a
实例没有name
属性,因此此处输出undefined
- 第14行,将
a.getA
作为一个函数对象赋值给变量funA
并在第15行直接调用,程序开始执行getA()
- 第10行,判断
this
指向的是全局对象window
,因此此处输出全局对象的name
属性,而var
定义的全局变量为全局对象的属性,即输出"bytedance"
- 第11行,同上,此处输出全局对象
window
扩展:函数对象自带name
属性,且值为本身的名称,如A.name
返回"A"
。