新版发布
ECMAScript 2023版(或者叫ES14、ECMAScript 14)已经于2023年6月27日获得了通过,正式发布,JavaScript又一个新的规范诞生了,新规范的网址:tc39.es/ecma262/202…。
对于开发人员来说,这确实不算什么大事,因为:
- 一方面,构建工具和polyfill中早已经实现了绝大部分特性,开发人员早已提前上用了。
- 另一方面,其实短期内我们并不指望可以脱离构建工具和polyfill直接使用这些特性,虽然各大浏览器今年已经陆续在新版本中实现了这些特性,但是用户端浏览器的版本却需要相当长一段时间才能升上来,服务端的Node.js也不一定能及时升级。
但是新的规范标志着我们可以更放心的使用这些新特性,并且值得花时间去研究学习一下。
新特性一览
这次的新特性并不算多,也很好理解,分别列出如下:
- 新增Array.prototype.toSorted、TypedArray.prototype.toSorted
- 新增Array.prototype.toReversed、TypedArray.prototype.toReversed
- 新增Array.prototype.toSpliced
- 新增Array.prototype.with、TypedArray.prototype.with
- 新增Array.prototype.findLast、TypedArray.prototype.findLast
- 新增Array.prototype.findLastIndex、TypedArray.prototype.findLastIndex
- 新增hashbang(#!)注释支持
- Symbols可以作为weak集合的key
下面我们分别进行讲解。
immutable版本的数组方法
数组的大部分方法在对数组进行操作时,会修改原有数组的值,例如:
Array.prototype.sort
Array.prototype.reverse
Array.prototype.splice
这些方法都是直接修改原有数组,另外像sort
、reverse
还会将原数组作为返回值返回,这样新手很容易误认为返回的是一个新数组,而全然不知原数组已经被修改,从而很容易导致bug发生。
ES2023分别新增了这三个函数的immutable版本:
Array.prototype.toSorted
Array.prototype.toReversed
Array.prototype.toSpliced
它们的功能和sort
reverse
splice
一致,但是它们不会修改原有数组,而是另创建一个新数组。
我们用最简单的reverse
举例如下:
const sourceArray = ['a', 'b', 'c', 'd', 'e'];
// ['e', 'd', 'c', 'b', 'a']
console.log(sourceArray.reverse());
// 原数组被改变:['e', 'd', 'c', 'b', 'a']
console.log(sourceArray);
reverse
函数用于将数组反转,可以看到,reverse
返回值是一个反转后的数组,一起反转的还有原数组sourceArray
。
reverse
的immutable版本toReversed
新函数举例如下:
const sourceArray = ['a', 'b', 'c', 'd', 'e'];
// ['e', 'd', 'c', 'b', 'a']
console.log(sourceArray.toReversed());
// 原数组没有改变:['a', 'b', 'c', 'd', 'e']
console.log(sourceArray);
可以看到,经过toReversed
操作的数组sourceArray
没有变化,而是产生了一个新的被反转的数组。
这里有两点需要注意:
-
虽然会产生新数组,但是并不会进行深度复制,如果数组成员是一个引用,那新数组仅仅会对引用本身进行复制,而不会复制引用所指向的对象。 举例如下:
const sourceArray = ['a', 'b', 'c', 'd', ['x', 'y', 'z']]; var newArray = sourceArray.toReversed(); // 数组成员并不会深拷贝 newArray[0] == sourceArray[4] // true // 新数组 newArray != sourceArray // true
newArray
是新产生的数组,newArray != sourceArray
,但是newArray[0] == sourceArray[4]
。 因此虽然toReversed()
会产生一个新的数组,但是仅仅是浅拷贝,而不是深拷贝。 -
当对
Array
的子类的实例进行操作时,返回的数据类型永远是Array
,而不是子类型。 举例如下:class Users extends Array { isGood = true; } const users = new Users('a', 'b', 'c', 'd'); // true console.log(users instanceof Users); // true console.log(users.isGood == true) const reversedUsers = users.toReversed(); // false,新数组的类型不是扩展类型 console.log(reversedUsers instanceof Users); // true,新数组的类型是初始类型Array console.log(reversedUsers instanceof Array); // undefined,扩展属性丢失 console.log(reversedUsers.isGood);
可以看到,反转后产生的新数组
reversedUsers
的类型是初始类型Array
,而不是Users
,而且Users
的扩展属性isGood
也已丢失。也就是说如果你的数组是来自于Array
的扩展类型,并且有自定义的属性,那该数组被toReversed
反转后返回的新数组是不能访问扩展属性的。 这是一个容易让我们迷惑的地方。不过更容易迷惑的是,本批次的方法和之前的Array.prototype.slice
方法并不一致。slice
方法也会产生一个新的数组,自定义属性的值会丢失,但是新数组的类型并不会变,举例如下:class Users extends Array { isGood = true; } const users = new Users('a', 'b', 'c', 'd'); users.isGood = false; //true console.log(users.slice() instanceof Users); //true,新数组的扩展属性是被重新初始化的 console.log(users.slice().isGood);
可以看到
users.slice()
的返回值类型依然是Users
,isGood
虽然可以访问,但是原始数组的值已经丢失。
以上两点是我们使用这几个immutable版本的函数需要注意的。
关于这几个函数的更多参数细节可以参考MDN:
Array.prototype.toSorted
:
developer.mozilla.org/zh-CN/docs/…
Array.prototype.toReversed
:
developer.mozilla.org/zh-CN/docs/…
Array.prototype.toSpliced
: developer.mozilla.org/zh-CN/docs/…
另外除了Array
外,TypedArray
类型也添加了这三个方法,它们功能上和Array
的一致,TypedArray
类型有Int8Array
Uint8Array
等等。
如果我们客户端浏览器的版本不可控的话,那我们暂时还不能直接使用这几个函数,不过core-js已经提供了polyfill:github.com/zloirock/co… 只需要将core-js升级到最近的版本就可以使用了。 如果我们对客户端浏览器的版本可控,那我们只需要让客户端升级到最新版本的浏览器就可以了。除了IE,几大浏览器都已经支持了这些方法。
Array.prototype.with
Array.prototype.with
是arr[x] = xxx
的immutable版本,即set
操作的immutable版本。
例如,我们之前局部修改数组的内容是这样做:
const arr = ['a', 'b', 'c', 'd', 'e', 'f'];
arr[2] = 'xxxxx';
但是如果我们不想修改原数组,只想用新的值产生一个新的数组,可以这样:
const arr = ['a', 'b', 'c', 'd', 'e', 'f'];
// 产生新数组
const newArr = arr.with(2, 'xxxxx');
// 新数组和原数组不是同一个数组
console.log(newArr != arr); // true
// ['a', 'b', 'c', 'd', 'e', 'f'],新数组的数据不会改变
console.log(arr);
// ['a', 'b', 'xxxxx', 'd', 'e', 'f']
console.log(newArr);
另外值得注意的是,对于Array的扩展类型,Array.prototype.toReversed
需要注意的那两点,在用Array.prototype.with
时也一样需要注意。
关于Array.prototype.with
更多参数细节可以参考:developer.mozilla.org/zh-CN/docs/…
另外除了Array
以外,TypedArray
类型也添加了这个方法,它们功能上和Array
的一致,TypedArray
类型有Int8Array
Uint8Array
等等。
如果我们客户端浏览器的版本不可控的话,那我们暂时还不能直接使用这几个函数,不过core-js已经提供了polyfill:github.com/zloirock/co… 只需要将core-js升级到最近的版本就可以使用了。 如果我们对客户端浏览器的版本可控,那我们只需要让客户端升级到最新版本的浏览器就可以了。除了IE,几大浏览器都已经支持了这个方法。
数组倒查
所谓数组倒查,就是查找一个值时,是从数组的最后一位往前依次查找,也就是以index降序的顺序对数组元素进行查找。 这两个新的方法是:
Array.prototype.findLast(callback)
Array.prototype.findLastIndex(callback)
之前ES6版本规范推出两个方法:
Array.prototype.find
Array.prototype.findIndex
他们的用法和ES14的两个函数的用法一样,只是这find
和findIndex
是以index升序的形式对数组元素进行查找,也就是说从前往后查。
这两个新方法的具体用法可以参考MDN: developer.mozilla.org/zh-CN/docs/… developer.mozilla.org/zh-CN/docs/…
如果我们对客户端浏览器的版本不可控的话,那我们暂时还不能直接使用这两个函数,不过core-js已经提供了polyfill:github.com/zloirock/co… 只需要将core-js升级到最近的版本就可以使用了。 如果我们客户端浏览器的版本可控,那我们只需要让客户端升级到最新版本的浏览器就可以了。除了IE,几大浏览器都已经支持了这两个方法。
对hashbang#!的支持
hashbang是类Unix系统(GNU/Linux、BSD等)上用来给脚本文件指定解释器的。
例如,我在Linux机器上写了一段JavaScript,并保存成了文件“test”,如果我想执行这段JavaScript,我可能会在命令行进入“test”所在的目录,然后运行:node test
,这时shell就会用node
这个可执行程序去执行“test”中的代码。这种方式要求程序执行者必须知道“test”是一段JavaScript,并且知道如何调用node
可执行程序。
如果“test”文件的使用者在不知道其类型的情况下,也想执行“test”的话,类Unix系统提供了另外一种方式:./test
,./
代表当前路径,也就是说只要指定“test”路径就可以直接执行“test”。
但是这种方式下shell如何知道要用那个解释器来执行“test”中的代码呢?这时就需要hashbang:#!
。
在“test”文件的第一行写上:
#!这里写上解释器程序的位置
这样shell就知道用哪个解释器程序来执行下面的代码了。
例如我们要执行“test”程序,那可以这么写:
#!/usr/bin/node
//以下是test的示例代码
const x = 1;
console.log('x', x);
“test”文件的第一行指定了解释器程序的位置。
如果不想写死解释器程序的位置,我们也可以用env
指令:
#!/usr/bin/env node
env
指令可以用来定位node
的位置。
以上是对hashbang的讲解。 hashbang也叫shebang,hash的意思是“#”,bang的意思是“!”。 shebang也是一样,she来自于单词“sharp”,代表音符“#”,我们中文可以这样读它:“西班”。
很显然#!
不符合JavaScript语法,直接放到JavaScript代码中是会报错的。但是早在几年前,Node.js为了大家能够方便的使用hashbang,就加了一个自动去除hashbang的功能,这样在执行JavaScript时就不会报错了。因此JavaScript开发者其实早就可以直接使用hashbang了,它已经是一个既成事实了。
这次TC39将hashbang放到JavaScript标准中,JavaScript会自动忽略首行的这段字符。Hashbang成了JavaScript语言官方支持的功能。这让JavaScript成为广泛使用的服务器端脚本语言的可能性又更进了一步。
Symbols可以作为weak集合的key
这里所说的weak
集合是指WeakMap
WeakSet
,其实还有还有一个WeakRef
,前面两个是弱引用集合,最后这个是单个弱引用。
所谓weak
就是指弱引用,弱引用意思就是垃圾回收器在判断是否应该回收某对象内存时,不会考虑的一类特殊引用,也就是说如果某个对象仅仅被这类特殊引用关联时,垃圾回收器会把该对象当作可回收的对象。 由WeakMap
或者WeakSet
或者WeakRef
之类数据结构中保存的数据都采取这类特殊引用形式。
这个可以很好的帮助我们防止内存泄漏。
我们拿WeakMap
举例:
const map = new Map();
(() ={
let o = {x: 1, y: 2};
map.set(o, 1);
})()
// 虽然匿名函数已经运行结束,但是局部对象o的内容依然会被保留,因为它被外部的map引用
const weakMap = new WeakMap();
(() ={
let o = {x: 1, y: 2};
weakMap.set(o, 1);
})()
// 虽然weakMap引用了o对象的内容,但是o对象所占用的内存依然是待回收状态,因为WeakMap的key是弱引用
这里需要注意的是,WeakMap
只有key
是弱引用,value
并不是,value
是正常的引用,也就是说,只要key
和WeakMap
还存在,那value
是绝对不会被垃圾回收的。
以上就是对弱引用的介绍。下面咱们看一下这次新加的特新。
之前的弱引用是不允许对Symbol
类型进行引用的,也就是说,如果这样做会报错:
const weakMap = new WeakMap();
// 之前的版本会报错:Invalid value used as weak map key
weakMap.set(Symbol(), 999);
这个版本开始,Symbol
类型的值也可以作为弱引用了。Symbol
类型的特点是它可以保证其值的唯一性。
例如:
Symbol('aaa') != Symbol('aaa')
两个Symbol的创建方式一样,但是却是不相等的。
这里需要注意的是,并不是所有的Symbol
类型都可以作为弱引用,只有"non-registered"类型的Symbol
才可以作为弱引用。
咱先不看"non-registered"是什么,我们只要简单的记住:用Symbol.for()
创建的Symbol
是不可以作为弱引用的。
以下代码会报错:
const weakMap = new WeakMap();
// 报错:Uncaught TypeError: Invalid value used as weak map key
weakMap.set(Symbol.for('aaa'), 999);
其它类型的Symbol
都是可以的,例如:
const weakMap = new WeakMap();
weakMap.set(Symbol('aaa'), 999);
weakMap.set(Symbol.iterator, 888);
下面咱们来看一下,到底什么是"non-registered"类型的Symbol
。
我们可以先研究一下"registered"类型的Symbol
,这之外的就是“non-registered”类型了。
所谓“registered”类型,可以想象成在JavaScript内部有这样一个全局注册表,这种类型的“Symbol”会被记录在注册表中,这种类型Symbol
的创建方法是:Symbol.for()
,举例如下:
// 先查找全局注册表,如果当前没有aaa标识的Symbol,那就创建一个
const s = Symbol.for('aaa');
// 先查找全局注册表,如果已经有了aaa标识的Symbol,那就返回已有的
const n = Symbol.for('aaa');
// true
console.log(s == n);
可以看到Symbol.for('aaa')
创建的Symbol
还可以通过Symbol.for('aaa')
找回,随时可以被找回,所以这种Symbol
也是没有办法被垃圾回收的,哪怕已经没有变量在引用它,因为Symbol.for()
随时有可能被用来将已经创建的Symbol
找回。
这就是“registered”类型的Symbol
。
那“non-registered”类型的Symbol
就是指不是用Symbol.for
创建的Symbol
。
一共有两种方式:
Symbol()
,这样创建的Symbol
的特点是永远也不会和其它Symbol
重复。- 还有一种
Symbol
是语言自带的,例如:Symbol.iterator
。
因此总结一下,下面这两种方式创建的Symbol
是“non-registered”类型的Symbol
,它们可以作为弱引用:
Symbol()
Symbol.iterator
其实Symbol.for()
是否可以作为弱引用,在标准出来之前,社区的看法不一,争论很激烈。但是有一个最关键的问题是,Symbol.for()
创建的Symbol
是不能被垃圾回收的,它会一直存在,那这样的话弱引用也就没有意义了。另外Symbol.for()
重复执行时,可以将已有的Symbol
拉回,而Symbol()
和内置的Symbol
(Symbol.iterator
)都可以保证不可重建,具有唯一性。最终综合考量,Symbol.for()
这种形式被排除在外了。
以上就是ES2023(ES14、ECMAScript 14)的所有新特性了。