说明
下面这些是根据视频教程加上个人理解写的笔记,仅代表个人理解!
水平不足,如有错误请批评指正!
如需转载,请指明源链接。无需联系我!
-
PDF版本下载链接-防止md文档图片失效
- 图片说明:图片均上传到自建七牛云图传,说不好哪天空间或者域名失效,所以保留PDF版本
-
md版本:md版本图片引用七牛云外链 下载链接
-
课程目录
ES6-ES11新特性
基础概念
-
ECMAScript
ECMA (European Computer Manufacturers Association)中文名称为欧洲计算机制
造商协,这个组织的目标是评估、开发和认可电信和计算机标准。1994 年后该组织改名为 Ecma 国际。 -
版本迭代主要内容
-
兼容性检查:http://kangax.github.io/compat-table/es6/
ES6新特性
let相关
不能重复声明
// 不能重复声明,var可以
let a = 'a'
let a = 'A'
// Uncaught SyntaxError: Identifier 'star' has already been declared
块级作用域
es5中的作用域:全局、函数、eval(严格模式下才会有,虽然我不懂严格模式是啥子)
let声明的是块级作用域,块外不能访问
{
let a = '333'
console.log(a, '1')
}
console.log(a, '2') // Uncaught ReferenceError: a is not defined
不存在变量提升
变量提升:https://www.jianshu.com/p/24973b9db51a
console.log(a); //undefined,不会报错 var a = 123;
console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 'aaa'
不影响作用域链
小案例-for循环使用let
let items = 'items是三个div的dom'
// 遍历并绑定事件
for(let/var i=0;i<items.length;i++){
items[i].onclick = functionn(){
this.style.background = 'pink'
items[i].style.background = 'pink' // var下回报错(报错原因i=3超出边界)
}
}
- 原理说明–参考链接:https://www.cnblogs.com/echolun/p/10584703.html
//使用var声明,得到3个3
var a = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); //3
a[1](); //3
a[2](); //3
//使用let声明,得到0,1,2
var a = [];
for (let i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); //0
a[1](); //1
a[2](); //2
const–声明常量
-
一定要赋初始值
-
常量的值不能修改(引用地址不能修改)
-
块级作用域
-
对于数组和对象的元素修改,不算对常量的修改(引用地址没有修改)
- 常使用const声明数组、对象
const a = 1 a = 2 // Uncaught TypeError: Assignment to constant variable const a = ['1','2','3'] a.push('4') // 虽然对改变了数组a,但是a的地址并没有改变,所以没有问题
参考链接:https://www.cnblogs.com/minigrasshopper/p/9144223.html,MDN说明
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,
const
只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
数组解构和对象解构
- 解构赋值:ES6允许按照一定模式从数组和对象中提取值,对变量进行赋值
// 数组的解构赋值
const aaaa = ['一','二','三','四']
let [a,b,c,d] = aaaa // a='一',b='二'……
// 对象的解构赋值
const aa = {name:'yishen',age:18,school:'qinghua'}
let {name} = aa // yishen
let {age} = aa // 18
let {school} = aa // qinghua
模板字符串 ``
- 内容中可以直接出现换行符号、‘单引号’、"双引号"不行
- 变量拼接
let love = '爱'
let str = `${love}你哦!`
console.log(str) // 爱你哦!
简化对象写法
ES6允许在大括号里面,直接写入变量和函数,作为对象的属性和方法,书写更简洁
let name = 'yishen'
let change = function(){
console.log('yishen')
}
const school = {
name:name,
change:chage,
improve:function(){
console.log('提升')
}
}
// 等价于
const school = {
name,
change,
improve(){
console.log('提升')
}
}
箭头函数 =>
let fn = function(a,b){
return a+b
}
let fn = (a,b) => { // 当参数有且只有一个参数,小括号可以省略
return a+b
}
参考链接:MDN-箭头函数,this指向问题,this指向问题
-
箭头函数和普通函数区别
-
this是静态的(this始终指向函数声明时所在作用域下的this值)
-
function getName(){ console.log(this.name) } // 函数体内部只有一句,可以省略函数的花括号 // 如果是return ,return必须省略,语句的执行结果就是函数的返回值 let getName2 = () => console.log(this.name) // 设置window对象 window.name = 'yishen' const school = { name:'qinghua' } // 直接调用 getName() // yishen getName2() // yishen // call 方法调用 call方法可以改变this的指向 getName.call(school) // qinghua,this指向了school对象 getName2.call(school) // yishen ,this执行依然是window(函数声明时所在作用域下的this)
-
不能作为构造实例化对象
-
Uncaught TypeError:Person is not a constructor
-
不能使用 arguments 变量
-
箭头函数适用范围
- 箭头函数适合与this无关的回调。定时器,数组的方法回调
- 箭头函数不适合于this无关的回调。事件回调,对象的方法
给函数参数赋初始值
形参初始值
// 形参初始值,一般位置要靠后
const add = (a,b,c=100)=> a+b+c
add(1,2) // 103
与解构赋值结合
function connect({host='127.0.0.1',username,password,port=3306}){
console.log(host)
console.log(username)
console.log(password)
console.log(port)
}
connect({
host:'mdashen.com',
username:'yishen',
password:'yishen'
})
rest参数
用于获取函数的实参,代替arguments
- 传统arguments的使用
function date(){
console.log(arguments)
}
date('2','3','4') // arguments返回的是一个类数组对象,可以arguments[0]这样用
- ES6新增,rest参数
// rest参数必须放到最后
function fu (a,b,...args){
console.log(a)
console.log(b)
console.log(args)
}
fn(1,2,3,4,5,6,7) // a=1,b=2,args=[3,4,5,6,7]
扩展运算符…
… 扩展运算符能将 数组或对象 转换为逗号分隔的 参数序列
- 数组合并
const a = [1,3,5]
const b = [2,4,6]
const c = a.concat(b) //[1, 3, 5, 2, 4, 6]
const d = [...a,...b] // [1, 3, 5, 2, 4, 6]
数据类型 Symbol
基本概念
ES6引入一种新的原始数组类型Symbol,表示独一无二的值。
它是javascript语言的第七种数据类型,是一种类似于字符串的数组类型
- Symbol特点
- Symbol 的值是唯一的,用来解决命名冲容的问题
- ymbol 值不能与其他数据进行运算
- Symbol 定义的对象属性不能使用for.in 循环遍历,但是可以使用 Reflect. owners 来获取对象的所有键名
// 创建Symbol
let s = Symbol()
let s2 = Symbol('描述文字,相当于对这个变量的注释')
console.log(s, typeof s) // Symbol() "symbol"
console.log(s, typeof s2) // Symbol() "symbol"
console.log(Symbol('ddd')===Symbol('ddd')) // false
// Symbol.for创建
let s3 = Symbol.for('yishen')
let s4 = Symbol.for('yishen')
console.log(s3===s4) // true
- 补充js中数据类型,7中基本数据类型+复杂数据类型object
USONB you are so niubility
u undefined
s string symbol
o object
n null number
b boolean BigInt
使用场景
- 向对象中添加方法
// 方法一
let game = {
name:'俄罗斯方块',
up:function(){console.log('我是up方法')},
down:function(){console.log('我是down方法')}
}
let methods = {
up:Symbol(),
down:Symbol()
}
game[methods.up] = function(){
console.log('我也是up方法')
}
game[methods.down] = function(){
console.log('我也是down方法')
}
console.log(game) // 如下图
- 给对象添加方法-------方法二
Symbol内置值
除了定义自己使用的Symbol值以外,ES6还提供了内置的Symbol值,执行语言内部使用的方法。
可以称这些方法为魔术方法,因为它们会在特定的场 景下自动执行。
Symbol.hasInstance | 当其他对象使用 instanceof 运算符,判断是否为该对 象的实例时,会调用这个方法 |
---|---|
Symbol.isConcatSpreadable | 对象的 Symbol.isConcatSpreadable 属性等于的是一个 布尔值,表示该对象用于 Array.prototype.concat()时,是否可以展开 |
Symbol.species | 创建衍生对象时,会使用该属性 |
Symbol.match | 当执行 str.match(myObject) 时,如果该属性存在,会 调用它,返回该方法的返回值 |
Symbol.replace | 当该对象被 str.replace(myObject)方法调用时,会返回 该方法的返回值 |
Symbol.search | 当该对象被 str. search (myObject)方法调用时,会返回 该方法的返回值。 |
Symbol.split | 当该对象被 str. split (myObject)方法调用时,会返回该 方法的返回值 |
Symbol.iterator | 对象进行 for…of 循环时,会调用 Symbol.iterator 方法, 返回该对象的默认遍历器 |
Symbol.toPrimitive | 该对象被转为原始类型的值时,会调用这个方法,返 回该对象对应的原始类型值 |
Symbol. toStringTag | 在该对象上面调用 toString 方法时,返回该方法的返 回值 |
Symbol. unscopables | 该对象指定了使用with关键字时,哪些属性会被with 环境排除。 |
- 当o instanceof Person时,hasInstance方法会自动被调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oknjYeci-1595068131711)(/Users/yishen/Library/Application Support/typora-user-images/image-20200715140644214.png)]
- 可自定义返回结果
迭代器
迭代器(iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。
任何数据结构只要部署iterator接口(对象里面的一个属性Symbol.iterator),就可以完成遍历操作
- ES6创造了一种新的遍历命令 for … of 循环,iterator接口主要供 for … of消费
- 原生具备iterator接口的数据(可用for of 遍历)
- Array、Arguments、Set、Map、String、TypedArray、NodeList
- 工作原理
- 创建一个指针对象,指向当前数据结构的起始位置
- 第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员
- 接下来不断调用 next 方法,指针一直往后移动,直到指向最后一个成员
- 每调用 next 方法返回一个包含 value 和 done 属性的对象
- 需要自定义遍历数据的时候,要想到迭代器
// 迭代器应用
const beijing = {
name: '终极一班',
stus: [
'xiaoming',
'xiaoning',
'xiaotian',
'knight'
],
[Symbol.iterator]() {
let index = 0
let _this = this
return {
next: function () {
if (index < _this.stus.length) {
const result = {value: _this.stus[index],done: false}
index++
return result
} else {
return { value: undefined,done: true}
}
}
}
}
}
for (const v of beijing) {
console.log(v) //xiaoming xiaoning xiaotian knight
}
生成器
生成器函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同
js的异步是单线程的
生成器的声明与调用
function * gen(){
console.log(111)
yield 'yiele是代码的分割符1'
console.log(222)
yield 'yiele是代码的分割符2'
console.log(333)
yield 'yiele是代码的分割符3'
console.log(444)
}
let iterator = gen()
iterator.next() // 111
iterator.next() // 222
console.log(iterator.next()) // {value:'yiele是代码的分割符1',done:false}
console.log(iterator.next()) // {value:'yiele是代码的分割符2',done:false}
生成器的参数传递
- 生成器函数实例
Promise
参考链接,
Promise是ES6引入的异步编程的新解决方案。
语法上Promise是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果
- Promise构造函数:Promise(excutor){}
- Promise.prototype.then()
- Promise.prototype.catch()
.then()返回的结果也是Promise对象,对象状态有回调函数的执行结果决定
.then().then()可以链式调用
数组结构 Set&Map
Set
ES6提供了新的数据结构 Set(集合)。类似于数组,成员的值都是唯一的。
set实现了iterator接口,可以使用 扩展运算符 和 for…of进行遍历
set的属性和方法
size 返回集合的元素个数 add 增加一个新元素,返回当前集合 delete 删除元素,返回boolean值 has 检测集合中是否包含某个元素,返回boolean值 clear 清空集合,返回undefined
// 声明集合set
let s = new Set(['1', 'er', 'san', 'si', 'san'])
console.log(s, typeof (s)) // Set(4){"1", "er", "san", "si"}"object"
// 集合会自动去重,可以利用这点做数组快速去重[...new Set(array)]
s.size() // ->4
s.add('wu') // ->{"1","er","san","si","wu"}
Map
它类似于对象,也是键值对的结合。“键”的范围不限于字符串,各种类型的(包含对象)都可以当做键
Map也实现了iterator接口,可以使用扩展运算符和for…of遍历。
Map的属性和方法
size 返回Map的元素个数 set 增加一个新元素,返回当前Map get 返回键名对象的键值 has 检测Map中是否包含某个元素,返回boolean值 clear 清空集合,返回undefined
class类
ES6提供了更接近传统语言的写法,引入Class 概念。
基本上ES6的class可以看做是一个语法糖,其绝大多数ES5够可以做到。
新的class写法只是让对象原型的写法更加清晰,更像面向对象编程的语法
主要知识点
- class声明类
- constructor定义构造函数初始化
- extends继承父类
- super调用父级构造方法
- static定义静态方法和属性(静态方法属于类,不属于实例化对象)
- 父类方法可以重写
- ES5写法
function Phone(brand,price){
this.brand = brand // brand:品牌
this.price = price
}
Phone.prototype.call = function(){ // 添加方法
console.log("打电话")
}
let Huawei = new Phone('华为',4999) // 实例化对象
Huawei.call()
- ES6写法
class Phone{
constructor(brand,price){ // 构造方法,名字不能修改
this.brand = brand
this.price = price
}
call(){ // 方法必须使用该语法,不能使用ES5的对象完整形式
console.log('打电话')
}
}
let onePlus = new Phone("1+",4999)
onePlus.call()
- 深入研究:链接
数值扩展&对象方法扩展
数值扩展
- Number.EPSION
Number.EPSION 是 JavaScript 表示的最小精度
EPSILON属性的值接近于2.220446049250313e-16
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的
0.1+0.2 // 0.30000000000000004
function equal(a, b) {
return Math.abs(a - b) < Number.EPSILON
}
console.log(equal(0.1 + 0.1, 0.2))
console.log(0.1 + 0.2 === 0.3)
- 二进制、八进制、十进制、十六进制
let b = 0b1010 // 10
let o = 0o777 // 511
let d = 100 // 100
let x = 0xff // 255
下面这些,在Es5中可能是单独的方法,ES6中将其放到,对象下
举例:ES5中
isFinite(Infinity)
ES6中Number.isFinite(Infinity)
Number.isFinite | 检测一个数值是否为有限数 |
---|---|
Number.isNan | 检测一个数是否为NaN |
Number.parseInt | 字符串转数组-整数Int |
Number.parseFloat | 字符串转数组-浮点数Float |
Number.isInteger | 判断一个数是否为整数 |
Math.trunc | 将数字的小数部分抹掉 |
Math.sign | 检测一个数,正数(返回1) 负数(0) 零(-1) |
对象方法扩展
Object.is | 判断两个值是否完全相等 |
---|---|
Object.assign | 对象的合并(重复的覆盖) |
Object.setPrototypeOf | 设置原型对象 |
Object.getPrototypeOf | 获取原型对象 |
模块化
基本的暴露于导入
模块化值指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来
模块功能命令主要由两个命令构成:
- export:规定模块的对外接口
- import:输入其他模块提供的功能
- 三种暴露方式
// 分别暴露
export let school = 'string'
export function teach(){
cosole.log('我可以教你知识')
}
// 统一暴露
let school = 'string'
function teach(){
cosole.log('我可以教你知识')
}
export {school,teach}
// 默认暴露
export default {
school:'qinghua',
change: function(){
console.log('大学期间的学识,可以改变你的一生')
}
}
- 导入方式
// 通用导入方式
import * as m1 from "./m1.js"
console.log(m1.school) // string
// 解构赋值形式
import {school,teach} from "./m1.js"
console.log(school)
import {school as xuexiao } from "./m2.js" // 导入的不同模块重名,可以使用呢as起别名
console.log(xuexiao)
import {default as m3} from './m3.js'
console.log(m3.school) // qinghua
// 简便形式 针对默认暴露
import m3 from './m3.js'
babel转化ES
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
编译打包为es5过程,局部安装用npx 全局安装不需要
- 安装工具
babel-cli babel-preset-env browserify(webpack)
- npx babel src/js -d dist/js | 原路径,编译后路径
- 打包 npx browserify dist/app.js -o dist/bundle.js
- 使用npm
import $ from 'jquery' // const $ = require('jquery')
$('body').css('background','pink')
// 后面正常编译打包就好
ES7新特性
Array.prototype.includes
Includes 方法用来检测数组中是否包含某个元素,返回Boolean,indexOf也可以实现类似功能
const mingzu = ['西游记','红楼梦','三国演义','水浒传']
console.log(mingzhu.includex('西游记')) // true
指数操作符 **
ES7中引入指数运算符 ** ,用来实现幂运算,功能与Math.pow结果相同
console.log(2**10) // 1024
console.log(Math.pow(2,10)) // 1024
ES8新特性
async和await
async和await两种语法结合让异步代码像同步代码一样
- async函数
async函数的返回值为promise对象
promise对象的结果又async函数执行的返回值决定
- await表达式
await 必须写在async函数中
await右侧的表达式一般为promise对象
await返回的是promise成功的值
await的promise失败了,就会抛出异常,需要通过
tru ... catch
捕获异常
对象方法扩展
Object.values | 返回一个给定对象的所有可枚举属性值的数组 |
---|---|
Object.entries | 返回一个给定对象自身可遍历属性 [key,value] 的数组 |
Object.getOwnPropertyDescriptors | 返回指定对象所有自身属性的描述对象 |
const school = { // 声明对象
name:'qinghua',
cities:['北京','上海','广州','深圳'],
xueke:['前端','python','nodejs','php']
}
console.log(Object.keys(school)) // 获取所有的键 ["name", "cities", "xueke"]
console.log(Object.values(school)) // 获取有的值 ["qinghua", Array(4), Array(4)]
console.log(Object.entries(school)) // [[key1,value1],[key2,value2]...],可用于生产Map
console.log(new Map(Object.entries(school)))
console.log(new Map(Object.entries(school)).get('name')) // 'qinghua'
console.log(Object.getOwnPropertyDescriptors(school)) // 返回对象属性的 描述对象 见下图
const obj = Object.create(null,{ // 第一个参数:原型对象,第二个参数:描述对象
value:'yishe', // getOwnPropertyDescriptors返回的就是描述对象
writable:true, // 是否可写,(writable cofigurable enumerable)属性特性
cofigurable:true, // 是否可以删除,属性特性克隆对象时也许会用到
enumerable:true // 是否可以枚举
})
ES9新特性
Rest参数、spread扩展运算符
ES6中已经引入,在ES9中为对象提供了像数组一样的rest参数和扩展运算符
- rest参数在对象中的应用
function connect({host,port,...user}){
console.log(host)
console.log(port)
console.log(user) // user是包含username和password的对象
}
conect({
host:'127.0.0.1',
port:3306,
username:'root',
password:'root'
})
- 扩展运算符在对象中的应用
可以将对象展开成key:value,的形式;与展开数组类似
正则扩展
正则基础知识-菜鸟教程 自己总结的-xmind-蓝奏云-下载路径
正则扩展-命名捕获分组
// 传统方式,没用正则扩展
let str = '<a href="http://blog.mdashen.com">yishenBlog</a>'
const reg = /<a href="(.*)">(.*)<\/a>/
console.log(reg.exec(str)) //正则匹配href属性的值,和标签内容内容
// 正则扩展
const reg2 = /<a href="(?<url>.*)">(?<text>.*)<\/a>/ // 类似于给匹配到的内容起一个别名
正则扩展-反向断言
正则匹配时,有多个匹配到的内容;可以根据目标前面或后面,来对目标进行唯一性识别
let str = 'Js34342你知道吗555啦啦啦'
const reg = /\d+(?=啦)/ // 匹配后面是'啦'的数值 正向断言
const reg1 = /(?<=吗)\d+/ // 匹配前面是'吗'的数值 反向断言
正则扩展-dotAll模式
dot . 元字符 除换行符以外的任意单个字符
const reg = /正则内容/s ,末尾增加s,使’.'可以匹配换行符
ES10新特性
对象扩展方法
Object.fromEntries
用来创建一个对象,参数是一个二维数组或者Map对象( 将二维数组或者Map对象转化为对象 )
与Object.entries 为逆运算
字符串扩展方法
trimStart、trimEnd
ES5中有 trim方法用来清除字符串两边的空白字符
trimStart:清除左侧空白字符,保留右侧空白字符
trimEnd:清除右侧空白字符,保留左侧空白字符
数组方法扩展
flat、flatMap
- flap
flap:拍打、平
将多维数组转化为低维数组 arr.flat(argus) 参数为深度,3维转1维深度为2
- flapMap
如果arr.map()返回的是一个二维数组,可以使用flapMap代替map,返回一维数组
Symbol方法扩展
Symbol.prototype.description
获取Symbol的字符串描述
let s = Symbol('yishen')
s.description // -> yishen
ES11新特性
私有属性
只能在类的内部对其进行修改和访问
class Person {
name; // 公有属性
#age; // 私有属性,
#weight;
constructor(name,age,weight){ // 构造法方法
this.name = name
this.#age = age // 符号'#'带上
this.#weight = weight
}
intro(){
console.log(this.name)
console.log(this.#age) // 在内的内部正常访问私有属性
console.log(this.#weight)
}
}
const girl = new Personn('小红',18,'45kg')
console.log(girl) // 正常访问
console.log(girl.#age) // 在类的外部无法访问私有属性
Promise.allSettled
接收一个promise数组,返回promise对象(永远是成功的状态)
返回的值是每一个promise的状态和结果
参考链接-allSettled 扩展阅读-Promise 中的三兄弟 .all(), .race(), .allSettled()
- Promise.all([p1,p2])
全部成功,才返回成功,有一个失败就返回失败
Promise.all 和 Promise.allSettled常用于批量异步任务
字符串扩展
String.prototype.matchAll
用来得到正则批量匹配得到的结果
let str = `
<ul>
<li>
<a>肖申克的救赎</a>
<p>上映日期:1994-0910</p>
</li>
<li>
<a>阿甘正传</a>
<p>上映日期:1994-0716</p>
</li>
</ul>
`
const reg = /<li>.*?<a>(.*?)<\/a>.*?<p>(.*?)<\/p>/sg // [s](#正则扩展-dotAll模式)
const result = str.matchAll(reg)
// for (let v of result) { // 与下面...一个功能
// console.log(v)
// }
console.log([...result])
可选链操作符?.
按照操作符之前的属性是否有效,链式读取对象的属性或者使整个对象链返回
undefined
function main(config){
const dbHost = config && config.db && config.db.host // config &&判断config是否存在
console.log(dbHost) // 当config存在,才能读取config.db
} // 不先判断,当config不存在,直接config.db会报错
main({
db:{
host:'192.168.0.2',
username:'root'
}
})
// 可选链操作符?.
const dbHost = config?.db?.host // 可以起到上述判断的作用
动态import
当使用到需要导入的模块时,才导入。提升加载效率
const btn = document.getElementById('btn')
btn.onclick = function(){
import('./hello.js').then(module=>{ // import返回promise对象,module就是导入的对象
module.hello() // hello()hello.js中一个普通方法
})
}
BigInt
用于大数值运算
let n = 521n // 大整型
console.log(n,typeof(n)) // 521n "bigint"
let n1 = 521
BigInt(n1) // 普通整型转大整型 -> 521n
let max = Number.MAX_SAFE_INTEGER // 9007199254740991
console.log(max)
console.log(max+1) // 正常
console.log(max+2) // 不正常,在向后加,有的正常有的不正常
console.log(BigInt(max))
console.log(BigInt(max)+BigInt(1)) // 正常,BigInt 不能直接与Int运算,要先转成BigInt
console.log(BigInt(max)+BigInt(2)) // 正常
globalThis
始终指向全局对象,无论执行环境是什么(浏览器、nodejs)
浏览器下指:Window
nodejs下指:global