JavaScript中的this, 究竟指向什么?
在JavaScript代码的不同位置中,this所指向的数据是不一样的。比如大部分同学都知道,在对象的函数属性方法中,this指向对象本身;在构造函数中,this指向要生成的新对象。事实上,this指向的逻辑不止这几种,this也不只是与原型链有关。在这里我们研究一下,在不同的场景下,JavaScript中的this, 究竟指向什么。
globalThis
在观察各类this之前,先来了解一下globalThis的概念。globalThis是从不同的JavaScript环境中获取全局对象的方式。
由于在部分环境或者上下文中,使用this是无法直接获取到全局对象的,例如一些模块化的JS代码内,以及在部分上下文的严格模式下(具体场景和区别后面会描述)。因此globalThis提供了一个标准的方式来获取不同环境下的全局this对象。这个对象在不同的JavaScript环境中是不一样的。
// 浏览器环境
console.log(globalThis)
console.log(globalThis === window)
/* 输出
Window {window: Window, self: Window, document: document, ...省略 }
true
*/
// 严格模式下表现一致
// Node.js环境
console.log(globalThis)
console.log(globalThis === global)
/* 输出
<ref *1> Object [global] { ...省略 }
true
*/
// 严格模式下表现一致
可以看到,在浏览器中globalThis就是window对象,而在Node.js中,globalThis是global对象。我们直接在命令行中使用var定义的全局变量,实际上会被作为globalThis的属性(但let和const不会)。这里我们不过多介绍全局对象,感兴趣的同学可以自行了解更多。
命令行全局上下文
我们先试一下,直接在命令行的全局上下文中使用this,所指向的值是什么。
浏览器命令行
浏览器命令行,即是在浏览器调试工具的Console中使用this。
// 浏览器命令行
console.log(this)
console.log(this === globalThis)
console.log(this === window)
/* 输出
Window {window: Window, self: Window, document: document, ...省略 }
true
true
*/
// 严格模式下表现一致
可以看到,在浏览器命令行的全局上下文中直接使用this,实际指向的是globalThis,也就是window对象。
Node.js命令行
Node.js命令行,即使用node命令,不带其他参数,进入交互式shell。
// Node.js命令行
console.log(this)
console.log(this === globalThis)
console.log(this === global)
/* 输出
<ref *1> Object [global] { ...省略 }
true
true
*/
// 严格模式下表现一致
在浏览器命令行的全局上下文中直接使用this,实际指向的是globalThis,也就是global对象。
浏览器HTML中的全局上下文
在浏览器的HTML的全局上下文中的this,是否和命令行中不一样呢?我们来实验一下。
<html>
<body>
<script>
console.log(1, this);
console.log(1, this === globalThis);
console.log(1, this === window);
</script>
<script src="1.js"></script>
</body>
</html>
引用的1.js内容:
console.log(2, this);
console.log(2, this === globalThis);
console.log(2, this === window);
这里尝试了两种情况,一种是内部脚本语句,第二种是外部脚本文件。两种情况下,this都指向window。输出结果:
1 Window {window: Window, self: Window, document: document, ...省略 }
1 true
1 true
2 Window {window: Window, self: Window, document: document, ...省略 }
2 true
2 true
// 严格模式下表现一致
CommonJS中的模块上下文
由于JavaScript发展历史的原因,JavaScript有很多模块化开发规范,比如:AMD,CMD,UMD,CommonJS等等。后来ECMAScript标准官方定义了ESModule模块化规范,现在大部分环境都支持这个规范。我们对目前主流使用的ESModule和CommonJS规范进行说明。首先看一下CommonJS,这种规范最常用在Node.js环境。
单个文件
假设我们有一个js文件,里面没有任何模块化规范相关的代码。我们使用命令行直接执行这个文件node 1.js
,这时模块上下文中this的值指向什么呢?是否和命令行直接执行代码一致呢?这里举个例子看下:
console.log(this)
console.log(this === globalThis);
console.log(this === global);
/* 输出
{}
false
false
*/
// 严格模式下表现一致
注意我们不能在带package.json的项目里面执行,否则项目配置会干扰我们的判断。这时查看结果,看到并不是global,而是一个空对象。这个空对象是什么呢?我们继续实验下:
console.log(this)
console.log(module.exports)
console.log(this === module.exports)
/* 输出
{}
{}
true
*/
// 严格模式下表现一致
原来这时候的this是module.exports!这是CommonJS规范中的模块导出内容。也就是说,在我们没有指定规范,且代码内容也没有任何规范相关指示时,Node.js命令行执行的文件会包裹在CommonJS模块中运行。(后面部分会说明如何使文件在ESModule规范下运行)
这时候this的指向与直接命令行执行代码不同,实际指向的是module.exports。我们再看一个例子:
console.log(1, this)
console.log(1, module.exports)
console.log(1, this === module.exports)
this.a = 1;
exports.b = 2;
console.log(2, this)
console.log(2, module.exports)
console.log(2, exports)
console.log(2, this === module.exports)
module.exports.c = 3;
console.log(3, this)
console.log(3, module.exports)
console.log(3, this === module.exports)
module.exports = {d: 4};
console.log(4, this)
console.log(4, module.exports)
console.log(4, exports)
console.log(4, this === module.exports)
/* 输出
1 {}
1 {}
1 true
2 { a: 1, b: 2 }
2 { a: 1, b: 2 }
2 { a: 1, b: 2 }
2 true
3 { a: 1, b: 2, c: 3 }
3 { a: 1, b: 2, c: 3 }
3 true
4 { a: 1, b: 2, c: 3 }
4 { d: 4 }
4 { a: 1, b: 2, c: 3 }
5 false
*/
// 严格模式下表现一致
这个例子比较长。最上面我们输出了this和module.exports,都是空对象。然后我们将this和exports都添加了不同的属性,发现this和CommonJS的导出对象都增加了,也证明了exports和module.exports实际是同一个对象。然后在module.exports添加了属性,this中也同时被添加了。
然后看最后一步,我们将module.exports整个替换为其它对象,这时候this和module.exports就再不是一个对象了。而exports依旧是旧对象不变。这里this和exports被覆盖的逻辑是一样的,导出的内容会被新的module.exports覆盖。
实际上,Node.js的CommonJS的模块被包装在一个函数中,并且this值设置为module.exports。
CommonJS模块文件
这里新建两个CommonJS模块文件,看看this的指向问题。首先是入口文件a.js内容:
const b = require("./b");
console.log(b);
console.log("a1", this);
console.log("a1", module.exports);
console.log("a1", this === module.exports);
exports.a = 1;
console.log("a2", this);
console.log("a2", module.exports);
console.log("a2", this === module.exports);
然后是被引用的b.js内容:
console.log("b1", this);
console.log("b1", module.exports);
console.log("b1", this === module.exports);
this.b = 2;
module.exports.c = 3;
console.log("b2", this);
console.log("b2", module.exports);
console.log("b2", this === module.exports);
命令行执行node a.js
,然后我们看一下输出结果:
b1 {}
b1 {}
b1 true
b2 { b: 2, c: 3 }
b2 { b: 2, c: 3 }
b2 true
{ b: 2, c: 3 }
a1 {}
a1 {}
a1 true
a2 { a: 1 }
a2 { a: 1 }
a2 true
// 严格模式下表现一致
因为文件a中先引用了文件b,所以文件b先输出。首先可以看到,在文件b中,我们使用this和module.exports本身对导出对象添加了属性,可以看到这并不影响this的指向,this依旧指向导出对象,而且我们添加的属性在文件a中成功的输出了。而文件a中this指向的是该文件独立的导出对象,与文件b的导出对象无关。
this是不是模块内的"全局对象"
前面了解到,我们直接在命令行中使用var定义的全局变量,实际上会被作为globalThis的属性。上面我们也清楚了,在CommonJS模块内的this,并不是全局对象,而是该模块的初始导出对象。那么这里的this,是否可以作为这个模块局部的“全局对象”呢?也就是说,在模块中使用var定义的变量,会不会也挂在this上呢?我们来尝试一下。
console.log(this);
var a = 1;
this.b = 2;
module.exports.c = 3;
console.log(this);
console.log(b);
/* 输出
{}
{ b: 2, c: 3 }
ReferenceError: b is not defined
*/
// 严格模式下表现一致
首先使用var定义了变量a,但是后面输出this时,里面并没有a。然后对this添加了属性b,并尝试直接输出变量b,可以看到变量b找不到,引发了异常。可以得出结论,CommonJS中的this,用法并不像globalThis一样,并不是一个模块内的"全局对象"。
小总结
可以看到,当我们在CommonJS模块中使用this时,this指向的是该模块初始的导出对象。此时我们给this添加属性,属性值也会被导出。但如果我们覆盖了导出对象,此时导出对象就和this无关了。另外,模块中的this并不能类似像全局globalThis一样,不能把模块内var变量作为自身的属性。这个也容易理解,如果真的有这种特性,那模块内的变量统统被导出,模块导出机制会变得非常混乱。
ESModule中的模块上下文
ESModule模块化规范是ECMAScript标准官方定义的,目前大部分环境都支持这个规范。这里列举Node.js和浏览器环境,看一下在模块上下文中,this究竟指向什么。
ESModule和浏览器
我们来看下在浏览器中的表现。首先是index.html:
<html>
<body>
<script>
console.log(1, this);
</script>
<script type="module">
import a from "./a.js";
console.log(a);
console.log(2, this);
console.log(2, globalThis);
</script>
</body>
</html>
然后是index.html中引用的a.js:
console.log(3, this)
const a = 3;
export default a;
我们直接在浏览器中打开,却发现报错:
这是因为直接用Chrome浏览器打开的本地文件,协议为file://
。在这个协议下使用ESModule中的import会被认为是跨域。因此我们在本地启动一个Node服务来提供HTTP协议,用来支持import。
// main.js
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
let data = '';
console.log(`request url: ${req.url}`);
if(req.url === '/') {
data = fs.readFileSync('./index.html');
res.end(data);
} else if(req.url === '/a.js') {
data = fs.readFileSync('./a.js');
// Chrome浏览器要求必须设置Content-type才能使用import
res.setHeader('Content-type', 'text/javascript');
res.end(data);
}
}).listen(8000, () => {
console.log('server start!');
});
然后在命令行执行node main.js
启动服务,再到浏览器输入localhost:8000
即可访问页面。查看浏览器Console输出:
1 Window {window: Window, self: Window, document: document, ...省略 }
3 undefined
3
2 undefined
2 Window {window: Window, self: Window, document: document, ...省略 }
// 严格模式下表现一致
可以看到,在浏览器中非ESModule,this指向window,而在ESModule内,this却是undefined,而globalThis依然指向window不变。
ESModule和Node.js
虽然在Node.js下默认使用CommonJS规范,但Node.js也是支持ESModule的,但需要手动开启,方式主要有两种:
- 文件后缀名为.mjs。
- 所在项目的package.json文件中包含
type: "module"
。
我们在Node.js中开启ESModule,看看this的指向问题。首先是入口文件a.mjs:
import a from "./a.mjs";
console.log(a);
console.log(2, this);
console.log(2, globalThis);
然后是被引入的文件b.mjs:
console.log(3, this)
const a = 3;
export default a;
最后命令行执行node a.mjs
,结果如下:
3 undefined
3
2 undefined
2 <ref *1> Object [global] { ...省略 }
// 严格模式下表现一致
我们构造的示例与浏览器中ESModule的示例基本一致,结果也是一致的,除了在Node.js中,globalThis依然指向global对象。
因此,不管是Node.js还是浏览器环境,在ESModule的模块上下文中,this的指向都是undefined。
场景小总结
通过上面对于全局上下文/模块上下文的实验结果,我们总结出了六种场景:
- 浏览器命令行
- 浏览器HTML中
- Node.js命令行
- CommonJS和Node.js
- ESModule和浏览器
- ESModule和Node.js
后续的实验都会考虑这六种场景,以及对应的严格模式。
普通函数上下文
在普通函数上下文,以及普通函数的嵌套函数中,this指向什么?在不同的环境和模块化规范下,this指向有什么区别呢?我们在不同的场景执行同一段代码,看看结果区别如何。
function fun1() {
console.log(1, this);
function fun2() {
console.log(2, this);
}
fun2();
return fun2;
}
const fun2 = fun1();
fun2();
浏览器命令行
首先看看在浏览器命令行执行的结果:
// 非严格模式
1 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
可以看到,浏览器命令行的不管是单层函数,还是嵌套函数,都是指向window,也就是globalThis。但是在严格模式下表现并不一致:
// 严格模式
1 undefined
2 undefined
2 undefined
在严格模式下,this值为undefined。这里先不解释,在专门的严格模式总结中描述。
浏览器HTML中
看一下在浏览器HTML中执行的结果:
// 非严格模式
1 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
// 严格模式
1 undefined
2 undefined
2 undefined
浏览器HTML中与浏览器命令行的效果完全一致,而且是否严格模式的表现也不一致。
Node.js命令行
看一下在Node.js命令行中执行的结果:
// 非严格模式
1 <ref *1> Object [global] { ...省略 }
2 <ref *1> Object [global] { ...省略 }
2 <ref *1> Object [global] { ...省略 }
// 严格模式
1 undefined
2 undefined
2 undefined
结果与在浏览器中类似,非严格模式指向globalThis,严格模式为undefined。
CommonJS和Node.js
看一下在Node.js中,使用CommonJS规范中执行的结果:
// 非严格模式
1 <ref *1> Object [global] { ...省略 }
2 <ref *1> Object [global] { ...省略 }
2 <ref *1> Object [global] { ...省略 }
// 严格模式
1 undefined
2 undefined
2 undefined
结果还是与前面类似,非严格模式指向globalThis,严格模式为undefined。
ESModule和浏览器
看一下在浏览器中,使用ESModule规范中执行的结果:
// 非严格模式
1 undefined
2 undefined
2 undefined
// 严格模式
1 undefined
2 undefined
2 undefined
ESModule规范中,this的值全都是undefined。
ESModule和Node.js
看一下在Node.js中,使用ESModule规范中执行的结果:
// 非严格模式
1 undefined
2 undefined
2 undefined
// 严格模式
1 undefined
2 undefined
2 undefined
ESModule规范中,this的值全都是undefined。
小总结
通过上面不同环境下的实验,可以看到在普通函数上下文中,this指向globalThis;而在严格模式中,this值为undefined。至于ESModule规范,它是默认开启严格模式的,因此全是undefined。
构造函数上下文
构造函数是JavaScript原型链和类的重要概念,是生成实例对象的方法,构造函数中的this,指向的就是我们要生成的实例对象的this。这里我们来执行一段代码,试验一下。
let global1 = null;
function C1() {
global1 = this;
console.log(1, this);
this.a = 1;
console.log(1, this);
}
const c1 = new C1();
console.log(1, global1, c1, global1 === c1);
let global2 = null;
class C2 {
a = 2;
constructor() {
global2 = this;
console.log(2, this);
this.b = 2;
console.log(2, this);
}
}
const c2 = new C2();
console.log(2, global2, c2, global2 === c2);
let global3 = null;
function C3() {
global3 = this;
this.a = 1;
console.log(3, this);
return {};
}
const c3 = new C3();
console.log(3, global3, c3, global3 === c3);
代码中分别尝试了三种情形,分别是传统构造函数,Class类的构造函数,以及在构造函数中返回另一个对象。来看一下输出:
1 C1 {}
1 C1 { a: 1 }
1 C1 { a: 1 } C1 { a: 1 } true
2 C2 { a: 2 }
2 C2 { a: 2, b: 2 }
2 C2 { a: 2, b: 2 } C2 { a: 2, b: 2 } true
3 C3 { a: 1 }
3 C3 { a: 1 } {} false
由于构造函数中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。可以看到,构造函数上下文中的this,确实是指向创建的实例对象,不管是传统构造函数还是Class类的构造函数。
如果构造函数返回另一个对象,那么这个返回的对象并不属于这个构造函数的实例对象;构造函数中的this也不指向返回对象,而是指向真正的实例对象。
对象或实例属性的函数上下文
函数属性
如果一个函数是某个对象或者实例的属性,那么这个函数的内部的this,指向的应该是这个对象/实例本身。这里我们来执行一段代码,试验一下。
function fun() {
console.log(this);
}
const a = {};
a.fun = fun;
a.fun();
function C1() {
this.fun = fun;
}
const b = new C1();
b.fun();
对于函数fun,我们尝试了两种情况,一种先创建对象,再作为对象属性赋值,另一种是在构造函数中作为属性赋值。我们来看一下输出:
{ fun: [Function: fun] }
C1 { fun: [Function: fun] }
由于对象或实例属性函数中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。可以看到分别输出了普通对象和C1的实例。
结合上面关于普通函数上下文的实验,可以看到其实函数本身是“不拥有自己的this的”,函数内部的this,完全看调用这个函数所在的环境和调用方式。如果是以对象属性的形式调用,this就指向对象本身;如果是直接调用,则指向globalThis或者严格模式下为undefined。
get和set
一个对象的取值函数getter和存值函数setter也都是函数,在其中也能获取到this。在这里面this指向什么呢?
const c1 = {
get g1() {
console.log("1 get", this);
return 1;
},
set g1(val) {
console.log("1 set", this);
},
};
c1.g1 = c1.g1;
const c2 = {};
Object.defineProperty(c2, "g2", {
enumerable: true, // 对象可以枚举
get() {
console.log("2 get", this);
return 1;
},
set(val) {
console.log("2 set", this);
},
});
c2.g2 = c2.g2;
这里又尝试了两种情况,一种是定义对象时直接提供getter和setter,一种是先定义对象,后面使用Object.defineProperty添加。我们看一下输出。
1 get { g1: [Getter/Setter] }
1 set { g1: [Getter/Setter] }
2 get { g2: [Getter/Setter] }
2 set { g2: [Getter/Setter] }
可以看到,分别输出了getter和setter所属的对象。由于getter和getter函数中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
原型函数属性上下文
上面介绍了实例属性的函数中,this的指向问题。那我们再看一下如果这个函数属性是挂在原型上的,或者是原型上的set和get,this指向如何。
原型函数属性
首先来看看原型上的函数属性。
class C1 {
a = 1;
fun() {
console.log(1, this);
}
}
const c1 = new C1();
c1.fun();
function C2() {
this.a = 2;
}
C2.prototype = {
fun() {
console.log(2, this);
},
};
const c2 = new C2();
c2.fun();
function C3() {
this.a = 3;
}
const c3 = new C3();
const c3proto = {
fun() {
console.log(3, this);
},
};
c3.__proto__ = c3proto;
c3.fun();
c3proto.fun();
上面代码尝试了三种情况,分别是class关键字直接创建原型;在构造函数上提供原型;以及在实例上直接赋值原型。其中最后一种我们还尝试了在原型上直接调用函数,这相当于上一节对象的函数属性上下文。看一下输出结果:
// 浏览器输出
1 C1 { a: 1 }
2 C2 { a: 2 }
3 C3 { a: 3 }
3 { fun: [Function: fun] }
// Node.js输出
1 C1 { a: 1 }
2 { a: 2 }
3 { a: 3 }
3 { fun: [Function: fun] }
// 严格模式下表现一致
可以看到,在浏览器和Node.js中的输出有区别,是情况2和3的输出不同。浏览器中明确指出了这是C2和C3的实例,但是Node.js并没有。那是不是就说明Node.js中this指向的不是实例呢?
并不是的,查看代码发现所有的a都是实例属性,而不是原型属性。因此实际上无论浏览器或者Node.js,这三种场景this指向的都是实例对象,而不是原型对象。只不过原型更改后,Node.js对于console.log的输出处理不同。
再看最后一句输出:原型对象调用fun函数,函数中的this指向的是原型对象;实例调用fun函数,函数中的this指向的是实例对象。因此函数中this的指向和函数本身无关,而是和函数的“调用形式”有关。
原型的get和set
再来看看原型上的取值函数getter和存值函数setter。仿照上一节给出了代码:
class C1 {
a = 1;
get g1() {
console.log("1 get", this);
return 1;
};
set g1(val) {
console.log("1 set", this);
};
}
const c1 = new C1();
c1.g1 = c1.g1;
function C2() {
this.a = 2;
}
C2.prototype = {
get g2() {
console.log("2 get", this);
return 1;
},
set g2(val) {
console.log("2 set", this);
},
};
const c2 = new C2();
c2.g2 = c2.g2;
function C3() {
this.a = 3;
}
const c3proto = {
get g3() {
console.log("3 get", this);
return 1;
},
set g3(val) {
console.log("3 set", this);
},
};
const c3 = new C3();
c3.__proto__ = c3proto;
c3.g3 = c3.g3;
c3proto.g3 = c3proto.g3;
来看一下各个环境的输出结果:
// 浏览器输出
1 get C1 { a: 1 }
1 set C1 { a: 1 }
2 get C2 { a: 2 }
2 set C2 { a: 2 }
3 get C3 { a: 3 }
3 set C3 { a: 3 }
3 get { g3: [Getter/Setter] }
3 set { g3: [Getter/Setter] }
// Node.js输出
1 get C1 { a: 1 }
1 set C1 { a: 1 }
2 get { a: 2 }
2 set { a: 2 }
3 get { a: 3 }
3 set { a: 3 }
3 get { g3: [Getter/Setter] }
3 set { g3: [Getter/Setter] }
// 严格模式下表现一致
可以看到,浏览器和Node.js的输出是不同的,但不同点依然是Node.js对于console.log的输出处理不同,本质上指向的还是同一个对象。
然后我们看下输出结果,发现原型上的getter和setter与在原型上的函数属性一致,其中的this都指向调用它的对象。如果是实例调用就指向实例,原型直接调用就指向原型。
类的静态方法上下文
类的静态方法与getter,setter
与实例属性或者原型属性一样,类本身也有自己的静态属性。那么在类的静态方法中,this的指向如何呢?
class C1 {
static a = 1;
static fun() {
console.log(this, this === C1);
}
static get g1() {
console.log('get', this, this === C1);
return 1;
}
static set g1(val) {
console.log('set', this, this === C1);
}
}
C1.fun();
C1.g1 = C1.g1;
我们尝试了类的静态属性方法,以及类的静态getter,setter。来看下输出结果:
[class C1] { a: 1 } true
get [class C1] { a: 1 } true
set [class C1] { a: 1 } true
可以看到,指向的都是类本身。没有指向实例(甚至这个例子都没有创建实例),也没有指向原型。注意这里没有包含传统的构造函数形式的类静态方法的例子。因为那种场景与直接对一个函数赋值一个属性没有任何区别。如果没有对这个函数使用new,甚至都看不出它是一个构造函数。因此,类的静态方法实际上就是类这个对象的方法而已。由于类的静态方法的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
类的静态块
类的静态属性中有一个特殊一点的叫做静态块,它是为了类静态属性的初始化逻辑而设置的。我们来看一下,在类的静态块中,this指向什么。
class C1 {
static a = 1;
static {
console.log(this);
}
}
// 输出
// [class C1] { a: 1 }
可以看到,类的静态块中的this指向的就是类本身。由于类的静态块中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
继承-构造函数上下文
了解实例和原型的上下文之后,我们再来了解一下与继承有关的场景中this的指向问题。首先来看一下继承中的构造函数上下文。
class C1 {
constructor() {
console.log(1, this);
}
a = 1;
fun1() {}
}
class C2 extends C1 {
constructor() {
// console.log(this) 这里会报错
super();
console.log(2, this);
}
b = 2;
fun2() {}
}
const c1 = new C1();
const c2 = new C2();
/* 输出
1 C1 { a: 1 }
1 C2 { a: 1 }
2 C2 { a: 1, b: 2 }
*/
首先我们创建了一个父类C1的实例,此时this指向父类的实例。子类C2继承了C1,在创建实例的时候,调用super(),即执行父类的构造函数。在super()执行前,不可以使用this,会报错的。
可以看到父类的构造函数输出this中类名是子类C2,而不是父类本身C1。这是由继承的机制决定的,我们创建的是子类C2的实例,而不是父类C1的。但是由于父类构造函数需要先执行,此时子类的实例属性还没挂载到实例上,因此没有b: 2
。在子类的构造函数中,此时子类的实例属性就已经被挂载了。
由于继承构造函数中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
继承-实例函数属性上下文
再来看看继承中的函数属性上下文,首先来看下继承中的实例属性上下文。我们构造下例子:
function fun() {
console.log(this);
}
class C1 {
constructor() {
this.fun1 = fun;
}
a = 1;
}
class C2 extends C1 {
constructor() {
super();
this.fun2 = fun;
}
b = 2;
}
const c1 = new C1();
c1.fun1();
const c2 = new C2();
c2.fun1();
c2.fun2();
/* 输出
C1 { a: 1, fun1: [Function: fun] }
C2 { a: 1, fun1: [Function: fun], b: 2, fun2: [Function: fun] }
C2 { a: 1, fun1: [Function: fun], b: 2, fun2: [Function: fun] }
*/
首先我们创建了类C1的实例,没有用到继承,输出也是类C1的实例。然后我们创建了类C2的实例,继承C1,调用C1中的实例属性,发现此时this是C2的实例,与类C2自己绑定的fun2函数输出一致。至于函数中的super.xx用法指向的是原型,并不是实例属性,因此这个场景无法使用。
由于继承实例函数属性上下文中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
继承-原型函数属性上下文
看完了继承的实例函数属性,再看下继承的原型函数属性上下文。
函数属性
首先看一下普通的函数属性,我们构造下例子:
class C1 {
a = 1;
fun1() {
console.log(1, this);
}
}
class C2 extends C1 {
b = 2;
fun2() {
console.log(2, this);
}
fun3() {
super.fun1();
}
}
const c1 = new C1();
c1.fun1();
const c2 = new C2();
c2.fun1();
c2.fun2();
c2.fun3();
/* 输出
1 C1 { a: 1 }
1 C2 { a: 1, b: 2 }
2 C2 { a: 1, b: 2 }
1 C2 { a: 1, b: 2 }
*/
可以看到与实例属性一致,在类C2的实例上调用的方法,其中继承的this指向的都是类C2的实例。注意最后一个输出,我们是使用super.xxx调用父类的原型方法,结果也是一致的。
由于继承原型函数属性上下文中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
get和set
再来看看继承原型上的取值函数getter和存值函数setter。我们构造下例子:
class C1 {
a = 1;
get g1() {
console.log("1 get", this);
return 1;
}
set g1(val) {
console.log("1 set", this);
}
}
class C2 extends C1 {
b = 2;
get g2() {
console.log("2 get", this);
return 1;
}
set g2(val) {
console.log("2 set", this);
}
fun() {
super.g1 = super.g1;
}
}
const c1 = new C1();
c1.g1 = c1.g1;
const c2 = new C2();
c2.g1 = c2.g1;
c2.g2 = c2.g2;
c2.fun();
/* 输出
1 get C1 { a: 1 }
1 set C1 { a: 1 }
1 get C2 { a: 1, b: 2 }
1 set C2 { a: 1, b: 2 }
2 get C2 { a: 1, b: 2 }
2 set C2 { a: 1, b: 2 }
1 get C2 { a: 1, b: 2 }
1 set C2 { a: 1, b: 2 }
*/
与普通函数属性一致,我们在在类C2的实例上使用getter和setter,其中继承的this指向也是类C2的实例。包括我们使用super直接调用类C1原型上的方法。由于继承原型函数属性上下文中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
继承-类的静态方法上下文
看完了继承的实例属性和原型属性,再来看看它的静态方法上下文中,this的指向。
外部和静态方法中调用
class C1 {
static a = 1;
static fun1() {
console.log(1, this);
}
}
class C2 extends C1 {
static b = 1;
static fun2() {
console.log(2, this);
}
static fun3() {
super.fun1();
}
}
C1.fun1();
C2.fun1();
C2.fun2();
C2.fun3();
/* 输出
1 [class C1] { a: 1 }
1 [class C2 extends C1] { b: 1 }
2 [class C2 extends C1] { b: 1 }
1 [class C2 extends C1] { b: 1 }
*/
可以看到在子类中调用父类的静态方法,其中的this指向的是子类。这与在子类实例中调用父类(实例或原型)方法是一样的逻辑。最后我们尝试了以super的形式调用父类方法,this指向的也是子类。由于继承类的静态方法上下文中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
静态块中调用
在子类中调用父类方法还有一个场景,就是静态块。我们举例试一下:
class C1 {
static a = 1;
static fun() {
console.log('1 fun', this);
}
static {
console.log(1, this);
}
}
class C2 extends C1 {
static b = 1;
static {
console.log(2, this);
this.fun();
super.fun();
}
}
/* 输出
1 [class C1] { a: 1 }
2 [class C2 extends C1] { b: 1 }
1 fun [class C2 extends C1] { b: 1 }
1 fun [class C2 extends C1] { b: 1 }
*/
这里在静态块中尝试了使用this和super调用父类的静态方法,结果其中的this指向的都是子类。由于继承类的静态方法在静态块中调用上下文中的this是JavaScript语法规定的特性,因此不同的环境和是否严格模式表现都是一致的。
类的字段初始化器
字段初始化器指的是直接把this作为字段的值。我们分别看一下静态字段和实例字段的情况,以及在类继承中的情况。
实例和静态字段初始化器
虽然代码看起来“实例和静态字段初始化器在同一个作用域下”,但实际上并不是。
class C1 {
static a = this;
c = this;
}
console.log(C1.a);
const c1 = new C1();
console.log(c1.c);
/* 输出
<ref *1> [class C1] { a: [Circular *1] }
<ref *1> C1 { c: [Circular *1] }
*/
静态字段初始化器的this指向类本身,实例字段初始化器的this指向实例本身。
初始化器继承
class C1 {
static a1 = this;
c1 = this;
}
class C2 extends C1 {
static a2 = this;
c2 = this;
}
console.log(C2.a1);
const c2 = new C2();
console.log(c2.c1);
/* 输出
<ref *1> [class C1] { a1: [Circular *1] }
<ref *1> C2 { c1: [Circular *1], c2: [Circular *1] }
*/
在输出中可以看到,实例字段初始化器中的this指向的是继承后的实例(也只有这一个实例)。但是静态字段初始化器指向的确实父类,并不是子类。这是因为静态字段初始化器在创建类的时候就创建好了,继承只是继承了这个字段,并不会修改它的值。而实例字段初始化器是在创建实例的时候赋值的,因此可以指向继承后的实例。
call方法
上面我们介绍了很多函数场景下,this的指向问题,其中无论是普通对象,类还是原型,修改函数中this指向的方式实际上都是obj.fun()。那么有没有方法,不需要将函数附加到对象上即可绑定this?下面要介绍的三个方法,call,bind和aplly,都可以做到。首先介绍call方法。
使用 call()可以在调用函数时让this指向指定的值。我们看一下例子:
function fun(val) {
console.log(val, this);
}
const obj = { a: 1 };
fun.call(obj, 2);
/* 输出
2 { a: 1 }
*/
可以看到,使用call方法的第一个入参为要指向的this,后面的入参为函数本身原有的入参。call方法可以做到不修改对象绑定this,同时执行函数。
如果call方法的第一个入参为null和undefined:在非严格模式下,此时它的this指向和不使用call方法一致,即为globalThis,可以参考上面普通函数场景下的输出。如果为严格模式,那么还是指向call方法的第一个入参。
function fun() {
console.log(this);
}
fun.call();
fun.call(undefined);
fun.call(null);
/* 输出
// 非严格模式 Node.js
<ref *1> Object [global] { ...省略 }
<ref *1> Object [global] { ...省略 }
<ref *1> Object [global] { ...省略 }
// 非严格模式 浏览器
Window {window: Window, self: Window, document: document, ...省略 }
Window {window: Window, self: Window, document: document, ...省略 }
Window {window: Window, self: Window, document: document, ...省略 }
// 严格模式
undefined
undefined
null
*/
apply方法
apply方法与上面介绍的call方法非常类似,也是不需要将函数附加到对象上即可绑定this,执行函数。他俩的区别在于函数传参方式不同,apply是用数组的形式传参。我们看一下示例:
function fun(a, b) {
console.log(a, b, this);
}
const obj = { a: 1 };
fun.call(obj, 1, 2);
fun.apply(obj, [1, 2]);
/* 输出
1 2 { a: 1 }
1 2 { a: 1 }
*/
可以看到,call方法会将除了第一个参数外的所有参数都传给原函数,而apply的第二个方法是一个数组,数组的内容就是第二个函数的参数列表。
如果apply方法的第一个入参为null和undefined,它的表现与call方法一致:在非严格模式下,此时它的this指向和不使用call方法一致,即为globalThis,可以参考上面普通函数场景下的输出。如果为严格模式,那么还是指向call方法的第一个入参。
function fun() {
console.log(this);
}
fun.apply();
fun.apply(undefined);
fun.apply(null);
/* 输出
// 非严格模式 Node.js
<ref *1> Object [global] { ...省略 }
<ref *1> Object [global] { ...省略 }
<ref *1> Object [global] { ...省略 }
// 非严格模式 浏览器
Window {window: Window, self: Window, document: document, ...省略 }
Window {window: Window, self: Window, document: document, ...省略 }
Window {window: Window, self: Window, document: document, ...省略 }
// 严格模式
undefined
undefined
null
*/
bind方法
call和apply方法虽然可以绑定this,但都是立即执行该函数。那么有没有方法可以绑定this,但是并不会立即执行该函数呢?这就是bind方法的效果。
bind方法说明
我们来举个例子看一下:
// 示例1
function fun1() {
console.log(this);
}
const obj = { a: 1 };
const funa = fun1.bind(obj);
funa();
// 示例2
function fun2(a, b, c, d) {
console.log(this, a, b, c, d);
}
const funb = fun2.bind(obj, 1,2);
funb(3,4);
const func = funb.bind(null, 5);
func(6);
/* 输出
{ a: 1 }
{ a: 1 } 1 2 3 4
{ a: 1 } 1 2 5 6
*/
示例1可以看到,使用bind方法绑定了obj作为函数的this,但是并没有直接执行,而是返回了新函数,可以延迟执行。而且比call和apply方法更高级的是,bind方法可以暂存入参,且可以多次调用,多次暂存。看示例2,funb绑定了this,且传了两个参数;在执行新函数的时候前两个就不需要传了,相当于起到暂存参数的作用。而且可以重复调用bind方法,多次暂存参数,例如func就二次调用了bind方法。不过多次调用时,绑定this就无效了,以第一次为准。
bind方法特殊场景
首先是常见的第一个入参为null和undefined,它的表现与call,apply方法一致:在非严格模式下,此时它的this指向和不使用call方法一致,即为globalThis,可以参考上面普通函数场景下的输出。如果为严格模式,那么还是指向bind方法的第一个入参。
function fun() {
console.log(this);
}
fun.bind()();
fun.bind(undefined)();
fun.bind(null)();
/* 输出
// 非严格模式 Node.js
<ref *1> Object [global] { ...省略 }
<ref *1> Object [global] { ...省略 }
<ref *1> Object [global] { ...省略 }
// 非严格模式 浏览器
Window {window: Window, self: Window, document: document, ...省略 }
Window {window: Window, self: Window, document: document, ...省略 }
Window {window: Window, self: Window, document: document, ...省略 }
// 严格模式
undefined
undefined
null
*/
然后是bind方法创建的函数作为构造函数,此时我们绑定的this是无效的:
function Fun1() {
console.log(this, new.target === Fun1);
}
const obj = { a: 1 };
const fun1 = Fun1.bind(obj);
console.log(new fun1());
function Fun2(a, b, c, d) {
console.log(a, b, c, d);
}
const fun2 = Fun2.bind(obj, 1, 2);
new fun2(3, 4);
/* 输出
Fun1 {} true
Fun1 {}
1 2 3 4
*/
可以看到,Fun1在作为构造函数使用时,绑定的obj是无效的,this此时还是构造函数生成的实例。虽然此时绑定this无效,但是暂存参数的功能还是有效的。例如fun2就暂存了Fun2的两个参数,然后作为构造函数使用时,成功读入了暂存的参数。
bind方法创建的函数虽然能作为构造函数,但不能作为父类被其他子类继承。bind方法可以绑定类,此时静态方法会失效,但继承的静态方法依旧生效。bind还有一些其它特性,不过并不是this的新情形,因此这里就不多介绍了。
原始值原型函数属性上下文
在JavaScript中,原始值(原始数据类型)是一种既非对象也无方法或属性的数据。所有原始值都是不可变的,即它们的值不能被修改。但是当在原始值上访问属性时,JavaScript自动将值装入包装对象中,并访问该对象上的属性。这里我们尝试执行原始值的原型函数属性,看看其中this的指向。
function fun() {
console.log(this);
}
Number.prototype.fun = fun;
(1).fun();
console.log(new Number(1), 1);
String.prototype.fun = fun;
('a').fun();
console.log(new String('a'), 'a');
Boolean.prototype.fun = fun;
(false).fun();
console.log(new Boolean(false), false);
/* 输出
// 非严格模式
[Number: 1]
[Number: 1] 1
[String: 'a']
[String: 'a'] a
[Boolean: false]
[Boolean: false] false
// 严格模式
1
[Number: 1] 1
a
[String: 'a'] a
false
[Boolean: false] false
*/
可以看到,我们先在原始值对应的原型上增加了一个函数属性,然后再在原始值上调用。结果在严格模式和非严格模式是不同的。严格模式下,this指向原始值;非严格模式下,this指向包装对象。
回调函数上下文
当一个函数作为回调函数传递时,this的值取决于如何调用回调。
自定义回调场景
如果是我们自己写的调用回调代码,那么this的值就由我们的调用方式决定。这里举例看下:
let globThis = null;
function funStore() {
globThis = this;
}
funStore();
function fun() {
console.log(globThis === this);
}
fun();
function call1(call) {
call();
}
call1(fun);
const obj = {
call2: function (call) {
call();
}
};
obj.call2(fun);
/* 输出
true
true
true
*/
首先我们执行了一个普通函数,记录了this,然后在两种回调中尝试this值与普通函数直接执行的区别。输出发现没有任何区别,this的指向实际是一样的,可以参考上面普通函数场景下的输出,且不同的环境和是否严格模式表现都普通函数场景下一致。
JavaScript提供的回调场景
JavaScript本身提供了很多回调函数的调用场景,比如迭代数组方法。其中大部分回调的this指向都与普通函数执行时一致,而且也可以传入可选的this值。我们看一下例子:
let globThis = null;
function funStore() {
globThis = this;
}
funStore();
function fun() {
console.log(globThis === this);
}
fun();
[1].forEach(fun);
[1].map(fun);
function fun2() {
console.log(this);
}
const obj = {a:1};
[1].forEach(fun2, obj);
[1].map(fun2, obj);
/* 输出
true
true
true
{ a: 1 }
{ a: 1 }
*/
这里我们举例了数组的两个原型方法:forEach和map。在执行回调函数时,this的值与普通函数直接执行没有区别。但是这些接受回调的方法允许我们多传一个参数作为this指向,传了之后,回调中的this值就是我们指定的了。
部分特殊情形
还有一些特殊的可以接收回调的函数,此时回调函数中this指向是有意义的。我们举例看下:
function fun() {
console.log(this);
}
JSON.parse("true", fun);
JSON.stringify({ a: 1 }, fun);
/* 输出
{ '': true }
{ '': { a: 1 } }
*/
这里尝试了JSON.parse和JSON.stringify。可以看到,虽然我们没有传额外的参数,但是回调函数中的this却与普通函数直接执行是有区别的。这里的this指向的是当前解析的对象,具体可以查看相关文档了解。
箭头函数
箭头函数是一种简洁的函数表达式,且除了简洁之外,与普通函数相比在用法上有一些差异,比如这里要说的this。普通函数中this的指向是看调用函数的对象,但箭头函数的this指向是创建箭头函数时,作用域中this的指向。
箭头函数的this指向,在不同的情形下与普通函数的效果有较大区别。
不同的环境和模块化规范
首先我们试一下,在不同的环境和模块化规范下,直接执行函数。
function fun1() {
console.log(1, this);
}
const fun2 = () => {
console.log(2, this);
}
console.log(0, this);
fun1();
fun2();
/* 输出
// 浏览器命令行 非严格模式
0 Window {window: Window, self: Window, document: document, ...省略 }
1 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
// 浏览器命令行 严格模式
0 Window {window: Window, self: Window, document: document, ...省略 }
1 undefined
2 Window {window: Window, self: Window, document: document, ...省略 }
// 浏览器HTML中 非严格模式
0 Window {window: Window, self: Window, document: document, ...省略 }
1 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
// 浏览器HTML中 严格模式
0 Window {window: Window, self: Window, document: document, ...省略 }
1 undefined
2 Window {window: Window, self: Window, document: document, ...省略 }
// Node.js命令行 非严格模式
0 <ref *1> Object [global] { ...省略 }
1 <ref *1> Object [global] { ...省略 }
2 <ref *1> Object [global] { ...省略 }
// Node.js命令行 严格模式
0 <ref *1> Object [global] { ...省略 }
1 undefined
2 <ref *1> Object [global] { ...省略 }
// CommonJS和Node.js 非严格模式
0 {}
1 <ref *1> Object [global] { ...省略 }
2 {}
// CommonJS和Node.js 严格模式
0 {}
1 undefined
2 {}
// ESModule和浏览器
0 undefined
1 undefined
2 undefined
// ESModule和Node.js
0 undefined
1 undefined
2 undefined
*/
我们尝试了六种场景,以及对应的严格模式和非严格模式。可以看到不同的场景下,箭头函数的指向的this值是有区别的,而且很多场景下普通函数和箭头函数的this指向也不一样。
但是可以看到,箭头函数中this的指向和箭头函数外,直接输出this的指向是一样的。因此证明了箭头函数中的this指向,即是作用域中this的指向。
外部的箭头函数在对象和类中作为属性
如果一个在对象和类外面的箭头函数,被作为对象属性,类的静态属性或者实例属性来执行,其中的this指向又是如何呢?我们来看一下例子:
function fun1() {
console.log(1, this);
}
const outerThis = this;
const fun2 = () => {
console.log(this === outerThis);
};
const obj = {
fun1: fun1,
fun2: fun2,
};
obj.fun1();
obj.fun2();
class C1 {
static sf1 = fun1;
static sf2 = fun2;
f1 = fun1;
f2 = fun2;
}
C1.sf1();
C1.sf2();
const c1 = new C1();
c1.f1();
c1.f2();
/* 输出
1 { fun1: [Function: fun1], fun2: [Function: fun2] }
true
1 [class C1] { sf1: [Function: fun1], sf2: [Function: fun2] }
true
1 C1 { f1: [Function: fun1], f2: [Function: fun2] }
true
*/
在这里我们同时对比了普通函数和箭头函数的情况。可以看到普通函数在对象属性,类的静态属性或者实例属性执行时,this指向的值就是“拥有这个属性的对象”,我们在上面的章节讨论过。但是箭头函数的this却始终与外面直接输出的this指向是一致的,不管箭头函数被哪个类或者对象拥有。这也说明了箭头函数的this指向是创建箭头函数时,作用域中this的指向。
为什么这里要输出this === outerThis
而不是直接输出this的值呢?因为不同的环境和模块化规范,以及是否严格模式之下,这里的this值的情况太多了,这里重复输出太罗嗦了,因此统一用这个表达式代替。
类或对象内部的箭头函数
我们再来看一下,如果箭头函数定义在对象或者类的内部,此时箭头函数指向什么。
const outerThis = this;
const obj = {
fun1() {
console.log(this);
},
fun2: () => {
console.log(this === outerThis);
},
};
obj.fun1();
obj.fun2();
class C1 {
static staticThis = this;
static sf1 = function () {
console.log('sf1', this);
};
static sf2 = () => {
console.log('sf2', this);
};
cthis = this;
f1 = function () {
console.log('f1', this);
};
f2 = () => {
console.log('f2', this);
};
}
console.log('staticThis', C1.staticThis);
C1.sf1();
C1.sf2();
const c1 = new C1();
console.log('cthis', c1.cthis);
c1.f1();
c1.f2();
/* 输出
{ fun1: [Function: fun1], fun2: [Function: fun2] }
true
staticThis [class C1] { staticThis: [Circular *1], sf1: [Function: sf1], sf2: [Function: sf2] }
sf1 [class C1] { staticThis: [Circular *1], sf1: [Function: sf1], sf2: [Function: sf2] }
sf2 [class C1] { staticThis: [Circular *1], sf1: [Function: sf1], sf2: [Function: sf2] }
cthis C1 { cthis: [Circular *1], f1: [Function: f1], f2: [Function: f2] }
f1 C1 { cthis: [Circular *1], f1: [Function: f1], f2: [Function: f2] }
f2 C1 { cthis: [Circular *1], f1: [Function: f1], f2: [Function: f2] }
*/
首先我们看一下对象属性,虽然箭头函数是在对象内部定义的,但this的指向还是与外部的this指向一致,因此可以认为对象没有形成自己的作用域。
再来看看类。类这里分为静态属性和实例属性,这里的作用域实际上就是字段初始化器的作用域。静态字段初始化器中this指向类C1,因此作用域中的this指向就是类C1,这也是静态字段的箭头函数的指向。实例属性是创建实例时被初始化的,初始化时的作用域与构造函数中一致,即是对象实例。因此这里箭头函数中this指向的也是实例。
类继承
我们再来看一下继承情形下,箭头函数中this的指向。
class C1 {
static staticThis = this;
static sf1 = function () {
console.log("sf1", this);
};
static sf2 = () => {
console.log("sf2", this);
};
cthis = this;
f1 = function () {
console.log("f1", this);
};
f2 = () => {
console.log("f2", this);
};
}
class C2 extends C1 {
a = 1;
static b = 2;
}
console.log("staticThis", C2.staticThis);
C2.sf1();
C2.sf2();
const c2 = new C2();
console.log("cthis", c2.cthis);
c2.f1();
c2.f2();
/* 输出
staticThis [class C1] { staticThis: [Circular *1], sf1: [Function: sf1], sf2: [Function: sf2] }
sf1 [class C2 extends C1] { b: 2 }
sf2 [class C1] { staticThis: [Circular *1], sf1: [Function: sf1], sf2: [Function: sf2] }
cthis C1 { cthis: [Circular *1], f1: [Function: f1], f2: [Function: f2] }
f1 C1 { cthis: [Circular *1], f1: [Function: f1], f2: [Function: f2] }
f2 C1 { cthis: [Circular *1], f1: [Function: f1], f2: [Function: f2] }
*/
可以看到,这一节的例子实际上就是上一节类实例的例子增加了继承。首先看下静态字段初始化器,子类中输出是父类。可以认为父类中静态属性的作用域是父类,箭头函数中this的指向也是如此。但在普通函数的情形下,是子类调用的函数,因此普通函数中this的指向还是子类,符合普通函数this指向调用方的规则。
再看实例属性的继承。可以看到输出中不管是实例字段初始化器,普通函数和箭头函数,this指向的都是子类。因为在创建实例属性时,只有一个实例存在,这个实例就是子类的实例,因此这里的指向是一致的。
作用域中this变化的情形
假设箭头函数所处的作用域中的this在不同情形是变化的,那么箭头函数中this的指向会不会跟着变化呢?我们看一个例子。
function fun1() {
console.log(this);
const fun2 = () => {
console.log(this);
};
fun2();
}
// fun1();
const obj = {
fun: fun1,
};
obj.fun();
class C1 {
static fun1 = fun1;
fun = fun1;
}
C1.fun1();
const c1 = new C1();
c1.fun();
/* 输出
{ fun: [Function: fun1] }
{ fun: [Function: fun1] }
[class C1] { fun1: [Function: fun1] }
[class C1] { fun1: [Function: fun1] }
C1 { fun: [Function: fun1] }
C1 { fun: [Function: fun1] }
*/
可以看到,我们的箭头函数是定义在一个普通函数内部的,而这个普通函数我们使用了各种方式执行,因此函数内部的this指向各不相同。而且每次是新创建的箭头函数。箭头函数来说,作用域中的this一直在变化,因此内部this指向也一直在变化。
第一个在最外层执行的场景我们注释了,因为输出随着不同的场景变化,前面已经介绍过可能的值,因此这里省略了。
eval函数中的this
eval函数会将传入的字符串当做JavaScript代码执行。如果传入的“代码”中包含this,那么这时的this指向如何呢?eval函数有两种模式,一种是直接调用,一种是间接调用。两者的指向并不一致。
直接调用
在直接调用时,eval内部的作用域上下文就是eval执行时所处的上下文。
eval("console.log(1, this)");
const obj = {
fun() {
eval("console.log(2, this)");
}
}
obj.fun();
eval(`
class C1 { static a = this; b = this; }
const c = new C1();
console.log(3, C1.a);
console.log(4, c.b);
`);
/* 输出
// 第一行会随着外部作用域this的指向而变化,这里列举几种,不全列举了
1 {} // Node.js + Commonjs 非严格模式
1 undefined // Node.js + ESModule
1 <ref *1> Object [global] { ...省略 } // Node.js + 命令行 非严格模式
2 { fun: [Function: fun] }
3 <ref *1> [class C1] { a: [Circular *1] }
4 <ref *1> C1 { b: [Circular *1] }
*/
首先我们尝试了在最外层中调用eval输出this,发现确实会随着外部作用域this的指向而变化。然后尝试了在对象方法中执行eval,此时this指向为调用的对象。然后我们尝试了在eval内部创建作用域,输出内部的this,发现指向的规则与在外面一致。
间接调用
间接调用时,eval内部的作用域却是全局作用域,此时this的指向为globalThis。
const e = eval;
e("console.log(1, this)");
const obj = {
fun() {
const ef = eval;
ef("console.log(2, this)");
},
};
obj.fun();
/* 输出
// Node.js
1 <ref *1> Object [global] { ...省略 }
2 <ref *1> Object [global] { ...省略 }
// 浏览器
1 Window {window: Window, self: Window, document: document, ...省略 }
2 Window {window: Window, self: Window, document: document, ...省略 }
*/
我们把eval赋值给一个变量,然后间接调用它,发现无论eval执行时所处的作用域是什么,this的指向都是globalThis,即使是严格模式也不会变化。
事件处理器
JavaScript中的事件处理器可以绑定事件处理函数,这个处理函数是个回调函数,那么其中this的指向如何呢?
<html>
<div onclick="console.log(1, this)" onwheel="console.log(2, this)">点击1</div>
<div id="click2">点击2</div>
<body>
<script>
const click2 = document.getElementById("click2");
click2.addEventListener("click", function () {
console.log(3, this);
});
click2.addEventListener("click", () => {
console.log(4, this);
});
</script>
</body>
</html>
/* 输出
1 <div onclick="console.log(1, this)" onwheel="console.log(2, this)">点击1</div>
2 <div onclick="console.log(1, this)" onwheel="console.log(2, this)">点击1</div>
3 <div id="click2">点击2</div>
// 不同环境输出不一致
4 Window {window: Window, self: Window, document: document, ...省略 } // 浏览器 非ESmodule
4 undefined // 浏览器 + ESmodule
*/
实际上,如果事件处理函数是一个普通函数,那么其中的this指向的是引发事件的对象。如果是一个箭头函数,那么this指向符合箭头函数的自己的规则,即外部作用域中this的指向。
严格模式总结
严格模式是JavaScript的一种模式,相对于非严格模式限制了部分语法和使用方式。严格模式与非严格模式对于this的处理是有区别的,我们上面描述不同场景下this的指向时,已经碰到过一些了。
严格模式的条件
这里先整理下使用严格模式的条件:
- 在ESModule中是自动使用严格模式的。
- 文件开头设置
"use strict";
,整个文件内会使用严格模式。 - 函数内开头设置
"use strict";
,整个函数内会使用严格模式。 - class中是自动使用严格模式的。
严格模式与非严格模式this的区别
事实上在非严格模式下,传递给函数的this会被强制转换为一个对象。例如在普通函数上下文中,因为this没有指向(undefined)所以将this替换成了globalThis。或者原始值作为this时,会被替换为这个值的包装对象。我们看一下具体的例子(这些例子都是前面讲过的):
function fun() {
console.log(this);
}
fun.call();
fun.call(undefined);
fun.call(null);
fun.apply();
fun.apply(undefined);
fun.apply(null);
fun.bind()();
fun.bind(undefined)();
fun.bind(null)();
Number.prototype.fun = fun;
(1).fun();
String.prototype.fun = fun;
('a').fun();
Boolean.prototype.fun = fun;
(false).fun();
上面列举了普通函数,以及使用call, apply, bind执行函数,同时指定this是undefined或null的场景。在非严格模式下,this都指向globalThis。而严格模式下,输出则变成了this原始指向的值。另外还有原始值作为this访问属性方法的场景。在非严格模式下this指向值的包装对象;严格模式下this指向值本身。我们看下输出:
// 非严格模式输出
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
globalThis // 具体值由环境确定
[Number: 1]
[String: 'a']
[Boolean: false]
// 严格模式输出
undefined
undefined
undefined
null
undefined
undefined
null
undefined
undefined
null
1
a
false
严格模式内代码在外部调用
严格模式除了整个文件生效之外,也可以只在class中生效,或者只在某个函数内生效。那么严格模式外执行严格模式之内的代码,是否也具有严格模式的性质么? 我们来试一下。
function fun() {
console.log(0, this);
}
fun();
class C1 {
fun1() {
console.log(1, this);
}
fun2() {
return function () {
console.log(2, this);
};
}
}
const c1 = new C1();
const { fun1 } = c1;
fun1();
const fun2 = c1.fun2();
fun2();
let fun3;
function funStrict() {
"use strict";
fun3 = function () {
console.log(3, this);
};
}
funStrict();
fun3();
/* 输出
0 globalThis // 具体值由环境确定
1 undefined
2 undefined
3 undefined
*/
首先我们在外面输出了一个普通函数内部的this,结果是globalThis。然后我们将类和严格模式函数中的代码(实际为嵌套的函数)放到严格模式外执行,发现输出的this都是undefined,这说明严格模式依然是生效的。
特殊或组合场景讨论
这里我们讨论下部分容易搞错的特殊场景,又或者上面介绍的一些场景的组合情形下,this指向的值。
对象字面量不创建作用域
对象字面量不创建作用域,因此更不会更改外部的this。
const obj = {
a: this,
}
这里的this和对象字面量外部作用域中的this实际是一致的。
箭头函数使用call/bind/apply方法
上面描述过,箭头函数的this指向是创建箭头函数时,外部作用域中this的指向。虽然call/bind/apply方法可以改变函数内的this指向,但无法改变箭头函数内的this指向。我们看一下例子:
const obj = {a:1};
const fun1 = function () {
console.log(1, this);
};
const fun2 = () => {
console.log(2, this);
}
fun1();
fun2();
fun1.call(obj);
fun2.call(obj);
fun1.apply(obj);
fun2.apply(obj);
fun1.bind(obj)();
fun2.bind(obj)();
/* 输出 以 Node.js + CommonJs + 非严格模式为例
1 <ref *1> Object [global] { ...省略 }
2 {}
1 { a: 1 }
2 {}
1 { a: 1 }
2 {}
1 { a: 1 }
2 {}
*/
这里以 Node.js + CommonJs + 非严格模式 为例,此时外部作用域中的this指向为{}。可以看到普通函数的this指向受到了call/bind/apply方法的影响,但是箭头函数始终指向外部作用域中的this。因此箭头函数的this指向不会受到call/bind/apply方法的影响。
嵌套普通函数中的this并不会继承
嵌套普通函数中的this指向并不会继承外部作用域。我们看下例子:
const obj = {
f1: function () {
console.log(1, this);
function f2() {
console.log(2, this);
}
f2();
},
};
obj.f1();
class C1 {
static f3() {
console.log(3, this);
function f4() {
console.log(4, this);
}
f4();
}
}
C1.f3();
/* 输出 以 Node.js + CommonJs + 非严格模式为例
1 { f1: [Function: f1] }
2 <ref *1> Object [global] { ...省略 }
3 [class C1]
4 undefined
*/
我们尝试了普通对象方法和类的静态方法两种作用域,再其中创建了普通函数并执行,发现指向的this和当前作用域都无关,其中普通对象方法this指向了globalThis,class因为自动使用严格模式,所以输出undefined。
总结
在整理和撰写这篇文章之前,我一直认为this是简单的几条规则就可以概括的内容,但写完之后我发现,this的规则比想象的要复杂一些。并不是说难,而是情况很多。包含类与对象,箭头函数,JS运行环境,模块化,构造函数,继承,回调,箭头函数,call/bind/apply,eval,严格模式等很多主题。可以说贯穿了半个JavaScript语法的领域。
阮一峰写过一篇JavaScript的this原理里面介绍了为什么JavaScript语言有this的设计,实际上这和内存中存储的数据结构有关系。但是JavaScript中的this搞出了如此多的场景和情形,实际上并不好用,也不容易理解。(只是容易面试出题了)
参考
- MDN globalThis
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis - Javascript 中 let 声明的全局变量不在 window 上
https://juejin.cn/post/7064043813534171149 - MDN 严格模式
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode - MDN this
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this - JS 中在严格模式下 this 的指向 超详细
https://www.cnblogs.com/cyy22321-blog/p/16672057.html - 「万字进阶」深入浅出 Commonjs 和 Es Module
https://juejin.cn/post/6994224541312483336 - 《ECMAScript6入门教程》Class 的基本语法
https://es6.ruanyifeng.com/#docs/class - 《ECMAScript6入门教程》Class 的继承
https://es6.ruanyifeng.com/#docs/class-extends - MDN Function.prototype.call()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call - MDN Function.prototype.bind()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind - MDN Function.prototype.apply()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply - MDN JSON.parse()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse - MDN JSON.stringify()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify - MDN 原始值
https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive - MDN 箭头函数表达式
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions - MDN eval()
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval - JavaScript的this原理 阮一峰
https://www.ruanyifeng.com/blog/2018/06/javascript-this.html