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的,但需要手动开启,方式主要有两种:

  1. 文件后缀名为.mjs。
  2. 所在项目的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 = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值