ES2022(ES13)新标准详解

308 阅读25分钟
ES2022

日内瓦时间2022年6月22日,Ecma国际宣布ES2022标准获得通过,这已经是ECMAScript的第13个版本了。

这并不是什么大事,因为该标准中加入的新语言特性早已被各大浏览器实现,开发人员或许早已用上了这些新特性。Ecma国际中负责ECMAScript的通用语言部分的工作组叫TC39,他们有一个不断迭代的机制,即著名的5个stage,所有新特性都是经过这几个阶段最终面世。每年一次的新标准发布实际上只是他们每年工作的一个快照,每年找一个时间把已经在stage4阶段的新特性合并到上一版spec,便是一个新版的ECMAScript spec。

不过我们还是来详解一下这几个新特性:

类实例公有属性字段声明

没有该功能时

曾经,每每在声明一个类时,由于不能声明属性字段,总觉得有点不舒服,感觉可读性很差,就像下面这样:

class Counter {
    // 不支持这样写
    // n = 0;
    
    constructor() {
        this.n = 0;
    }
    
    add() {
        this.n++;
    }
}

n无法明确的声明,不能一眼看出该类有那些固定的成员字段,不是很优美。

有该功能后

现在这个问题解决了,新的语法是这样:

class Counter {
    // 声明成员字段,并赋值
    n = 0;
    
    add() {
        this.n++;
    }
}

新语法的特性

n是一个公有字段,公有字段有两个特性:

  1. 可以从Counter的外部访问到n
const counter = new Counter();
console.log(counter.n); // 可以打印出n
  1. 可以被继承
class Computer extends Counter {
}
const computer = new Computer();
consol.log(computer.n); // 打印0
computer.add();
console.log(computer.n); // 打印1

虽然class有了很大的改进,但是JavaScript基于原型模式的对象模型并没有变化,那么n在Counter的实例对象中,是什么样的角色呢?看下面的代码:

const counter = new Counter();
console.log(counter.hasOwnProperty('n')); // 打印true
console.log(counter.hasOwnProperty('add')); // 打印false
console.log(counter);

可以看到,n是counter自己的属性,而add是counter原型对象上的方法。 n被继承后呢?看下面的代码:

console.log(computer.hasOwnProperty('n')); // 打印true
console.log(Object.getPrototypeOf(computer).hasOwnProperty('n')); //打印false
console.log(Object.getPrototypeOf(computer).hasOwnProperty('add')); //打印false
console.log(Object.getPrototypeOf(Object.getPrototypeOf(computer))
.hasOwnProperty('add')); // 打印true

可以看到n被继承后直接被挂到了子类computer上,是computer的自有属性,而不是在computer的原型对象上。n是被单个实例对象独占的。 而add方法则在computer的原型的原型上。add被放到原型上是完全可以理解的,因为我们并不希望add方法有多个副本,多个副本只会增加内存开销,没有其他意义。

一个有争议的问题

看如下代码:

class Counter {
    set n(value) {
        console.log(value);
    }
}

class Computer extends Counter {
    n = 9;
}
const computer = new Computer(); // setter n中的代码不会被执行
console.log(computer);

上面的代码中,n虽然在子类中被赋值,但是setter n中的代码并不会被执行。 因为在子类中n = 9;被认为是一个字段的定义,而是一个set操作,打印computer会发现,n属性直属于computer,而setter n在computer原型的原型中。 TC39在讨论这个新功能时曾经有另一个声音,就是setter n应该被执行,也就说n = 9;被认为是一次set操作,不过这个想法最终被TC39否决了。

没有被初始化的字段会被设置成undefined

看下面的代码:

class Counter {
    
    n = 9;
}

class Computer {
    n;
}
const computer = new Computer();
console.log(computer.n); // 打印undefined

Computer中的n虽然没有初始化,但是会被赋上默认值undefined,并且会覆盖父类中的n的值。

浏览器兼容性

这里我们部分引用MDN整理的数据,供大家参考:

ChromeEdgeInternet ExplorerSafari
2019年 72版2019年 79版未实现2021年14.1版
Chrome AndroidSafari on iOSWebview Android
2019年 72版2021年 14.5版2019年 72版
FirefoxOperaFirefox for Android
2020年 69版2019年 60版2020年 79版
Opera AndroidSamsung Internet
2019年 51版2019年 11.0版
DenoNode.jsBabel
2020年 1.0版2019年 12.0.0版2018年 7.0+版

