前言
本文是【JavaScript专栏】系列文章。
前面两篇文章 深入浅出JavaScript执行机制(上)、深入浅出JavaScript执行机制(中) 已经介绍了 【变量提升、JS 代码执行流、调用栈、块级作用域、作用域链、词法作用域、闭包】 等相关工作原理。本文将从 JavaScript 执行上下文的视角讲一下 this。
引言
先看下一段代码,它的执行结果是什么:
js
var bar = {
myName: "juejin.cn",
printName: function () {
console.log(myName)
}
}
let myName = "juejin"
bar.printName()
根据前两篇文章的介绍,相信大家已经知道结果了,在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是"juejin"。这是因为作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。
但是按照常理来说,调用 bar.printName() 时,访问到的 myName 变量应该是 bar 对象中的,因为这是一个整体,大多数面向对象语言都是这样设计的。面向对象语言中:在对象内部的方法中使用对象内部的属性是一个非常普遍的需求 ,但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又设计出来另外一套 this 机制。
为了实现在 printName 函数中访问到 bar 对象的 myName 属性, printName 函数可以这样改写:
js
printName: function () {
console.log(this.myName)
}
注意一点:作用域链 和 this 是两套不同的系统,它们之间基本没太多联系。
JavaScript 中的 this 是什么
前面说了,执行上下文中包含 变量环境、词法环境、外部环境(outer),其实还有个 this。
图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。
执行上下文分为:全局执行上下文、函数执行上下文、eval 执行上下文。所以对应的 this 有:全局执行上下文中的 this、函数执行上下文中的 this、eval 中的 this。
全局执行上下文中的 this
在控制台中输入console.log(this)来打印出来全局执行上下文中的 this,最终输出的是 window 对象.
所以:全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。
函数执行上下文中的 this
js
function foo(){
console.log(this)
}
foo()
foo 函数内部打印出来 this 值,也是 window 对象,这说明在默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的。但是可以通过以下的方式去设置函数执行上下文中的 this 值。
1、通过函数的 call 方法设置 this
通过函数的 call 方法来设置函数执行上下文的 this 指向。 如下:
js
let bar = {
myName : "zhihu",
test1 : 1
}
function foo(){
this.myName = "juejin"
}
foo.call(bar)
console.log(bar) // 'juejin'
console.log(myName) // Uncaught ReferenceError: myName is not defined
从输出结果中看出:foo 函数内部的 this 已经指向了 bar 对象。bar.myName = 'juejin',同时在全局执行上下文中打印 myName,提示该变量未定义。
当然还可以使用 apply、bind 方法可以设置函数执行上下文的 this。
2、通过对象调用方式设置 this
js
var myObj = {
name : "juejin",
showThis: function(){
console.log(this)
}
}
myObj.showThis()
通过 myObj 对象来调用 showThis 方法,最终输出的 this 值是指向 myObj。
所以:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。
再来看下这段代码:
js
var myObj = {
name : "极客时间",
showThis: function(){
this.name = "极客邦"
console.log(this)
}
}
var foo = myObj.showThis
foo()
发现 this 又指向全局对象 window。
所以:在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
通过构造函数设置 this
js
function CreateObj(){
this.name = "juejin"
}
var myObj = new CreateObj()
使用 new 关键字创建了对象 myObj,此时的构造函数 CreateObj 中的 this 到底指向了谁?
new CreateObj(),主要做了以下工作:
- 创建一个新对象
obj
; - 将构建函数中的
this
绑定到新建的对象obj
上; - 返回新对象
obj
;
js
var obj = {}
CreateObj.call(obj)
return obj
所以,此时的构造函数 CreateObj 中的 this 指向新对象本身。
this 的设计缺陷
分析一下以下代码:
js
var myObj = {
name : "juejin",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()
可能会很自然地觉得,bar 中的 this 应该和其外层 showThis 函数中的 this 是一致的,都是指向 myObj 对象的,这很符合人的直觉.
但实际情况是:函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象。
所以这是 this 的一点设计缺陷:嵌套函数中的 this 不会从外层函数中继承。
可以通过两种方式去解决这个问题:
1、声明一个变量 self 用来保存 this;
js
var myObj = {
name : "zhihu",
showThis: function(){
console.log(this)
const self = this
function bar(){
self.name = "juejin"
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
最终 myObj 中的 name 属性值变成了"juejin"。其实,这个方法的的本质是把 this 体系转换为了作用域的体系。
2、使用箭头函数;
js
var myObj = {
name : "zhihu",
showThis: function(){
console.log(this)
const bar = ()=> {
this.name = "juejin"
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
使用 ES6 中的箭头函数也能得到我们想要的结果,也就是箭头函数 bar 里面的 this 是指向 myObj 对象的。这是因为 ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。
总结
- 全局执行上下文中的 this,指向 window 对象;
- 函数执行上下文中的 this:
- 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
- 当函数作为对象的方法调用时,函数中的 this 就是该对象, 如: obj.foo() ;
- 嵌套函数中的 this 不会继承外层函数的 this 值;
- 设置函数中的 this:
- 通过调用函数的 call、apply、bind 方法可以改变 this 的指向;
- 通过对象调用方法来设置;
- 通过构造函数来设置;
- 箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this;