龙空技术网

未来的JavaScript还缺少什么?

水落 94

前言:

当前兄弟们对“js过时了吗”可能比较关切,我们都想要分析一些“js过时了吗”的相关知识。那么小编也在网摘上搜集了一些关于“js过时了吗””的相关资讯,希望兄弟们能喜欢,姐妹们一起来了解一下吧!

原文:

近年来,JavaScript的规模已经有很大的增长,这篇博客探讨了它仍然缺少的内容。

注意:

我仅仅列出那些我认为最重要的特性。许多其他的可能很有用,但是也存在添加太多的风险。这些选择带有主观意愿。这篇博客提到的几乎所有内容都受到TC39的关注。也就是说,它还可以作为将来可能使用的JavaScript预览。一、值按值比较对象

目前,JavaScript仅按值比较原始值,例如字符串--通过查看它们的内容。

> 'abc' === 'abc'true

相比之下,对象是按地址引用来比较的(每一个对象有一个唯一的地址,并且它仅仅和它自己相等)。

> {x: 1, y: 4} === {x: 1, y: 4}false> ['a', 'b'] === ['a', 'b']false> const obj = {x: 1, y: 4};> obj === objtrue

如果有一种方法可以创建按值比较的对象,那就太好了:

> #{x: 1, y: 4} === #{x: 1, y: 4}true> #['a', 'b'] === #['a', 'b']true

另外一种可能性是引入一种新的类(具体细节待定):