可以看到,几个主要厂商中,谷歌和微软都是比较积极的,在第一时间就实现了该功。 苹果在新标准的支持上总是要慢几个节拍,相比谷歌,对Web不那么友好,大家在用到新功能时一定要记得在苹果机上多测试一下。 Node.js的行动也很迅速,值得称赞。


类私有字段

面向对象一个重要特性就是封装性,之前在JavaScript中是没法直接做到的,大部分时候通过君子之约:_aPrivateField,在字段前加一个下划线,来表示私有,然而还是可以在外界访问到,算是个缺陷。 现在,有了新语法,我们可以直接做到了,示例如下: 私有成员属性字段:

class Counter {
    #n = 0;
    
    add() {
        this.#n++;
    }

私有成员函数:

class Counter {
   #n = 0;
   
   #print(s) {
    console.log(s);
   }
   
   add() {
       this.#n++;
       this.#print(this.#n);
   }
}

要想让某个字段或者成员函数私有,只要在它的名字前面加上#。 这个语法乍一看确实有些奇怪,但是看一下他的来源就不会觉得奇怪了。上面说过在这之前,我们经常用下划线前缀来表示私有成员字段,现在把下划线改成#,就可以从语言层面实现私有了,_aPrivateField => #aPrivateField,_就是#这个想法的来源。

