在之前的文章中,我们已经了解了JavaScript中的call
、apply
、bind
这些方法(JavaScript | apply、bind与call),它们都与this
有着密切的关系,而今天,我们将继续探讨this
指向的问题。
this指的是什么?
在JavaScript
中,this
指向的是当前执行代码的环境对象。
在MDN上给出的this
描述是这样的:
this
的值取决于它出现的this:函数、类或全局。------this - JavaScript | MDN
那么针对这些不同的场景,相应的会出现不同的this
指向,而对于这些不同的this
指向会有些对应的规律,那么接下来我们将针对不同的场景下的this
指向来好好的理解并解决一些常见的面试题。
⚡注1:本文是主要讨论的是对于浏览器环境下this
的指向的变化
❕注2:如果文章有误请在评论区指出,我会核实并修改
在不同情况下this的指向
this
的指向是当我们调用函数的时候确定的- 调用方式的不同决定了
this
的指向不同
一、通过对象打点方式调用的函数
- 对象打点调用它的方法函数,则函数的
this
为这个打点的对象
javascript
let obj = {
a: 1,
b: 2,
fn: function() {
return this
}
}
console.log(obj.fn() === obj) // true
练习1:
下面这题的输出是?
javascript
let getObj = function(){
var a = 1;
var b = 2;
return {
a: 22,
b: 33,
fn: function() {
return this.a + this.b
}
}
}
console.log(getObj().fn())
答案:55
解析:
- 调用了
getObj()
这个函数,这个函数返回了一个对象 - 通过
1
中返回的对象调用该对象的fn
属性,该fn
属性指向一个匿名函数 - 此时是通过对象调用方法,满足了规则:对象打点调用它的方法函数,则函数的
this
为这个打点的对象,所以此时fn
属性中的函数的this
指向为调用者,也就是对象,所以本题的答案为22 + 33 = 55
练习2:
下面这题的输出是?
javascript
var boss1 = {
name: 'boss1',
returnThis () {
return this
}
}
var boss2 = {
name: 'boss2',
returnThis () {
return boss1.returnThis()
}
}
console.log(boss1.returnThis())
console.log(boss2.returnThis())
答案:
两次的输出都为boss1
解析:
- 第一次输出:调用
boss1.returnThis()
,因为boss1
为对象,直接调用自身的returnThis
方法返回boss1
自身,所以输出的this
为boss1
- 第二次输出:调用
boss2.returnThis()
,在boss2
对象的returnThis
函数中返回了boss1
的returnThis()
的调用,所以这里的返回结果变成了boss1
调用returnThis()
这个函数,根据规则:对象打点调用它的方法函数,则函数的this
为这个打点的对象,所以this
指向为boss1
二、通过圆括号直接调用的函数
- 通过圆括号直接调用的函数,函数的
this
会指向window
对象
javascript
function showThis () {
console.log(this)
}
showThis() // window
- 实际上上面的代码我们可以认为是
window.showThis()
的简写
练习1:
下面这段代码的输出是?
javascript
var boss1 = {
name: 'boss1',
returnThis () {
return this
}
}
var boss3 = {
name: 'boss3',
returnThis () {
var returnThis = boss1.returnThis
return returnThis()
}
}
console.log(boss3.returnThis())
答案:window
解析:
- 首先在调用
boss3.returnThis()
这个方法的时候,函数体内会定义一个变量即returnThis
变量 returnThis
变量的值为boss1
这个对象的returnThis
属性,而boss1
的returnThis
这个属性指向一个函数,等于说这里把这个函数赋值给了定义的returnThis
变量- 然后返回这个函数的调用,而此时满足了规则2:通过圆括号直接调用的函数,函数的this会指向window对象,所以此时
this
指向window
对象
练习2:
下面这段代码的输出是?
javascript
function fun() {
return this.a + this.b
}
var a = 1;
var b = 2;
var obj = {
a: 3,
b: fun(),
fun: fun
}
var result = obj.fun()
console.log(result)
答案:
6
解析:
- 首先这里的通过
obj
对象调用其fun
属性,满足了规则1:对象打点调用它的方法函数,则函数的this是这个打点的对象,所以此时的结果为this.a + this.b
就相当于obj.a + obj.b
- 而这里
obj
对象的b
属性为一个函数的调用,满足了规则2:通过圆括号直接调用的函数,函数的this会指向window
对象,所以此时的结果相当于obj.a + window.a + window.b => 3 + 1 + 2
结果为6
三、数组(类数组对象)枚举出的函数
- 数组(类数组对象)枚举出函数进行调用,this是这个数组(类数组对象)
- 什么是类数组对象:所有键名为自然数序列(从0开始),且有
length
属性的对象 - arguments对象是最常见的类数组对象,它是函数的实参列表
javascript
// 1. 数组
var arr = ['a', 'b', 'c', function(){
console.log(this[0])
}]
arr[3]()
// 2. 类数组对象
function fun() {
arguments[3]()
}
fun('a', 'b', 'c', function(){
console.log(this[1])
})
练习:
下面代码执行后会打印什么?
javascript
function func() {
console.log(this)
}
debugger
var arr = [{
func
}, 'b', 'c', function(){
this[0].func()
}]
arr[3]()
答案:
arr[0]
的对象{func}
解析:
- 首先这里一开始就调用了
arr[3]()
,满足规则3:数组(类数组对象)枚举出函数进行调用,this是这个数组(类数组对象),所以此时函数的内部this
指向arr
数组 this[0]
相当于获取arr[0]
也就是{func}
这个对象,调用这个对象的func
方法,满足规则1:对象打点调用它的方法函数,则函数的this是这个打点的对象,所以此时this
就指向于{func}
这个对象
四、IIFE
[IIFE](https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE)
也就是立即执行函数中this为window
对象
javascript
(function(){
console.log(this)
})();
练习:
下面这段代码打印的结果是?
javascript
var a = 1;
var obj = {
a: 2,
fun: (function() {
var a = this.a;
return function() {
console.log(a + this.a)
}
})()
}
obj.fun()
答案:
3
解析:
- IIFE在定义的时候会立刻执行,所以
obj
对象的实际值为IIFE中返回的函数 - 因为规则4:IIFE中this为window对象,所以
var a = this.a
,这里的this
指向了window
,所以这里a
的值为1
- IIFE返回的函数中的
console.log(a + this.a)
,根据2可以知道前者的值为1
(这里形成了闭包)后者的this
取决于实际调用的情况 - 根据
obj.fun()
,满足规则1:对象打点调用它的方法函数,则函数的this是这个打点的对象,所以a + this.a
中的this
指向obj
,所以结果为3
五、定时器与延时器调用函数
- 定时器、延时器调用函数,上下文是
window
对象
javascript
setTimeout(function() {
console.log(this)
}, 200)
javascript
let i = 0
let timer = setInterval(function() {
console.log(this)
i++
if(i > 1){
clearInterval(timer)
}
}, 1000)
练习:
下面这道题的运行后打印的结果是?
javascript
function fun() {
console.log(this.a + this.b)
return this.a + this.b
}
var obj = {
a: 1,
b: 2,
fun
}
var a = 3
var b = 4
setTimeout(obj.fun, 200)
答案:
7
解析:
- 首先这里设置了一个定时器,规定200毫秒以后执行
obj.fun
中的函数体- 相当于定义了这样一个定时器:
javascript
setTimeout(obj.fun, 200)
// 相当于
setTimeout(function() {
console.log(this.a + this.b)
return this.a + this.b
}, 200)
- 满足规则5:定时器、延时器调用函数,上下文是window对象,所以此时的打印结果为
7
- 注意点:下面这种写法和上面完全不一样
javascript
setTimeout(function() {
obj.fun()
}, 200)
- 这里的意思是200毫秒后执行
obj.fun()
这个函数,此时满足规则1:对象打点调用它的方法函数,则函数的上下文是这个打点的对象,所以此时的this
为obj
这个对象,结果就为3
六、事件处理函数
- 事件处理函数的上下文是绑定事件的DOM元素
比如给一个div
元素添加了一个点击事件,则当前绑定的this
指向该div
元素
html
<body>
<div id="test_div1">TEST1</div>
<div id="test_div2">TEST2</div>
<script>
let testDOM1 = document.getElementById('test_div1')
let testDOM2 = document.getElementById('test_div2')
function clickHandle() {
console.log(this)
}
testDOM1.addEventListener('click', clickHandle)
testDOM2.addEventListener('click', clickHandle)
</script>
</body>
七、通过call、apply调用的函数
call
和apply
可以指定函数的this
指向- 详细用法参考之前写的文章:JavaScript | apply、bind与call
关于bind
- 多次
bind
时只认第一次bind
的值(使用bind
后相当于永久改变this
的指向)
练习:
javascript
let obj = {
func() {
let that = this
const arrowFunc = function() {
console.log(that._name)
}
return arrowFunc
},
_name: "obj",
}
obj.func()()
obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })()
答案:
依次输出obj
、bindObj
解析:
obj.func()()
- 上面这段代码先是调用了
obj.func()
这个方法,该方法返回一个函数arrowFunc
- 在
obj.func()
这个方法中首先通过that
保存了当前的this
,而当前的this
因为满足规则1:对象打点调用它的方法函数,则函数的this
为这个打点的对象,所以此时的that
就指向obj
对象 - 因为
obj.func()
的返回值为arrowFunc
函数,所以obj.func()()
就相当于arrowFunc()
,调用arrowFunc()
会输出that._name
,上一步中我们知道了that
此时的指向为obj
,所以此时会输出obj
- 上面这段代码先是调用了
obj.func.bind({ _name: "bindObj" }).apply({ _name: "applyObj" })()
obj.func.bind({ _name: "bindObj" })
通过bind
返回一个原函数的拷贝,并拥有指定的 this(此时为{ _name: "bindObj" }) ,注意bind方法不会直接调用函数- 通过
apply({ _name: "applyObj" })
调用之前bind
函数返回的原函数的拷贝 并指定此时的this
为{ _name: "applyObj" }
,因为使用bind
后相当于永久改变this
的指向,所以这个时候不会改变this
的指向为{ _name: "applyObj" }
,此时this
的指向依旧是上一步通过bind
指定的{ _name: "bindObj" }
对象,函数执行后会得到内部返回的arrowFunc
函数 - 调用
arrowFunc
函数,因为通过bind
方法指定了this
为{ _name: "bindObj" }
,所以obj.func
函数保存的that
此时指向了{ _name: "bindObj" }
对象,所以打印结果为bindObj
八、通过new关键字调用函数(构造函数)
- 通过
new
关键字调用函数,函数中的this
此时指向这个"new
出来的对象",且优先级高于bind
javascript
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
};
console.log(this)
}
Person.bind(1)
let person1 = new Person("Walter White", 50, "Chemist");
特殊情况:箭头函数
- 箭头函数没有自己的
this
,箭头函数中的this
指向外层非箭头函数中的this
。 - 不支持
call
/apply
的函数特性,且使用bind
不会改变this
指向,bind
指定的参数会传给函数 - 箭头函数不可以当做构造函数
- 构造函数是通过new关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this的过程,而箭头函数没有自己的this。
- 创建对象过程,new 首先会创建一个空对象,并将这个空对象的__proto__指向构造函数的prototype,从而继承原型上的方法,但是箭头函数没有prototype因此不能使用箭头作为构造函数,也就不能通过new操作符来调用箭头函数------可参考。
练习:
javascript
var returnThis = () => this
returnThis() // 1
var boss1 = {
name: 'boss1',
returnThis () {
var func = () => this
return func()
}
}
returnThis.call(boss1) // 2
var boss1returnThis = returnThis.bind(boss1)
boss1returnThis() // 3
boss1.returnThis() // 4
var boss2 = {
name: 'boss2',
returnThis: boss1.returnThis
}
boss2.returnThis() // 5
答案:
- 1->
window
- 2->
window
- 3->
window
- 4->
boss1
- 5->
boss2
解析:
- 箭头函数中的
this
指向外层非箭头函数中的this
,此时外层为window
,所以这个时候对应的this
为window
- 不支持
call
/apply
的函数特性,所以此时的this
指向为外层非箭头函数中的this
为window
- 箭头函数中使用
bind
不会改变this
指向,此时的this
指向为外层非箭头函数中的this
为window
boss1.returnThis()
返回一个匿名函数的调用,箭头函数中的this
指向外层非箭头函数中的this
,通过对象打点方式调用的函数,函数的this
为这个打点的对象,所以此时的this
为boss1boss2.returnThis()
返回一个匿名函数的调用,箭头函数中的this
指向外层非箭头函数中的this
,通过对象打点方式调用的函数,函数的this
为这个打点的对象,所以此时的this
为boss2
严格模式下的变化
- 非严格模式下全局作用域中的函数中的
this
指向window
对象。 - 严格模式中全局作用域中函数中的
this
指向undefined
javascript
function test1() {
console.log("test1:",this)
}
function test2() {
// 只给test2函数开启严格模式
"use strict"
console.log("test2:",this)
}
test1()
test2()
javascript
'use strict'
// 全局作用域中函数中的this指向undefined
function test() {
console.log('test->this:', this) // undefined
}
test()
- 非严格模式下构造函数不使用
new
也可以作为普通函数调用,this
指向全局对象
javascript
function Person() {
this.sex = 'male'
console.log(this, this.sex)
}
Person()
- 严格模式下,如果 构造函数不加
new
调用,this
指向的是undefined
如果给他赋值则 会报错
javascript
'use strict'
function Person() {
this.sex = 'male'
console.log(this, this.sex)
}
Person()
new
实例化的构造函数指向创建的对象实例。
javascript
'use strict'
function Star() {
this.sex = 'male'
console.log(this, this.sex)
}
new Star()
- 定时器中的
this
指向的还是window
javascript
setTimeout(function () {
console.log(this)
}, 100)
- 事件处理函数或者对象打点方式调用的函数指向的还是调用者
javascript
'use strict'
let obj = {
a: 1,
b: 2,
fn: function () {
return this
}
}
console.log(obj.fn() === obj)
javascript
'use strict'
let testDOM1 = document.getElementById('test_div1')
let testDOM2 = document.getElementById('test_div2')
function clickHandle() {
console.log(this)
}
testDOM1.addEventListener('click', clickHandle)
testDOM2.addEventListener('click', clickHandle)
- IIFE中的
this
指向为undefined
javascript
'use strict';
(function () {
console.log(this)
})()