先看一段代码
1 | var bar = { |
在printName
函数里面使用的变量myName
是属于全局作用域下面的,所以最终打印出来的值都是“李四”。这是因为JavaScript
语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。
不过按照常理来说,调用bar.printName
方法时,该方法内部的变量myName
应该使用bar
对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计的,比如我用 C++ 改写了上面那段代码,如下所示:
1 | #include <iostream> |
在这段 ```C++ 代码中,我同样调用了
bar对象中的
printName方法,最后打印出来的值就是
bar对象的内部变量
myName值
——test,而并不是最外面定义变量
myName的值——“李四”,所以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是
JavaScript的作用域机制并不支持这一点,基于这个需求,
JavaScript 又搞出来另外一套
thi`s机制。
所以,在 JavaScript
中可以使用this
实现在printName
函数中访问到ba
r对象的myName
属性了。具体该怎么操作呢?你可以调整printName
的代码,如下所示:
1 | printName: function () { |
作用域链和this
是两套不同的系统,它们之间基本没太多联系。
JavaScript 中的 this 是什么
关于this
,我们还是得先从执行上下文说起。执行上下文中包含了变量环境、词法环境、外部环境,还有一个this
,this
是和执行上下文绑定的,也就是说每个执行上下文中都有一个this
。执行上下文主要分为三种——全局执行上下文、函数执行上下文和eva
l执行上下文,所以对应的 this
也只有这三种——全局执行上下文中的this
、函数中的this
和eval
中的this
。
全局执行上下文中的 this
你可以在控制台中输入console.log(this)
来打印出来全局执行上下文中的this
,最终输出的是window
对象。所以你可以得出这样一个结论:全局执行上下文中的this
是指向window
对象的。这也是this
和作用域链的唯一交点,作用域链的最底端包含了window
对象,全局执行上下文中的this
也是指向window
对象。
函数执行上下文中的 this
1 | function foo() { |
我们在foo
函数内部打印出来this
值,执行这段代码,打印出来的也是window
对象,这说明在默认情况下调用一个函数,其执行上下文中的this
也是指向window
对象的。估计你会好奇,那能不能设置执行上下文中的this
来指向其他对象呢?答案是肯定的。通常情况下,有下面三种方式来设置函数执行上下文中的this
值。
1. 通过函数的 call 方法设置
你可以通过函数的call
方法来设置函数执行上下文的this
指向,比如下面这段代码,我们就并没有直接调用foo
函数,而是调用了foo
的call
方法,并将bar
对象作为call
方法的参数。
1 | let bar = { |
执行这段代码,然后观察输出结果,你就能发现foo
函数内部的this
已经指向了bar
对象,因为通过打印bar
对象,可以看出bar
的myName
属性已经由“李四”变为“张三”了,同时在全局执行上下文中打印myName
,JavaScript
引擎提示该变量未定义。
其实除了call
方法,你还可以使用bind
和apply
方法来设置函数执行上下文中的this
。
通过对象调用方法设置
要改变函数执行上下文中的this指向,除了通过函数的
call`方法来实现外,还可以通过对象调用的方式,比如下面这段代码
1 | var myObj = { |
在这段代码中,我们定义了一个myObj
对象,该对象是由一个name
属性和一个showThis
方法组成的,然后再通过myObj
对象来调用showThis
方法。执行这段代码,你可以看到,最终输出的this
值是指向myObj
的。
所以,你可以得出这样的结论:使用对象来调用其内部的一个方法,该方法的 this
是指向对象本身的。
其实,你也可以认为 JavaScript
引擎在执行myObject.showThis()
时,将其转化为了:
1 | myObj.showThis.call(myObj) |
接下来我们稍微改变下调用方式,把showThis
赋给一个全局对象,然后再调用该对象,代码如下所示:
1 |
|
执行这段代码,你会发现this
又指向了全局window
对象。
所以通过以上两个例子的对比,你可以得出下面这样两个结论:
在全局环境中调用一个函数,函数内部的this
指向的是全局变量window
。
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的this
指向对象本身。
通过构造函数中设置
你可以像这样设置构造函数中的this
,如下面的示例代码:
1 | function CreateObj(){ |
在这段代码中,我们使用new
创建了对象myObj
,那你知道此时的构造函数CreateObj
中的this到底指向了谁吗?
其实,当执行new CreateObj()
的时候,JavaScript
引擎做了如下四件事:
首先创建了一个空对象tempObj
;
接着调用CreateObj.call
方法,并将tempObj
作为call
方法的参数,这样当CreateObj
的执行上下文创建时,它的this就指向了tempObj
对象;
然后执行CreateOb
j函数,此时的CreateObj
函数执行上下文中的this
指向了tempObj
对象;
最后返回tempObj
对象。
为了直观理解,我们可以用代码来演示下:
1 | var tempObj = {} |
这样,我们就通过new
关键字构建好了一个新对象,并且构造函数中的this其实就是新对象本身。
this 的设计缺陷以及应对方案
this
并不是一个很好的设计,因为它的很多使用方法都冲击人的直觉,在使用过程中存在着非常多的坑。下面咱们就来一起看看那些this
设计缺陷
1. 嵌套函数中的 this 不会从外层函数中继承
1 | var myObj = { |
我们在这段代码的showThis
方法里面添加了一个bar
方法,然后接着在showThis
函数中调用了bar函数,那么现在的问题是
bar函数中的
this`是什么?
你可能会很自然地觉得,bar
中的this
应该和其外层showThis
函数中的this
是一致的,都是指向myObj
对象的,这很符合人的直觉。但实际情况却并非如此,执行这段代码后,你会发现函数bar
中的this指向的是全局window
对象,而函数showThis
中的this
指向的是myObj
对象。这就是 JavaScript
中非常容易让人迷惑的地方之一,也是很多问题的源头。
你可以通过一个小技巧来解决这个问题,比如在showThis
函数中声明一个变量self
用来保存this
,然后在bar函数中使用
self`,代码如下所示:
1 |
|
执行这段代码,你可以看到它输出了我们想要的结果,最终myObj
中的name
属性值变成了“李四”。其实,这个方法的的本质是把this
体系转换为了作用域的体系。
其实,你也可以使用 ES6
中的箭头函数来解决这个问题,结合下面代码:
1 | var myObj = { |
执行这段代码,你会发现它也输出了我们想要的结果,也就是箭头函数bar
里面的this
是指向myObj
对象的。这是因为 ES6
中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的this
取决于它的外部函数。
你现在应该知道了this
没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承this
,这样会造成很多不符合直觉的代码。要解决这个问题,你可以有两种思路:
第一种是把this
保存为一个self
变量,再利用变量的作用域机制传递给嵌套函数。
第二种是继续使用this
,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的this
。
普通函数中的 this 默认指向全局对象 window
在默认情况下调用一个函数,其执行上下文中的this
是默认指向全局对象window
的。
不过这个设计也是一种缺陷,因为在实际工作中,我们并不希望函数执行上下文中的this默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的this
指向某个对象,最好的方式是通过call
方法来显示调用。
这个问题可以通过设置 JavaScript
的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的this
值是undefined
,这就解决上面的问题了。
总结
首先,在使用this
时,为了避坑,你要谨记以下三点:
当函数作为对象的方法调用时,函数中的this
就是该对象;
当函数被正常调用时,在严格模式下,this
值是undefined
,非严格模式下this
指向的是全局对象window
;
嵌套函数中的this
不会继承外层函数的this
值。
最后,因为箭头函数没有自己的执行上下文,所以箭头函数的this
就是它外层函数的this
。