知识点

  • 私有变量只能在类内部访问,不能被继承

  • 不支持动态访问 要注意的是,this['#x']和this.#x是不一样的,私有字段名字不支持动态的属性访问,也不支持计算属性,请看示例代码:

     class Counter {
      
      #n = 9;
      add() {
          
          console.log(this['#n']); //打印undefined
          console.log(this['n']); //打印undefined
      }
      
     }
    

    #n是一个私有标识符,只能是通过字面量的形式访问,无法通过变量的形式动态访问。这个是故意为之的,为的是避免下面的情况:

     class Counter {
      
      #n = 9;
      #m = 'abc';
      
      add(key, value) {
          this[key] = value;
      }
     }
     const counter = new Counter();
     counter.add('#m', 55);
    

    上面这段代码中,如果私有字段可以通过this['#x']的形式动态访问,Counter类的使用者容易就不经意的覆盖Counter的任意一个私有字段,从而造成不可预知的后果。私有字段就是为了私密封装性,而动态访问会破坏封装性,久而久之,这就会成为大家的槽点。

  • 为什么不用private x,这种形式? private x这种形式Java中是这样用的,所以广为人知,但是在JavaScript中情况稍有不同,private x对应的访问语法是this.x,在JavaScript中,属性是可以轻而易举的挂到对象上的,不管是class内部还是外部,请看如下代码:

    // 下面的代码是用假设的语法写的伪代码,不能再实际中执行
    class Counter {
        private n = 9;
        
        add() {
            this.n++;
            console.log(this.n);
        }
    }
    const counter = new Counter();
    counter.n = 20;
    counter.add();
    

    上面的代码中,Counter有一个私有属性字段n,通过this.n访问,而在Counter外部,又给counter实例挂了一个属性n,那这时内部的this.n是访问内部私有的n还是外部挂载的n呢?在Typescript中,使用的private x的形式,但是Typescript也是不允许在外部动态挂载一个和私有变量重名的属性的,在JavaScript如果也做这种限制,则很容易引起向后兼容问题。因此JavaScript必须另开辟出一套和已有的JavaScript属性字段访问完全不同的语法。

  • 在类中也可以访问该类的其他实例变量的私有属性字段 示例代码:

    class Counter {
    
     #n = 9;
    
     add(counter) {
      console.log(this.#n + counter.#n);
     }
    }
    const counter = new Counter();
    const anotherCounter = new Counter();
    counter.add(anotherCounter);
    

    counter.#n如果在Counter类的外部,则不能成功访问,但是在内部,这段代码是允许的。 那如果是访问子类或者父类实例对象的私有属性字段呢?规则如下:

    1. 访问父类实例的私有属性字段会报错
    2. 访问子类实例的私有属性字段时,实际访问到的是原型链中当前类实例的属性字段,并不能访问到子类实例本身所拥有的私有属性字段
  • 私有属性字段只能有当前类中的方法访问,继承过来的方法可以访问当前类的公有属性字段,但是对于私有属性字段,继承的方法只能访问它所属类实例的私有属性字段。示例代码如下:

      class Counter {
    	#n = 9;
    	show() {
    		console.log(this.#n);
    	}
      }
      class Computer extends Counter {
    	#n = 5;
      }
      const computer = new Computer();
      computer.show(); // 打印9
      ```
    show作为公有方法被Computer继承,但是它只能访问Counter下的#n字段。
    
    
  • 私有和公有属性字段可以有相同的名字,示例代码如下:

      class Counter {
    	#n = 9;
    	n = 5;
    	show() {
    		console.log(this.#n, this.n);
    	}
      }
      ```
    他们互不影响
    
    
  • 类之外没有访问私有属性字段的可能 在类外部检测私有属性字段是否存在、获取属性值、修改属性值,都是没有可能的。 除非该类主动暴露出方法、getter或者setter

浏览器等运行环境、转换器的兼容性

部分数据来自于MDN,供大家参考

ChromeEdgeInternet ExplorerSafari
2019年 74版2019年 79版未实现2021年 14.1版
Chrome AndroidSafari on iOSWebview Android
2019年 74版2021年 14.5版2019年 74版
FirefoxOperaFirefox for Android
2021年 90版2019年 62版2021年 90版
Opera AndroidSamsung Internet
2019年 53版2019年 11.0版
DenoNode.jsBabel
2020年 1.0版2019年 12.0.0版2018年 7.0+版

和公有属性字段对比,私有属性字段的实现相对晚一些。 相信大部分项目都会使用Babel,只需将Babel升级到7.0以上,便可以使用以上功能。

类静态属性字段和方法

ES2015(ES6)已经出了静态方法,这次由于有了公有/私有属性字段、私有方法,因此也就自然而然的加上了静态公/私有属性、静态私有方法。

静态公有属性字段和方法

之前要定义一个静态属性字段或者方法得这样做:

class Counter {

}
Counter.Max = 1000;
Counter.Min = 5;
Counter.isBigger = function() {
    
}

现在我们可以这样了:

class Counter {
    static Max = 1000;
    static Min = 5;
    static isBigger() {
        
    }
}

不同上下文中具体访问形式不同,可以分成这几类:公有实例方法中、私有实例方法中、静态公有方法中、静态私有方法中、类外部。 分别说明如下:

  • 公有实例方法中和私有实例方法中:

    class Counter {
        static Max = 1000;
        static isBigger() {
          console.log('isBigger');
        }
        #isBigger() {
    
          console.log(Counter.Max); // 打印1000
          console.log(this.constructor.Max); // 打印1000
          console.log(this.Max); // 打印undefined
        }
        show() {
            console.log(Counter.Max); // 打印1000
            console.log(this.constructor.Max); // 打印1000
            console.log(this.Max); // 打印undefined
            Counter.isBigger(); // 打印isBigger
            this.constructor.isBigger(); // 打印isBigger
            this.#isBigger();
            this.isBigger(); // 抛出异常,this中没有isBigger
        }
    }
    const counter = new Counter();
    counter.show();
    

    从以上两种形式中可以看到,this.Max是undefined,实例方法中是不可以通过this(实例对象)来访问静态属性字段的,从原型链角度看,静态属性字段和方法都在类上,而不在类的实例上。 在实例方法中,必须通过类来引用静态属性字段,在本例中就是Counter,也可以用this.constructor,因为在这个例子中,this.constructor指向的Counter。

  • 静态公有方法和静态私有方法中

    class Counter {
        static Max = 1000;
        static show() {
            console.log(Counter.Max);
            console.log(this.Max);
            this.#isBigger();
        }
        static #isBigger() {
            console.log(Counter.Max);
            console.log(this.Max);
        }
    }
    Counter.show();
    

    可以看到,Counter.Max和this.Max都是可以正常使用,由于show的调用对象是Counter,因此this指向的是Counter,而静态属性字段是挂在Counter下的,因此可以顺利访问。

  • 类外部

    class Counter {
        static Max = 1000;
    }
    console.log(Counter.Max); // 打印1000
    

    静态公有属性字段的继承: 静态公有属性字段可以被继承,示例代码如下:

    class Counter {
      static n = 9;
      
    }
    
    class Computer extends Counter {
      
    }
    console.log(Computer.n); // 打印9
    console.log(Computer.hasOwnProperty('n')); // 打印false
    Counter.n = 20;
    console.log(Computer.n); // 打印20
    Computer.n = 30;
    console.log(Counter.n); // 打印20
    console.log(Computer.hasOwnProperty('n')); // 打印true
    

    从上面的示例代码可以看到,Computer确实继承了Counter的n属性字段,但是n仍然挂在Counter上,Computer.n通过原型链找到了Counter的n,Counter.n变化时,Computer.n自然也就变化了,因为它们是同一个属性字段。 但是当Computer.n被赋值30时,Couter.n并没有变化,因为这时在Computer下新创建了属性n,Computer.hasOwnProperty('n')返回true。

