var bar = {
myName:"juejin.cn",
printName: function () {
console.log(myName)
}
}
function foo() {
let myName = "掘金"
return bar.printName
}
let myName = "稀土掘金"
let _printName = foo()
_printName()
bar.printName()
上面代码,在printName
函数中使用的变量myName
是属于全局作用域下面的,所以最终打印出来的值都是“稀土掘金”。这是因为JS的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。
按常理来讲,调用bar.printName
方法时,该方法内部的变量 myName
应该使用 bar
对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计的,比如 C++ 代码:
#include <iostream>
using namespace std;
class Bar{
public:
char* myName;
Bar(){
myName = "juejin.cn";
}
void printName(){
cout<< myName <<endl;
}
} bar;
char* myName = "稀土掘金";
int main() {
bar.printName();
return 0;
}
在这段 C++ 代码中,同样调用了 bar
对象中的 printName
方法,最后打印出来的值就是 bar
对象的内部变量 myName
值——“junjin.cn”,而并不是最外面定义变量 myName
的值——“稀土掘金”,所以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JS 的作用域机制并不支持这一点,基于这个需求,JS 又搞出来另外一套 this 机制。
所以,在 JS 中可以使用 this 实现在 printName
函数中访问到 bar
对象的 myName
属性。只需要修改代码如下:
printName: function () {
console.log(this.myName)
}
那么打印结果,将变为“juejin.cn”
JavaScript 中的 this
this 是和执行上下文绑定的,每一个执行上下文中都有一个this。
JS中只有 this 变量是在运行时动态绑定
的,其它所有变量都是编译时静态绑定
的。
执行上下文主要分为三种:
- 全局执行上下文;
- 函数执行上下文;
- eval执行上下文;
所以对应的 this 也有三种:
- 全局执行上下文中的 this;
- 函数中的 this;
- eval 中的 this;
全局执行上下文中的 this
可以在控制台中输入console.log(this)
来打印出来全局执行上下文中的 this,最终输出的是 window
对象。
所以可以得出这样一个结论:全局执行上下文中的 this 是指向 window
对象的。(更严谨的说法是,在非严格模式下,全局上下文的this指向的是window对象,严格模式下指的是undefined)
这也是 this 和作用域链的唯一交点,作用域链和this是两套不同系统,作用域链的最底端包含了
window
对象,全局执行上下文中的 this 也是指向window
对象。
函数执行上下文中的 this
function foo(){
console.log(this)
}
foo()
执行这段代码,打印出来的也是window
对象,这说明在默认情况下调用一个函数,其执行上下文中的 this 也是指向window
对象的。
eval执行上下文中的this
eval()是JS中的一个函数,它接受一个字符串参数,并将该字符串作为JS代码执行。非常强大,同时也存在一些风险,需谨慎使用。
eval()中的 this 指向取决于 eval 被调用的环境。
-
如果是在全局上下文中调用eval, this 指向window;
eval(function() { console.log(this); // 在全局上下文中指向 window }());
-
如果在函数上下文中调用eval,this 绑定到该函数的this上下文
function myFunction() { eval(function() { console.log(this); // 指向 myFunction 的 this 上下文 }()); } myFunction();
-
严格模式下,无论eval在哪里调用,this 总是指向
undefined
-
嵌套的eval调用,this 指向与最外层调用的上下文一致
function outer() { eval(function() { eval(function() { console.log(this); // 指向 outer 函数的 this 上下文 }.bind(this)); }); } outer();
如果想要使执行上下文中的 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
对象,因为通过打印 bar
对象,可以看出 bar
的 myName
属性已经由“掘金”变为“稀土掘金”了,同时在全局执行上下文中打印 myName
,JS引擎提示该变量为定义。
除了 call
方法外,还可以使用 bind
和 apply
方法来设置函数执行上下文中的 this。
2. 通过对象调用方法设置
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
}
}
myObj.showThis()
在这段代码中,定义了一个 myObj
对象,该对象是由一个 name
属性和一个 showThis
方法组成的,然后再通过 myObj
对象来调用 showThis
方法。执行这段代码,你可以看到,最终输出的 this 值是指向 myObj
的。
所以,使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。
其实,你也可以认为 JS 引擎在执行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
。当执行 new CreateObj()
的时候,JS引擎做了如下四件事:
- 首先创建了一个空对象
tempObj
; - 接着调用
CreateObj.call
方法,将tempObj
作为call
方法的参数,这样当CreateObj
的执行上下文创建时,它的 this 就指向了tempObj
对象; - 然后执行
CreateObj
函数,此时的CreateObj
函数执行上下文中的 this 指向了tempObj
对象; - 最后返回
tempObj
对象。
伪代码如下:
var tempObj = {}
CreateObj.call(tempObj)
return tempObj
这样,就通过 new
关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。
参考
this 的设计缺陷以及应对方案
同变量提升一样,JS中的this也会带来一些不符合直觉的执行结果。
1. 嵌套函数中的 this 不会从外层函数中继承
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
function bar(){console.log(this)}
bar()
}
}
myObj.showThis()
在这段代码的 showThis
方法里面添加了一个 bar
方法,然后接着在 showThis
函数中调用了 bar
函数,那么现在的问题是:bar
函数中的 this 是什么?
如果你刚接触 JS,那么你可能会很自然地觉得,bar
中的 this 应该和其外层 showThis
函数中的 this 是一致的,都是指向 myObj
对象的,这很符合人的直觉。但实际情况却并非如此,执行这段代码后,你会发现函数 bar
中的 this 指向的是全局 window 对象,而函数 showThis
中的 this 指向的是 myObj
对象。这就是 JS 中非常容易让人迷惑的地方之一,也是很多问题的源头。
this 的绑定可通过
new
、call/apply/bind
、对象调用
等方式进行,但不会通过作用域链绑定 this,所以,对于独立调用的函数,如果未进行有效的 this 绑定的话,this 就会绑定到window
对象(非严格模式)或者undefined
(严格模式)
可以通过以下两个小技巧来解决这个问题。
-
在函数内部声明一个变量self用来保存this
在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 没有作用域的限制,这点和变量不一样,所以嵌套函数不会从调用它的函数中继承 this,这样会造成很多不符合直觉的代码。
2. 普通函数中的 this 默认指向全局对象 window
在默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window
的。
在实际工作中,我们并不希望函数执行上下文中的 this 默认指向全局对象,因为这样会打破数据的边界,造成一些误操作。如果要让函数执行上下文中的 this 指向某个对象,最好的方式是通过 call
方法来显示调用。
这个问题可以通过设置 JS 的“严格模式”来解决。在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined
,这就解决上面的问题了。
小结
为了满足在对象内部的方法中使用对象内部的属性的需求, 引入了 this。
根据 this 所在的执行上下文不同, 分为:
- 全局执行上下文的 this: 指向
window
(非严格模式) 或undefined
(严格模式); - 函数执行上下文的 this; 默认指向
window
; - eval执行上下文的 this;
可以通过:
- 隐式绑定:obj.fn(),this 被绑定到obj上;
- 显示绑定:call、apply、bind,可以直接指定 this 的绑定对象;
- new绑定:创建一个全新的对象,并将这个新对象绑定到函数调用的 this
来改变 this 的指向。
注意 this 带来的问题:
嵌套函数中的this不会继承外层函数的this,可以通过
- 使用self保存this指针;
- 箭头函数
来解决。