上下文this是一个面试频率非常高问题,今天我们一起来了解了解this
执行上下文
首先,我们先来看看什么是执行上下文
前一篇文章我们有说到过,代码运行环境主要分为两类
- 全局运行环境
- 函数运行环境
而javascript代码的运行,主要分为两个阶段
- 编译阶段
- 执行阶段
代码编译主要由编译器完成,而代码执行,由js引擎完成,而执行上下文,其实就是在执行阶段创建的。
代码每进入到一个运行环境,都会创建一个当前环境的执行上下文。而执行上下文在创建的时候,会做以下事情
- 首先会生成变量对象:上篇文章我们说过,变量对象内部存储着当前环境的所有变量和函数,而变量对象最初是只有arguments参数列表的。所以在生成变量对象时,其实会先创建arguments, 然后检查function函数声明, 再检查var 变量声明。故变量提升,也是在这个阶段实现的。
- 之后,会确定this的指向。所以这个时候,我们就能想到可能经常会听过的一句话:当一个函数没有被调用时,你永远不知道他的this指向了谁
- 再之后,会进行变量的赋值以及函数的引用等。
所以this,其实只是执行上下文中的一个属性。它只有在函数被调用的时候才会被确定。
this
下面我们来看下this的使用场景
this的使用其实主要分为以下几种情况
1、全局上下文中
全局上下文中,this始终指向window
var a = 10
console.log(this.a)
输出
10
2、 普通函数调用 (以()的形式直接调用)
当一个函数直接以()的形式调用时,函数内this始终指向全局上下文window(严格模式下是undefind)
setTimeout(function () {
aaa()
}, 0)
function aaa () {
console.log(this) // window
}
这里有一种立即指向函数或者回调函数的情况,看下面代码
var promise = new Promise(function (resove, reject) {
console.log(this)
})
setTimeout(function () {
console.log(this)
}, 1000)
输出
window
window
上述代码中,new Promise内部是一个函数,但是这个函数内的this是指向window的,这是因为这个函数在Promise()这个方法内其实是通过 fun() 普通函数的形式去调用的,故指向window(看了我前面写Promise源码的人应该就能知道这个函数是怎么调用的)
而setTimeout的回调函数也是一样,在setTimeout这个方法中也是通过普通调用的方式去调用了这个函数,故this也指向window
3、 作为对象的方法被调用
当函数作为某个对象的方法被对象调用时,this指向调用该方法的对象
var obj = {
name: 'chen',
say: say
}
function say () {
console.log(this)
console.log(this.name)
}
obj.say()
输出
{ name: 'chen', say: [Function: say] }
chen
还有个是事件方法被触发时,方法内的this指向触发事件的dom元素。因它其实也是属于被dom对象所调用,所以我就把它归为这一类了
<button class="btn">点我</button>
<script>
var btn = document.querySelector('.btn')
btn.onclick = function () {
console.log(this)
}
</script>
输出
<button class="btn">点我</button>
4、new 关键字实例化对象时调用
使用new实例化一个对象时调用构造函数,那么函数内的this就会指向new 出来的那个实例
function China (name, age) {
this.name = name
this.age = age
}
const zhangSan = new China('张三', 18)
console.log(zhangSan.name, zhangSan.age)
输出
张三 18
5、 通过call, apply 改变this指向
当通过call,apply调用某个方法时,可以指定该方法调用时的this指向。故这种行为也被称为方法借用
var zhang = {
name: '张三',
age: 18,
say: say,
sayAag: sayAag
}
var wang = {
name: '王五',
age: 20
}
function say () {
console.log(this.name)
}
function sayAag () {
console.log(this.age)
}
zhang.say.call(wang)
zhang.sayAag.apply(wang)
输出
王五
上述代码可以看出,zhang对象有个say的方法,而 wang对象并没有,此时zhang调用他的say方法时,使用call方法将this指向wang,那么say内部输出的name其实就是wang的name了,apply也是一样。
call和apply区别在于后面第参数的传递,一个是传递参数列表(一个一个的形式传递),一个是传递包含参数的数组。
obj.fun.call(obj2, a, b, c)
obj.fun.apply(obj2, [a, b, c])
6、bind绑定this指向
通过bing给一个方法绑定一个对象后,那么这个方法内部this就始终指向被绑定的对象(不会再发生变化)
bind方法和call,apply不同,bind不会立即触发函数的调用,他其实是返回了一个新的函数,并将这个新的函数的this指向了被绑定的对象。
注意:bind只会生效一次,也就是说你如果给新返回的函数再使用bind重新绑定一个对象,那么,是不会生效的(this指向不会再次发生改变)
var name = '张三'
var obj = {
name: '王五',
age: 20
}
var obj2 = {
name: '李四',
age: 28
}
function say () {
console.log(this.name)
}
var fun = say.bind(obj)
fun()
fun.call(obj2)
输出
王五
王五
上述代码可以看出,第一个fun()直接通过普通方式调用时,原本this应该是指向window,输出会是张三的,但通过bind使他的this始终指向了obj,故输出的是obj的name
而此时我们通过call方法去改变this指向时,this指向是不会再发生变化的。
7、箭头函数
箭头函数内部的this,是指向这个箭头函数所处的上下文中的this指向
obj = {
name: '张三',
say: () => {
console.log(this)
}
}
obj.say()
输出
window
上述代码可以看出,调用obj.say this原本是指向obj的,但因为say方法用的是箭头函数,那么this指向这个箭头函数所处的上下文的this。而这个箭头函数是不是处在全局环境中啊。所以指向window。再看一个例子
var obj = {
say: say
}
function say () {
setTimeout(function () {
console.log(this)
}, 1000)
setTimeout(() => {
console.log(this)
}, 2000)
}
obj.say()
输出
window
obj
可以看出,setTimeout内的回调函数默认是普通方式调用的(这个我们在第一种类型中说过),故第一个输出window,而当回调函数以箭头函数的形式使用时,他的this就指向函数外的上下文this了,而函数外指的是say函数内的this, 当obj.say()调用时,say函数内的this是不是指向obj啊, 所以setTimeout内的this也会指向obj
this就写到这了,喜欢就点个赞吧