本篇博客主要是对 Javcscript this 学习理解
# 前言
我们先从一个例题开始:
let foo = '哇'
let obj = {
foo: '蛙哇',
fn: function() {
console.log(foo)
}
}
obj.fn()
最后返回的是外部的 哇
属性变量, 而不是对象内部的变量。我们知道在对象内部方法中使用对象内部属性是一种常见的需求。但 JavaScript 的作用域不支持这一点,基于这个需求,JavaScript 又搞出另一套 this
机制。
所以,在 JavaScript 中通过 this
就可以访问对象里面的属性,调整上面的代码:
fn: function() {
console.log(this.foo)
}
最终返回的是对象的内部属性。
开始之前,需要我了解的是 JavaScript 中的作用域链和 this
是两套不一样的系统,他们之间没有太多的联系。
# JavaScript中this是什么
关于 this
,还得从执行上下文说起,执行上下文中除了包含变量环境、词法环境、外部环境,还包含了 this
,一图胜前言,具体可参考下图:
从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。而执行上下文主要分为三种-全局执行上下文、函数执行上下文 和 eval 执行上下文。所以对应的 this 也分为三种-全局执行上下文中的 this、函数执行上下文中的 this 和 eval 执行上下文中的 this。
由于 eval 使用的不多,所以这里只讨论全局和函数执行上下文中的 this。
# 全局执行上下文中的 this
console.log(this) // window
你可以在控制台打印 this, 最终输出的是 window 对象。所以你可以得出这样一个结论:全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。
# 函数执行上下文中的 this
现在我们知道了全局对象中 this 的指向 window 对象,那么接下来,重点分析函数执行上下文的 this,还是先看下面这段代码:
function foo(){
console.log(this)
}
function bar(){
'use strict'
console.log(this)
}
foo() // window
bar() // undefined
执行上面这段代码,打印出来也是 window 对象,这说明在非严格模式的默认情况下调用函数,其执行上下文中的 this 也是指向 window 对象的。严格模式下,this 为 undefined 。估计你会好奇,那能不能设置执行上下文中的 this 来指向其他对象呢?答案是肯定的。通常情况下,有下面三种方式来设置函数执行上下文中的 this 值。
# 1.通过函数的call方法设置
你可以通过函数的 call 方法来设置函数执行上下文的 this 指向,比如下面这段代码,我们就并没有直接调用 foo 函数,而是调用了 foo 的 call 方法,并将 bar 对象作为 call 方法的参数。
let bar = {
myName : "哇",
test1 : 1
}
function foo(){
this.myName = "蛙哇"
}
foo.call(bar)
console.log(bar)
console.log(myName)
执行这段代码,然后观察输出结果,你就能发现 foo 函数内部的 this 已经指向了 bar 对象。其实除了 call 方法,你还可以使用 bind 和 apply 方法来设置函数执行上下文中的 this。
# 2. 通过对象调用方法设置
要改变函数执行上下文中的 this 指向,除了通过函数的 call 方法来实现外,还可以通过对象调用的方式,比如下面这段代码:
var myObj = {
name : "蛙哇",
showThis: function(){
console.log(this)
}
}
myObj.showThis()
执行这段代码,你可以看到,最终输出的 this 值是指向 myObj 的。
所以,你可以得出这样的结论:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。
其实,你也可以认为 JavaScript 引擎在执行myObject.showThis()
时,将其转化为了:
myObj.showThis.call(myObj)
接下来我们稍微改变下调用方式,把 showThis 赋给一个全局对象,然后再调用该对象,代码如下所示:
var myObj = {
name : "哇",
showThis: function(){
this.name = "蛙哇"
console.log(this)
}
}
var foo = myObj.showThis
foo()
执行这段代码,你会发现 this 又指向了全局 window 对象。
所以通过以上两个例子的对比,你可以得出下面这样两个结论:
- 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
- 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
# 3. 通过构造函数中设置
function CreateObj(){
this.name = "蛙哇"
}
var myObj = new CreateObj()
在这段代码中,我们使用 new 创建了对象 myObj,那你知道此时的构造函数 CreateObj 中的 this 到底指向了谁吗?其实,当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
- 首先创建了一个空对象 tempObj;
- 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
- 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
- 最后返回 tempObj 对象。
为了直观理解,我们可以用代码来演示下:
var tempObj = {}
CreateObj.call(tempObj)
return tempObj
# this 缺陷和解决方案
# 1. 嵌套函数中的 this 不会从外层函数中继承
var myObj = {
name : "蛙哇",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()
执行这段代码后,你会发现函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。这就是 JavaScript 中非常容易让人迷惑的地方之一,也是很多问题的源头。
你可以通过一个小技巧来解决这个问题,比如在 showThis 函数中声明一个变量 self 用来保存 this,然后在 bar 函数中使用 self,代码如下所示:
var myObj = {
name : "蛙哇",
showThis: function(){
console.log(this)
var self = this
function bar(){
self.name = "哇"
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
执行这段代码,你可以看到它输出了我们想要的结果,最终 myObj 中的 name 属性值变成了“哇”。其实,这个方法的的本质是把 this 体系转换为了作用域的体系。
其实,你也可以使用 ES6 中的箭头函数来解决这个问题,结合下面代码:
var myObj = {
name : "蛙哇",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = "哇"
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
执行这段代码,你会发现它也输出了我们想要的结果,也就是箭头函数 bar 里面的 this 是指向 myObj 对象的。这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。
通过上面的讲解,你现在应该知道了 this 没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承 this,这样会造成很多不符合直觉的代码。要解决这个问题,你可以有两种思路:
- 第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
- 第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。