JavaScript的未来:还缺少什么?
近年来,JavaScript的规模已经大大增加。这篇博客文章探讨了仍然缺失的内容。
注意:
- 我只列出了我发现最重要的缺失功能。许多其他的都很有用,但也有增加太多的风险。
- 我的选择是主观的。
- 本博客文章中提及的几乎所有内容都在TC39的雷达上。也就是说,它还可以作为未来可能的JavaScript的预览。
有关前两个问题的更多想法,请参阅语言设计部分。
1. 数值
1.1 按值比较对象
目前,JavaScript只比较原始值,例如字符串值(通过查看其内容):
> 'abc' === 'abc'
true
相反,对象比较是通过引用地址进行判断的(对象仅严格等于自身,即指向的是同一个对象):
> {x: 1, y: 4} === {x: 1, y: 4}
false
如果有一种方法可以创建按值进行比较的对象,那将是很好的:
> #{x: 1, y: 4} === #{x: 1, y: 4}
true
另一种可能性是引入一种新的类(待讨论):
@[ValueType]
class Point {
// ···
}
注:将类标记为值类型的类似装饰器的语法基于草案提案。
1.2 将对象放入数据结构
当对象通过引用地址进行比较时,将它们放入(非弱)ECMAScript数据结构(如Maps)中很少有意义:
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键的管理。例如:
-
通过哈希表映射:需要一个操作来检查相等性,另一个操作用于创建哈希码。如果使用哈希码,则希望对象是不可变的。否则,破解数据结构就太容易了。
-
通过排序树映射:需要一个比较两个值的操作,以管理它存储的值。
1.3 超大整数
JavaScript数字总是64位(双精度),它为整数提供53位加号。这意味着超过53位,您不能再代表每个数字了:
> 2 ** 53
9007199254740992
> (2 ** 53) + 1 // can’t be represented
9007199254740992
> (2 ** 53) + 2
9007199254740994
对于某些用例,这是一个相当大的限制。现在有一个关于BigInts的提议,即实数整数,其精度随着需要的增长而增长:
> 2n ** 53n
9007199254740992n
> (2n ** 53n) + 1n
9007199254740993n
BigInts还支持强制转换,它为您提供固定位数的值:
const int64a = BigInt.asUintN(64, 12345n);
const int64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, int64a * int64b);
1.4 十进制计算
JavaScript的数字是基于IEEE 754标准的64位浮点数(双精度数)。鉴于它们的表示形式是base-2,在处理小数分数时可能会出现舍入误差:
> 0.1 + 0.2
0.30000000000000004
这在科学计算和金融技术(金融科技)中尤其成问题。基数为10的提案目前处于第0阶段。它们可能最终被这样使用(注意m
十进制数字的后缀):
> 0.1m + 0.2m
0.3m
1.5 值类型
目前,在JavaScript中对值进行分类非常麻烦:
- 首先,你必须决定是否使用
typeof
或instanceof
。 - 其次,null使用
typeof时值类型为
'object',我还认为
function的值类型'function'
。> typeof null 'object' > typeof function () {} 'function' > typeof [] 'object'
- 第三,
instanceof
不适用于来自其他领域(框架等)的对象。
有可能通过库来解决这个问题(一旦我有时间,我就会创建一个概念证明)。
2. 函数编程
2.1 更多表达式
C风格的语言在表达式和语句之间做出了区分:
// Conditional expression
let str1 = someBool ? 'yes' : 'no';
// Conditional statement
let str2;
if (someBool) {
str2 = 'yes';
} else {
str2 = 'no';
}
特别是在函数式语言中,一切都是表达式。Do表达式允许您在所有表达式上下文中使用语句:
let str3 = do {
if (someBool) {
'yes'
} else {
'no'
}
};
以下代码是一个更现实的例子。如果没有do-expression,则需要立即调用箭头函数来隐藏result
范围内的变量:
const func = (() => {
let result; // cache
return () => {
if (result === undefined) {
result = someComputation();
}
return result;
}
})();
使用do-expression,您可以更优雅地编写此代码:
const func = do {
let result;
() => {
if (result === undefined) {
result = someComputation();
}
return result;
};
};
2.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
在某些方面类似,但使用解构来挑选案例。当人们使用嵌套数据结构(例如在编译器中)时,这种功能非常有用。模式匹配的提议目前处于第1阶段。
2.3 Pipeline运算
目前有两个针对pipeline运算的竞争提案。在这里,我们正在研究智能pipeline(另一个提议称为F#管道)。
pipeline运算的基本思想如下。请考虑以下嵌套函数调用。
const y = h(g(f(x)));
但是,这种表示法通常不反映我们对计算步骤的看法。直觉上,我们将它们描述为:
- 从值开始
x
。 - 然后申请
f()
。 - 然后应用于
g()
结果。 - 然后应用于
h()
结果。 - 然后将结果分配给
y
。
pipeline运算符让我们更好地表达这种直觉:
const y = x |> f |> g |> h;
换句话说,以下两个表达式是等价的。
f(123)
123 |> f
此外,pipeline运算符支持部分应用程序(类似于.bind()
函数方法):以下两个表达式是等效的。
123 |> f(#)
123 |> (x => f(x))
pipeline运算符的一个重要好处是,您可以像使用方法一样使用函数 - 而无需更改任何原型:
import {map} from 'array-tools';
const result = arr |> map(#, x => x * 2);
最后,让我们看一个更长的例子(取自提案并稍作编辑):
promise
|> await #
|> # || throw new TypeError(
`Invalid value from ${promise}`)
|> capitalize // function call
|> # + '!'
|> new User.Message(#)
|> await stream.write(#)
|> console.log // method call
;
3. 并发
JavaScript一直对并发性的支持有限。并发进程的事实标准是Worker API,它可以在Web浏览器和Node.js中使用(在v11.7及更高版本中没有标志)。
从Node.js中使用它如下所示。
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);
}
唉,工人是相对重量级的 - 每个人都有自己的领域(全球变量等)。我想在未来看到一个更轻量级的构造。
4. 标准库
JavaScript仍然明显落后于其他语言的一个领域是其标准库。保持最小化是有意义的,因为外部库更容易进化和适应。但是,有一些核心功能是有用的。
4.1 模块而不是命名空间对象
JavaScript的标准库是在语言具有模块之前创建的。因此,职能放在命名空间中的物体,如Object
,Reflect
,Math
和JSON
:
Object.keys()
Reflect.ownKeys()
Math.sign()
JSON.parse()
如果将这个功能放在模块中会很棒。它必须通过特殊URL访问,例如使用伪协议std
:
// Old:
assert.deepEqual(
Object.keys({a: 1, b: 2}),
['a', 'b']);
// New:
import {keys} from 'std:object';
assert.deepEqual(
keys({a: 1, b: 2}),
['a', 'b']);
好处是:
- JavaScript将变得更加模块化(这可以加快启动时间并减少内存消耗)。
- 调用导入的函数比调用存储在对象中的函数更快。
4.2 迭代的助手(同步和异步)
迭代的好处包括按需计算值和支持许多数据源。但是,JavaScript目前只提供了很少的工具来处理iterables。例如,如果要过滤,映射或减少可迭代,则必须将其转换为数组:
const iterable = new Set([-1, 0, -2, 3]);
const filteredArray = [...iterable].filter(x => x >= 0);
assert.deepEqual(filteredArray, [0, 3]);
如果JavaScript具有可迭代的工具函数,您可以直接过滤迭代:
const filteredIterable = filter(iterable, x => x >= 0);
assert.deepEqual(
// We only convert the iterable to an Array, so we can
// check what’s in it:
[...filteredIterable], [0, 3]);
这些是迭代的工具函数的一些示例:
// Count elements in an iterable
assert.equal(count(iterable), 4);
// Create an iterable over a part of an existing iterable
assert.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
注意:
4.3 不可变数据
对非破坏性转换数据有更多支持会很高兴。两个相关的库是:
- Immer相对轻量级,适用于普通对象和数组。
- Immutable.js更强大,更重量级,并拥有自己的数据结构。
4.4 更好地支持日期时间
JavaScript对日期时间的内置支持有许多弊端。这就是为什么目前的建议是使用库来完成除最基本任务之外的所有工作。
值得庆幸的是,temporal
正在进行更好的日期时间API,正在进行中:
const dateTime = new CivilDateTime(2000, 12, 31, 23, 59);
const instantInChicago = dateTime.withZone('America/Chicago');
5. 可能不需要的特性
5.1可选链的优缺点
一个相对流行的提议特性是可选链。以下两个表达式是等效的。
obj?.prop
(obj === undefined || obj === null) ? undefined : obj.prop
此功能对于属性链特别方便:
obj?.foo?.bar?.baz
但是,此功能有缺点:
- 深层嵌套的结构更难管理。
- 在访问数据时如此宽容隐藏了稍后会出现的问题,然后更难以调试。
可选链接的替代方法是在单个位置提取一次信息:
- 您可以编写一个提取数据的辅助函数。
- 或者您可以编写一个函数,其输入是深度嵌套数据,其输出更简单,标准化数据。
无论采用哪种方法,都可以执行检查并在出现问题时尽早失败。
进一步阅读:
5.2 需要重载操作吗?
目前的重载操作是早期工作,但是中缀功能应用程序可能就足够了(尽管目前还没有提议):
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)
中缀功能应用的好处是:
- 您可以创建除JavaScript已支持的运算符之外的运算符。
- 与普通函数应用程序相比,嵌套表达式仍然可读。
这是嵌套表达式的示例:
a @plus b @minus c @times d
times(minus(plus(a, b), c), d)
有趣的是,pipeline操作者还有助于提高可读性:
plus(a, b)
|> minus(#, c)
|> times(#, d)
6. 各种小事
这些是我偶尔会遗漏的一些事情,但我认为不像我之前提到的那样重要:
-
链式异常:使您能够捕获错误,在其周围包含其他信息并再次抛出它。
new ChainedError(msg, origError)
-
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'
-
匹配和解构的模式(KatMarchán提议):
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的差距 - 已经有枚举。目前有两份提案草案(尚未处于正式阶段)。一个是Rick Waldron,另一个是Ron Buckton。在两个提案中,最简单的语法如下所示:
enum WeekendDay { Saturday, Sunday } const day = WeekendDay.Sunday;
-
标记的集合文字(由KatMayán提议 - 并撤回):允许您创建地图和集合,如下所示:
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'])
7. 常见问题:未来的JavaScript
7.1 JavaScript会不会支持静态类型?
不是很快!开发时静态类型(通过TypeScript或Flow)和运行时纯JavaScript之间的当前分离效果很好。所以没有直接的理由改变任何事情。
7.2 为什么我们不能通过删除缺陷和过时的功能来清理JavaScript?
Web的一个关键要求是永远不会破坏向后兼容性:
- 缺点是该语言有许多遗留功能。
- 但好处超过了这个缺点:大型代码库仍然是同质的; 迁移到新版本很简单; 引擎仍然较小(不需要支持多个版本); 等等
通过引入现有功能的更好版本,仍然可以修复一些错误。
有关此主题的更多信息,请参阅“ 针对不耐烦的程序员的JavaScript ”。
8. 关于语言设计的思考
作为一名语言设计师,无论你做什么,你总会让一些人开心,有些人会伤心。因此,设计未来JavaScript功能的主要挑战不是让每个人都满意,而是让语言尽可能保持一致。
但是,对于“一致”的含义,也存在分歧。因此,我们可以做的最好的事情就是建立一致的“风格”,由一小群人(最多三人)构思和执行。这并不排除他们被许多其他人建议和帮助,但他们应该设定一般基调。
引用弗雷德布鲁克斯:
稍微回顾一下,尽管许多优秀,有用的软件系统都是由委员会设计的,并且是作为多部分项目的一部分而构建的,那些拥有激情的粉丝的软件系统是那些由一个或几个设计思想的产品,优秀的设计师。
这些核心设计师的一个重要职责是对功能说“不”,以防止JavaScript变得太大。
他们还需要一个强大的支持系统,因为语言设计者往往会遭受相当大的滥用(因为人们关心并且不喜欢听到“不”)。最近的一个例子是Guido van Rossum辞去了他作为首席Python语言设计师的工作,因为他受到了虐待。
8.1 其他想法
这些想法也可能有助于设计和记录JavaScript:
-
创建路线图,描述JavaScript的未来前景。这样的路线图可以讲述故事并将许多单独的部分连接成一个连贯的整体。我所知道的最后一个这样的路线图是Brendan Eich的“ 我的梦想的和谐 ”。
-
记录设计理念。现在,ECMAScript规范记录了工作原理,但不是原因。一个例子:可枚举性的目的是什么?
-
一个规范的翻译。规范的半正式部分几乎已经可执行。如果能够像编程语言一样对待和运行它们会很棒。(您可能需要一个约定来区分规范代码和非规范辅助函数。)
-
原文链接:http://2ality.com/2019/01/future-js.html