@[ValueType]class Point {  // ···}

此外:这种类装饰器的语法,将类标记为值类型,也是基于一个draft proposal(提议草案)。

2、将对象放入数据结构

由于对象是按引用比较的,将他们放入(非弱引用)ECMAScript的数据结构(例如Map)中几乎没有任何意义。

const m = new Map();m.set({x: 1, y: 4}, 1);m.set({x: 1, y: 4}, 2);assert.equal(m.size, 2);

此问题可以通过自定义值类型解决。另外,Set元素和Map键的管理可以自定义,例如:

通过Hash Table管理的Map:检查是否相等需要一项操作,创建hash码也需要一项单独的操作。如果你使用的是hash码管理的Map,你应该是希望你的对象是不可变的,否则它太容易破坏数据结构了。通过排序树管理的Map:为了管理它存储的值,它需要一个比较两个值的操作。

3、大整数(ECMAScript2020)

JavaScript的数字总是64位双精度,这个最大只能给你53位带符号的整数。总之,你不能任意的表达任意整数。

> 2 ** 539007199254740992> (2 ** 53) + 1  // can’t be represented9007199254740992> (2 ** 53) + 29007199254740994

在某些情况下,它是一个比较大的限制。对于BigInts目前有个提案(ES2020已纳入标准),精度根据需要增长的实整数。

> 2n ** 53n9007199254740992n> (2n ** 53n) + 1n9007199254740993n

BigInts还支持强制转换,从而为您提供具有固定位数的值。

const int64a = BigInt.asUintN(64, 12345n);const int64b = BigInt.asUintN(64, 67890n);const result = BigInt.asUintN(64, int64a * int64b);

4、小数计算

JavaScript的数字是64位的浮点数(双精度),基于IEEE754标准。在底层,它是基于二进制表示的,因此当你处理小数的时候,总是会出现舍入错误:

> 0.1 + 0.20.30000000000000004

在科学计算和金融技术(一个快速增长的领域)中,这尤其是个问题。一个基于10进制的数字提案目前在stage-0。它们最终可能会像下面这样调用(请注意后缀m为小数):

> 0.1m + 0.2m0.3m

5、值分类

当前,在JavaScript中归类值是相当笨重的。

首先,你得决定是使用typeof还是instanceof第二,typeof有一个大家都知道的怪癖,就是把null归类为'object',并且还需要考虑它把函数归类为'function'这一怪癖。

> typeof null'object'> typeof function () {}'function'> typeof []'object'
第三,instanceof在其他上下文不一定能正常工作(例如frame)

这个可能会通过一个库来修复(一旦我有时间,我就会创建概念来证明)。

二、函数式编程

1、Do表达式

C风格的语言在表达式和语句之间做了不好的区分。

// Conditional expressionlet str1 = someBool ? 'yes' : 'no';// Conditional statementlet str2;if (someBool) {  str2 = 'yes';} else {  str2 = 'no';}

特别是在函数式语言中,所有的东西都是表达式。Do表达式能够让你在表达式上下文中使用语句。

let str = do {  if (someBool) {    'yes'  } else {    'no'  }};

`switch`同样在Do表达式中工作:

const n = 3;let str = do {  switch (n) {    case 1:      'one';      break;    case 2:      'two';      break;    case 3:      'three';      break;  }};assert.equal(str, 'three');

Do表达式有助于消除最后一个使用立即执行函数表达式的主要原因(IIFEs):在一个函数上绑定静态数据。

做一个示例,下面就是使用IIFE的代码:

const func = (() => { // open IIFE  let cache;  return () => {    if (cache === undefined) {      cache = compute();    }    return cache;  }})(); // close IIFE

通过Do表达式,你不再需要IIFE:

const func = do {  let cache;  () => {    if (cache === undefined) {      cache = compute();    }    return cache;  };};

2、匹配一个解构的`switch`

JavaScript中直接处理对象很容易。但是,没有基于对象结构的内置方式去匹配案例。如下所示(示例来源于提案):

const resource = await fetch(jsonService);case (resource) {  when {status: 200, headers: {'Content-Length': s}} -> {    console.log(`size is ${s}`);  }  when {status: 404} -> {    console.log('JSON not found');  }  when {status} if (status >= 400) -> {    throw new RequestError(res);  }}

正如你看到的那样,在某种程度上新的case语句有点和switch相同,但是使用解构匹配案例。每当使用嵌套数据结构(例如在编译器中),这种功能就非常有用。模式匹配的提案目前已经进入stage-1。

3、管道操作符

对于管道操作符,目前有两个相互竞争的提案。在这里,我们看一下`Smart`管道(另外一个称作F#管道)。

管道操作符的基本思想如下。考虑如下的嵌套函数调用。

const y = h(g(f(x)));

然而,这种表示法不能反映出我们对计算步骤的想法。直观的,我们将它描述为:

值x开始对x调用f对上一步的结果调用g对上一步的结果调用h把值赋值给y

这个管道操作符让我们能够很直观表达:

const y = x |> f |> g |> h;

换一种说法,下面两个表达式是等价的:

f(123)123 |> f

此外,这个管道操作符支持`partial application`(类似于函数的bind方法):下面两个表达式是等价的:

123 |> f('a', #, 'b')123 |> (x => f('a', x, 'b'))

管道操作符一个重要的好处是你可以像使用方法一样使用函数,不需要去改变任何原型:

import {filter, map} from 'array-tools';const result = arr  |> filter(#, x => x >= 0)  |> map(#, x => x * 2);
三、并发

JavaScript一直以来对并发的支持有限。并发进程事实上的标准是Worker Api,它在浏览器和NodeJs(在11.7版本之后不需要加flag)上都是可用的。

在NodeJs上使用它看起来像下面这样:

const {  Worker, isMainThread, parentPort, workerData} = require('worker_threads');if (isMainThread) {  const worker = new Worker(__filename, {    workerData: 'the-data.json'  });  worker.on('message', result => console.log(result));  worker.on('error', err => console.error(err));  worker.on('exit', code => {    if (code !== 0) {      console.error('ERROR: ' + code);    }  });} else {  const {readFileSync} = require('fs');  const fileName = workerData;  const text = readFileSync(fileName, {encoding: 'utf8'});  const json = JSON.parse(text);  parentPort.postMessage(json);}

很遗憾的是,Worker是相当的“重”,每一个都有自己的环境上下文(全局变量等等)。我希望在将来能够看到更轻巧的结构。

四、标准库

JavaScript一个明显落后于其他语言的地方是它的标准库。保持它体量较小确实有意义,因为外部库更加容易发展和适应,然而有少量的核心功能是很有用的。

1、模块代替对象命名空间

在JavaScript语言有模块功能之前,它的标准库就已经被创建了。因此,所有的功能方法都被放进了对象的命名空间中,例如Math、JSON、Object和Reflect。

Math.max()JSON.parse()Object.keys()Reflect.ownKeys()

如果这些功能能被放进模块中,那就太好了。它们必须通过一个特殊的URLs去访问,例如使用伪协议`std:`

// New:import {max} from 'std:math';assert.equal(  max(-1, 5), 5);// Old:assert.equal(  Math.max(-1, 5), 5);

优点:

JavaScript将会变得更加模块化(这个将会加快启动时间和减少内存消耗)。比起调用存储在对象上的方法,调用通过模块导入的方法速度更快。

2、更加有助于可迭代对象(同步和异步)

迭代器的优势在于按需求值和支持大量数据源。但是JavaScript当前只提供了很少的工具用于迭代器。例如,如果你想filter、map、reduce一个迭代器,你必须把它转换为列表:

const iterable = new Set([-1, 0, -2, 3]);const filteredArray = [...iterable].filter(x => x >= 0);assert.deepEqual(filteredArray, [0, 3]);

如果JavaScript有针对于迭代器的工具函数,你就可以直接filter迭代对象:

const filteredIterable = filter(iterable, x => x >= 0);assert.deepEqual(  // Only convert iterable to Array to check what’s in it  [...filteredIterable], [0, 3]);

这里有少量针对迭代器的示例:

// Count elements in an iterableassert.equal(count(iterable), 4);// Create an iterable over a part of an existing iterableassert.deepEqual(  [...slice(iterable, 2)],  [-1, 0]);// Number the elements of an iterable// (producing another – possibly infinite – iterable)for (const [i,x] of zip(range(0), iterable)) {  console.log(i, x);}// Output:// 0, -1// 1, 0// 2, -2// 3, 3

注意:

有关迭代器工具函数的示例,请查阅Python的itertools。对于JavaScript,任何针对可迭代对象的工具函数应该有两个版本:一个支持同步迭代,同一个支持异步迭代。

3、不可变数据

如果对无损转换数据有着更多的支持,那将会非常nice。如下是两个相关的库:

Immer是一个相对较轻的库,并且很好的与正常对象和数组协调工作。Immutable.js 是一个更加重量级并且功能更加丰富,且带有自己的数据结构。

4、对日期时间更好的支持

JavaScript内置的日期时间支持有很多怪癖。这就是为什么除了最基本的功能外都推荐使用第三方库。

谢天谢地,一个更好的日期时间API正在到来:

const dateTime = new CivilDateTime(2000, 12, 31, 23, 59);const instantInChicago = dateTime.withZone('America/Chicago');
五、可能不需要的功能

1、可选链的优缺点

可选链是一个非常受欢迎的功能提案。下面那两个表达式是等价的:

obj?.prop(obj === undefined || obj === null) ? undefined : obj.prop

这个功能对于链式属性读取非常方便:

obj?.foo?.bar?.baz

但是这个功能也有它的缺点:

深度嵌套的结构更难管理。例如,如果有许多的属性名称序列,则重构将会更加困难。每个 属性都会限制多个对象的结构。在访问数据时如此宽容容易隐藏问题,并且这个问题出现的很慢,而且很难调试。例如,在一系列可选属性的名称中,早期的拼写错误比正常的方式取值的拼写错误更加不容易被发现。

一个可替代可选链的方式是在一个地方一次性提取信息:

您可以编写一个辅助函数来提取数据或者你能够写一个函数,它的输入是深层次嵌套的数据并且它的输出是简单的、正常化的数据。

通过以上任意一个方法,都可以执行检查,并且在出现问题的时候尽早失败。

2、我们需要运算符重载吗?

对于运算符重载,一些早期工作正在进行。但可能infix函数应用就已经足够了(尽管目前没有关于它的提案)。

import {BigDecimal, plus} from 'big-decimal';const bd1 = new BigDecimal('0.1');const bd2 = new BigDecimal('0.2');const bd3 = bd1 @plus bd2; // plus(bd1, bd2)

infix函数应用的好处:

你可以创建不被内置支持的JavaScript操作符与普通函数相比,嵌套表达式仍然具有很高的可读性

下面是一个嵌套表达式的例子:

a @​plus b @​minus c @​times dtimes(minus(plus(a, b), c), d)

有趣的是,管道操作符也帮助这个具有很高的可读性

plus(a, b)  |> minus(#, c)  |> times(#, d)
六、其他小功能

这里还有些许我遗漏的功能点,但是我认为它们是不如我之前提到的那些重要。

链式异常:允许你捕获一个异常,并且可以包装一些额外的信息,然后再次抛出它。

new ChainedError(msg, origError);
可组合的正则表达式:
const regex = re`/^${RE_YEAR}-${RE_MONTH}-${RE_DAY}$/u`;
转译正则表达式的文本(对.replace是很重要的)
> const re = new RegExp(RegExp.escape(':-)'), 'ug');> ':-) :-) :-)'.replace(re, '')'  '
Array.prototype.get支持负数索引
> ['a', 'b'].get(-1)> 'b'
用于匹配和解构的模式
function f(...[x, y] as args) {  if (args.length !== 2) {    throw new Error();  }  // ···}
检查对象的深度相等(可能可选择使用第二个参数式的形式以支持自定义的数据结构)
assert.equal(  {foo: ['a', 'b']} === {foo: ['a', 'b']},  false);assert.equal(  deepEqual({foo: ['a', 'b']}, {foo: ['a', 'b']}),  true);
枚举:给JavaScript添加枚举类型的一个好处就是减少了与TypeScript(已经有枚举类型了)的差异。当前已经有两个草案(还未处于正式阶段)。在这两个草案中,简单的语法都类似下面这样:
enum WeekendDay {  Saturday, Sunday}const day = WeekendDay.Sunday;
标记集合类型(由KatMarchán提出和撤销)。允许你像下面这样使用Set和Map:
const myMap = Map!{1: 2, three: 4, [[5]]: 6}  // new Map([1,2], ['three',4], [[5],6])const mySet = Set!['a', 'b', 'c'];  // new Set(['a', 'b', 'c'])
七、FAQ:未来的JavaScriptJavaScript是否会支持静态类型?

短时间内是不会的。目前这种在开发期间进行类型检查(TypeScript,Flow),在运行期间是纯JavaScript的状况,协调得很好。因此目前没有任何理由去立即改变任何东西。

为什么我们不能通过删除怪癖和过时的功能来清理JavaScript?

web的一个关键要求是永远不要破坏向后兼容。

这个语言的缺点就是有许多传统的功能但利大于弊的点在于:大型代码库都是用的相同的特性。迁移到新版本是比较简单的;引擎仍然保持的比较小巧(无需支持多个版本)等等。

通过引入现有功能的更好版本,仍然可能修复一些错误。

八、思考语言设计

作为一个语言设计者,不管你做什么,总有人开心,也总有人会难受。因此,设计未来JavaScript特性的挑战不是让所有人都高兴,而是去尽可能的保持语言的不变性。

然而,在"不变性"是什么的问题上依然存在着分歧。因此,我们能够做的最好的事就是建立一个不变的“风格”,由一小群人(最多三个)构思和执行。

这并没有排除他们会受到其他许多人们的建议和帮助,但他们应该保持一致的风格。

引用弗雷德·布鲁克斯的话:

稍作回顾可以看出,不管委员会设计多么美好,有用的软件系统,也不管它是否是大型项目的一部分。能激起粉丝热情的软件系统总是那些一个或几个设计师设计的产品。

这些核心设计者的一个重要的职责就是对新特性说"no",防止JavaScript变得太大。

他们也需要一个强壮的支持系统,作为语言设计者往往会遭受大量的辱骂(因为人们在意自己并且不喜欢被拒绝)。最近的一个例子是Guido van Rossum因受到谩骂而辞去了首席Python语言设计师的工作。

其他想法

这些想法可能也会帮助设计和记录JavaScript:

创建一个前景图,用来描述JavaScript的未来。这样的前景图不仅可以讲述故事,还可以将许多不同的部分连接成一个连贯的整体。我知道的最后一个前景图是,Brendan Eich的“Harmony Of My Dreams”。记录设计原理。现在的ECMAScript规范记录了它们会怎么工作,但是却没有讲述为什么。一个例子就是,可枚举的目的是什么?一个权威的解释。这一半的正式规范其实是已经实现了。如果他们能够像编程语言那样直接运行那就非常棒。(你可能需要一个规范,去使得可以从非规范的帮助函数中把规范代码区分开来。)

标签: #js过时了吗