静态私有属性字段

class Counter {
    static #n = 9;
    
    static isBigger() {
        console.log(this.#n);
        console.log(Counter.#n);
    }
    show() {
        Counter.isBigger();
        console.log(Counter.#n);
        console.log(this.constructor.#n);
    }
    
}
const counter = new Counter();
counter.show();

静态私有属性字段名以#开头,不能被继承且只能在类内部访问。 在静态方法中,可以直接通过this访问静态私有属性字段,在上面的例子中,isBigger中的this指向的是Counter,但是如果涉及到继承,用this去访问静态私有属性,很容易出问题:

class Counter {
    static #n = 9;
    static isBigger() {
        console.log(this.#n); // 这里会抛异常
    }
}

class Computer extends Counter {
    
}
Computer.isBigger();

当Computer.isBigger()执行时,isBigger中的this指向的是Computer,但是由于#n是私有成员,无法被继承,所以Computer无法访问#n,并且会有异常抛出,这是一个fail fast的操作。

这里我们对比一下实例私有属性字段,会发现有些不一致的地方:

class Counter {
    #n = 9;
    show() {
        console.log(this.#n); // 打印9
    }
}
class Computer extends Counter {
    
}
const computer = new Computer();
computer.show();

可以看到相同的方式,如果是实例私有属性字段,则不会报错,会获取到Counter中#n的初始值9。 computer.show()的调用中,show的上下文是computer,因此show方法中的this指向的是computer,而computer中并没有定义#n,尽管#n不支持继承,this.#n还是拿到了Counter中#n的初始值。其实即使Computer中定义了#n,show方法中的this.#n也还是会访问Counter中#n,因为私有属性字段只能自己类内部访问。其实这里是私有属性字段的一个特性,即上面说的,私有属性字段可以在当前类中被这个类的实例所访问,子类实例也可以,但是只能获取当前类实例所对应的#n的值。这个和静态私有属性字段直接抛异常的行为不一致。总体上看这个特性还是挺让人困惑的。

浏览器等运行环境的兼容性:

ChromeEdgeInternet ExplorerSafari
2019年 72版2019年 79版未实现2021年 14.1版
Chrome AndroidSafari on iOSWebview Android
2019年 72版2021年 safari14.1 iOS14.52019年 72版
FirefoxOperaFirefox for Android
2021年 90版2019年 60版2021年 90版
Opera AndroidSamsung Internet
2021年 64版2020年 11.1版
DenoNode.jsBabel
2020年 1.0版2019年 12.0.0版2018年 7.4版

类静态初始化代码块

如果我们希望类在加载时(区别于创建类的实例)除了进行字段和方法的声明和初始化外,还要执行一些逻辑,例如有两个静态属性字段都依赖同一个方法调用的返回值。静态代码块便是用来做这个的。 示例代码:

function f() {
    return {n: Math.random(), m: Math.random()};
}
class Counter {
    static n;
    static m;
    static {
        const obj = f();
        Counter.n = obj.n;
        Counter.m = obj.m;
    }
}

静态代码块相当于一块独立的编程区域,有自己的作用域和特殊的上下文,具体规则列出如下:

  • this指向的是当前类。可通过this访问其他静态成员,也可以直接通过类名字访问
  • super.parentProperty访问父类的静态属性字段。这里parentProperty指代父类的静态属性字段或者方法,但不可以调用super()
  • 可以有多个静态代码块,可以和其他成员穿插放置
  • 可以访问私有属性字段。这个是之前的任何方案(Decorator或者类外挂载)都无法做到的
  • var let const function定义的变量的作用域都只局限在静态代码块的大括号内,都是静态代码块级作用域
  • var不会变量提升
  • 静态属性字段和静态代码块的初始化是从上往下进行的。后面的可以访问前面的,前面的不可以访问后面的,但是静态函数是个例外,静态属性字段和代码块中可以直接使用在它们后面声明定义的静态函数

示例代码如下:

class Counter {
    static #p = 'private property';
    static n = 9;
    static {
        var myVar = 6;
        console.log(Counter.n); // 通过类名访问其它静态成员,打印9
        console.log(this.n); // 通过this访问静态成员字段,打印9
        console.log(this.#p); // 可以访问私有成员字段,打印private property
        console.log(Counter.m); // 前面的不可以访问后面的,打印undefined
        this.print(); // 通过this访问其它静态成员函数,打印print
        console.log(n); // 抛异常,n未定义
    }
    static m = 22;
    
    static print() {
        console.log('print');
    }
    
    show() {
        
    }
    
    // 可以有多个静态代码块
    static {
        console.log('另一个static block');
    }
}
console.log(myVar);

访问父类静态成员:

class Counter {
    static n = 9;
    static f() {
        console.log('f');
    }
}
class Computer extends Counter {
    static {
        console.log(super.n); // 通过super访问父类静态属性字段
        super.f(); // 通过super访问父类静态成员函数
    }
}

浏览器等运行环境的兼容性如下,部分数据来自于MDN,供大家参考:

ChromeEdgeInternet ExplorerSafari
2021年 94版2021年 94版未实现未实现
Chrome AndroidSafari on iOSWebview Android
2021年 94版未实现2021年 94版
FirefoxOperaFirefox for Android
2021年 93版2021年 80版2021年 93版
Opera AndroidSamsung Internet
2021年 66版2022年 17.0版
DenoNode.jsBabel
2021年 1.14版2021年 16.11.0版2020年 7.12.0版

私有字段是否存在检查

首先说明的是在一个类的外部是不可能检测到某个私有字段是否存在于某个实例中的。 因此这里说的检查,是指在类的内部,来检测某个实例对象是否包含该类的私有字段,如果包含的话,说明该实例是当前类的实例。 检查的新语法:#xxx in obj 示例代码:

class Counter {
    #n = 9;
    show(obj) {
        console.log(#n in obj);
    }
}
const counter = new Counter();
counter.show({}); // 打印false
counter.show(counter); // 打印true

class Computer extends Counter {

}
const computer = new Computer();
counter.show(computer); // 打印true
computer.show(counter); // 打印true

从示例代码可以看出,被检测对象也可以是当前类的子类的实例。

这个新语法基本上断绝了以后将#xxx变成this.#xxx快捷方式的可能,因为如果#xxx是this.#xxx的快捷方式,那#n in obj就等于this.#n in obj,那这句话的意思应该是this.#n的值是否存在于obj中,而实际想做的是#n是否在obj中。

浏览器兼容性:

ChromeEdgeInternet ExplorerSafari
2021年 91版2021年 91版未实现2021年 15版
Chrome AndroidSafari on iOSWebview Android
2021年 91版2021年 152021年 91版
FirefoxOperaFirefox for Android
2021年 90版2021年 77版2021年 90版
Opera AndroidSamsung Internet
2021年 64版2022年 16.0版
DenoNode.jsBabel
2021年 1.9版2022年 16.4.0版2020年 7.10.0版

正则匹配indices

不要被标题吓住。一句话解释就是: 给正则对象的exec、字符串的match、字符串的matchAll方法的返回对象添加了indices属性,indices中记录了捕获组匹配出的分组字符串在目标字符串中的起始和结束位置。

所谓捕获组默认情况下就是正则表达式中用括号括起来的部分,被括号中的正则匹配的部分字符串会在返回结果对象中单独列出来,如果不想括号中的内容被单独捕获,可以在括号中内容前面加上?:、?=、?!、?<=、?<!,将分组设置成非捕获组。 下面是一个捕获组的例子:

const str = '2022-07-03';
const result = str.match(/(\d{4})-(\d{2})-(\d{2})/);

result的结构如下: image.png

返回的数组共有有4项,第一项是匹配出的完整的字符串,第二项是第一个捕获组匹配的字符串,第三项是第二个捕获组匹配的字符串,依次类推。另外还有几个属性,groups是命名捕获组,咱们后面讲;index是匹配出的字符串的起始位置,input表示目标字符串。

ES2022这次新加的功能做了如下事情: 在返回结果中加入了indices属性,index属性代表的是整个正则所匹配的字符串在目标字符串中的起始位置。indices则代表的是整个正则和其捕获组所匹配的字符串在目标字符串中的起始和(结束位置+1)。考虑到性能问题,默认情况下是不会开启indices的,需要给正则加上参数d,示例代码如下: javascript const str = '2022-07-03'; const result = str.match(/(\d{4})-(\d{2})-(\d{2})/d); result的结果如下: image.png indices是个数组,第一项代表着整个正则匹配字符串的起始和(结束位置+1),第二项表示第一个捕获组匹配的字符串起始和(结束位置+1),依次类推。

什么是命名组: 命名组是ES2022之前就有的特性。上面说捕获组的返回结果中,会分别给出整个正则匹配的字符串和按照捕获组匹配的字符串,索引是数字,顺序是在正则中的先后顺序,但是有命名组后,我们可以在正则中给捕获组命名,然后groups属性中会按照捕获组名字为键值(即索引变成了捕获组的名字),给出匹配的子字符串,举例如下:

const str = '2022-07-03';
const result = str.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);

result结果如下: image.png 可以看到捕获组匹配出的子字符串除了用数字索引作为键展现出来,groups属性中还使用了命名组的名字作为键。

在ES2022的indices特性中,对于带命名组的捕获组,可以以命名组的名字作为键值,来列出匹配出的子字符串的起始和结束位置。示例代码如下:

const str = '2022-07-03';
// 加d标记,开启indices属性
const result = str.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/d);

result结果如下: image.png 可以看到indices下的groups下,有day、month、year属性,分别列出了每个捕获组匹配字符串的起始和结束位置。

indices属性给出了匹配子字符串的起始和结束位置,如果捕获组有名字,还可以以名字做键,给出起始和结束位置。

另外正则对象还添加了hasIndices属性,用来判断当前正则对象是否加了d参数来开启indices。

浏览器兼容性,部分数据来自于MDN,供加大家参考:

ChromeEdgeInternet ExplorerSafari
2021年 90版2021年 90版未实现2021年 15
Chrome AndroidSafari on iOSWebview Android
2021年 90版2021年 152021年 90版
FirefoxOperaFirefox for Android
2021年 88版2021年 76版2021年 88版
Opera AndroidSamsung Internet
2021年 64版2021年 15.0版
DenoNode.jsBabel
2021年 1.8版2021年 16版未实现

代码顶层可直接await

相信都有过忘记在await外层包一个async的经历,直接就报错了。 现在可以直接写await了,不用非得在外面包一个async。 但是这也会给我们的代码带来问题,asycn的作用是给我们的异步变同步代码设定一个范围,在async范围内且在await语句后面的代码会等await后面的Promise resolve了再去执行,如果不写async的话,那就意味着整个模块中await后面的代码和依赖当前模块的模块代码都需要等await后面的promise变为resolve状态才能继续执行。开发人员很有可能会漏写外层的那个async,从而导致错误发生。

有顶层await之前的代码示例:

(async function() {

    const t = await Promise.resolve('a');
    console.log('tttttttt', t);
})()

我们一般会封装一个自执行函数,把我们需要同步顺序执行的代码都放进去。 现在,不用自执行函数了:

const t = await Promise.resolve('a');
console.log('tttttttt', t);

仅从概念上看很简单,但是有几点需要关注一下:

  • 在ES模块中使用时,如果一个模块中有await没有被包在async中,那不仅仅是当前模块await后面的代码需要等待,依赖该模块的模块和整个依赖链中后面的代码也需要等await后面的Promise变成resolve状态才能继续执行。

  • 和当前模块没有直接或者间接依赖关系的模块不受await的影响。当代码执行过程中遇到了一个顶层的await,则该模块后面的代码和依赖链后面的模块的代码会被跳过,先去执行其他没有依赖关系的模块的代码。 借TC39提案中的例子:

    // x.mjs
    console.log("X1");
    await new Promise(r => setTimeout(r, 1000));
    console.log("X2");
    
    // y.mjs
    console.log("Y");
    
    // z.mjs
    import "./x.mjs";
    import "./y.mjs";
    

上面这段程序打印的顺序是:X1 Y X2。 一旦遇到await,会先跳过后面的代码,先执行没有依赖关系的模块代码,从而打印Y,等Promise变成resolved后,await语句之后的语句开始执行,打印X2。

数组添加了at()方法

Array.prototype.at(),at方法除了可以用0和正整数对数组进行索引外,还可以用负数从数组的最后一项往前索引。对于下面的数组,正整数从左向右从小到大,负整数从右向左,从大到小:

image.png

const arr = ['one', 'two', 'three', 'four', 'five'];
arr.at(-1); // five

浏览器兼容性,polyfill的支持,部分数据来自于MDN,供加大家参考:

ChromeEdgeInternet ExplorerSafari
2021年 92版2021年 92版未实现2022年 15.4
Chrome AndroidSafari on iOSWebview Android
2021年 92版2022年 15.42021年 92版
FirefoxOperaFirefox for Android
2021年 90版2021年 78版2021年 90版
Opera AndroidSamsung Internet
未实现2021年 16.0版
DenoNode.jscore-js
2021年 1.12版2021年 16.6.0版3版

Object添加了hasOwn方法

Object.hasOwn(object, 'prop'),用于检测prop是否是object自己的属性,而不是继承而来的。

在这之前有Object.prototype.hasOwnProperty('prop'),这个方法被挂在了Object.prototype上,意思是说所有对象都可以直接调用hasOwnProperty方法,例如:

const o = {};
o.hasOwnProperty('p');

但是如果hasOwnProperty被覆盖呢?

const o = {hasOwnProperty: 1};
o.hasOwnProperty('p'); //抛异常
Object.prototype.hasOwnProperty.call(this)

或者,对象并没有继承自Object.prototype:

Object.create(null).hasOwnProperty("foo") // 抛异常

因此Object.hasOwn(object, 'prop')应运而生。它的功能和hasOwnProperty是一样的。 可能有人会问,为什么不叫Object.hasOwnProperty?因为Object.hasOwnProperty已经存在了,Object本身也是继承自Object.prototype,所以Object.hasOwnProperty('prop')用于检测Object是否自带某个属性而不是继承。 既然已经被占用了,就干脆叫Object.hasOwn了,也不影响理解。

不过要注意的是,Object.haOwn也可以被修改:

Object.hasOwn = 9;
Object.hasOwn({}, 'p'); // 抛异常, Object.hasOwn现在是9

创建Error时可传入cause

throw new Error('有错误发生', {cause: error});

当抛异常时,我们一般都希望语言能够提供更多的上下文信息,以便更好的进行错误修复。 之前我们有这几种做法: 做法一,容易丢失调用栈信息:

try {
    const data = await fetch('...');
} catch(err) {
   console.log('fetch失败了');
   throw new Error('fetch失败了' + err.message);
}

上面的代码异常的调用栈追踪只能追踪到catch里的throw那一行,而真正的出错的代码则不能追踪到。

做法二,代码可能会有些复杂,通过继承的方式,扩展出一些自定义Error:

class CustomError extends Error {
    constructor(msg, cause) {
        super(msg);
        this.cause = cause;
    }
}
try {
    const data = await fetch('...');
} catch(err) {
   console.log('fetch失败了');
   throw new CustomError('fetch失败了', err);
}

现在的做法:

function getData () {
    try {
        const data = await fetch('...');
    } catch(err) {
       console.log('fetch失败了');
       throw new Error('fetch失败了', {cause: err});
    }
}

try {
    getData();
} catch(e) {
    console.log(e);
    console.log('Caused by', e.cause);
}

Firefox中new Error还支持在第二第三个参数中传入fileName和lineNumber,但是这并没有遵循任何标准。还好新标准中第二个参数是个对象,可以和fileName、lineNumber区分开,新版Firefox对新标准也是支持的。

浏览器等运行环境、polyfill的支持 部分数据来自于MDN,供大家参考

ChromeEdgeInternet ExplorerSafari
2021年 93版2021年 93版未实现2021年 15版
Chrome AndroidSafari on iOSWebview Android
2021年 93版2021年 15版2021年 93版
FirefoxOperaFirefox for Android
2021年 91版未实现2021年 91版
Opera AndroidSamsung Internet
未实现2022年 17.0版
DenoNode.jscore-js
2021年 1.13版2021年 16.9.0版3版

就是这些了。