每日前端夜话0x28
每日前端夜话,陪你聊前端。
每天晚上18:00准时推送。
正文共:9219 字 1 图
预计阅读时间: 15 分钟
翻译:疯狂的技术宅 原文:http://2ality.com/2019/01/future-js.html
近年来,JavaScript 的功能得到了大幅度的增加,本文探讨了其仍然缺失的东西。
说明:
有关前两个问题的更多想法,请参阅本文第8节:语言设计部分。
目前,JavaScript 只能对原始值(value)进行比较,例如字符串的值(通过查看其内容):
1> 'abc' === 'abc'
2true
相反,对象则通过身份ID(identity)进行比较(对象仅严格等于自身):
1> {x: 1, y: 4} === {x: 1, y: 4}
2false
如果有一种能够创建按值进行比较对象的方法,那将是很不错的:
1> #{x: 1, y: 4} === #{x: 1, y: 4}
2true
另一种可能性是引入一种新的类(确切的细节还有待确定):
1@[ValueType]
2class Point {
3 // ···
4}
旁注:这种类似装饰器的将类标记为值类型的的语法基于草案提案。
如果对象通过身份ID进行比较,将它们放入 ECMAScript 数据结构(如Maps)中并没有太大意义:
1const m = new Map();
2m.set({x: 1, y: 4}, 1);
3m.set({x: 1, y: 4}, 2);
4assert.equal(m.size, 2);
可以通过自定义值类型修复此问题。 或者通过自定义 Set 元素和 Map keys 的管理。 例如:
JavaScript 的数字总是64位的(双精度),它能为整数提供53位二进制宽度。这意味着如果超过53位,就不好使了:
1> 2 ** 53
29007199254740992
3> (2 ** 53) + 1 // can’t be represented
49007199254740992
5> (2 ** 53) + 2
69007199254740994
对于某些场景,这是一个相当大的限制。现在有[BigInts提案](http://2ality.com/2017/03/es-integer.html),这是真正的整数,其精度可以随着需要而增长:
1> 2n ** 53n
29007199254740992n
3> (2n ** 53n) + 1n
49007199254740993n
BigInts还支持 casting,它为你提供固定位数的值:
1const int64a = BigInt.asUintN(64, 12345n);
2const int64b = BigInt.asUintN(64, 67890n);
3const result = BigInt.asUintN(64, int64a * int64b);
JavaScript 的数字是基于 IEEE 754 标准的64位浮点数(双精度数)。鉴于它们的表示形式是基于二进制的,在处理小数部分时可能会出现舍入误差:
1> 0.1 + 0.2
20.30000000000000004
这在科学计算和金融技术(金融科技)中尤其成问题。基于十进制运算的提案目前处于阶段0。它们可能最终被这样使用(注意十进制数的后缀 m
):
1> 0.1m + 0.2m
20.3m
目前,在 JavaScript 中对值进行分类非常麻烦:
typeof
或 instanceof
。typeof
有一个众所周知的的怪癖,就是把 null
归类为“对象”。我还认为函数被归类为 'function'
同样是奇怪的。1> typeof null
2'object'
3> typeof function () {}
4'function'
5> typeof []
6'object'
instanceof
不适用于来自其他realm(框架等)的对象。也许可能通过库来解决这个问题(如果我有时间,就会实现一个概念性的验证)。
不幸的是C风格的语言在表达式和语句之间做出了区分:
1// 条件表达式
2let str1 = someBool ? 'yes' : 'no';
3
4// 条件声明
5let str2;
6if (someBool) {
7 str2 = 'yes';
8} else {
9 str2 = 'no';
10}
特别是在函数式语言中,一切都是表达式。 Do-expressions 允许你在所有表达式上下文中使用语句:
1let str3 = do {
2 if (someBool) {
3 'yes'
4 } else {
5 'no'
6 }
7};
下面的代码是一个更加现实的例子。如果没有 do-expression,你需要一个立即调用的箭头函数来隐藏范围内的变量 result
:
1const func = (() => {
2 let result; // cache
3 return () => {
4 if (result === undefined) {
5 result = someComputation();
6 }
7 return result;
8 }
9})();
使用 do-expression,你可以更优雅地编写这段代码:
1const func = do {
2 let result;
3 () => {
4 if (result === undefined) {
5 result = someComputation();
6 }
7 return result;
8 };
9};
JavaScript 使直接使用对象变得容易。但是根据对象的结构,没有内置的切换 case 分支的方法。看起来是这样的(来自提案的例子):
1const resource = await fetch(jsonService);
2case (resource) {
3 when {status: 200, headers: {'Content-Length': s}} -> {
4 console.log(`size is ${s}`);
5 }
6 when {status: 404} -> {
7 console.log('JSON not found');
8 }
9 when {status} if (status >= 400) -> {
10 throw new RequestError(res);
11 }
12}
正如你所看到的那样,新的 case
语句在某些方面类似于 switch
,不过它使用解构来挑选分支。当人们使用嵌套数据结构(例如在编译器中)时,这种功能非常有用。 模式匹配提案【https://github.com/tc39/proposal-pattern-matching】目前处于第1阶段。
管道操作目前有两个竞争提案 。在本文,我们研究其中的 智能管道(另一个提议被称为 F# Pipelines)。
管道操作的基本思想如下。请考虑代码中的嵌套函数调用。
1const y = h(g(f(x)));
但是,这种表示方法通常不能体现我们对计算步骤的看法。在直觉上,我们将它们描述为:
x
开始。f()
作用在 x
上。g()
作用于结果。h()
应用于结果。y
。管道运算符能让我们更好地表达这种直觉:
1const y = x |> f |> g |> h;
换句话说,以下两个表达式是等价的。
1f(123)
2123 |> f
另外,管道运算符支持部分应用程序(类似函数的 .bind()
方法):以下两个表达式是等价的。
1123 |> f(#)
2123 |> (x => f(x))
使用管道运算符一个最大的好处是,你可以像使用方法一样使用函数——而无需更改任何原型:
1import {map} from 'array-tools';
2const result = arr |> map(#, x => x * 2);
最后,让我们看一个长一点的例子(取自提案并稍作编辑):
1promise
2|> await #
3|> # || throw new TypeError(
4 `Invalid value from ${promise}`)
5|> capitalize // function call
6|> # + '!'
7|> new User.Message(#)
8|> await stream.write(#)
9|> console.log // method call
10;
一直以来 JavaScript 对并发性的支持很有限。并发进程的事实标准是 Worker API,可以在 web browsers 和 Node.js (在 v11.7 及更高版本中没有标记)中找到。
在Node.js中的使用方法它如下所示:
1const {
2 Worker, isMainThread, parentPort, workerData
3} = require('worker_threads');
4
5if (isMainThread) {
6 const worker = new Worker(__filename, {
7 workerData: 'the-data.json'
8 });
9 worker.on('message', result => console.log(result));
10 worker.on('error', err => console.error(err));
11 worker.on('exit', code => {
12 if (code !== 0) {
13 console.error('ERROR: ' + code);
14 }
15 });
16} else {
17 const {readFileSync} = require('fs');
18 const fileName = workerData;
19 const text = readFileSync(fileName, {encoding: 'utf8'});
20 const json = JSON.parse(text);
21 parentPort.postMessage(json);
22}
唉,相对来说 Workers 是重量级的 —— 每个都有自己的 realm(全局变量等)。我想在未来看到一个更加轻量级的构造。
JavaScript 仍然明显落后于其他语言的一个领域是它的标准库。当然保持最小化是有意义的,因为外部库更容易进化和适应。但是有一些核心功能也是有必要的。
JavaScript 标准库是在其语言具有模块之前创建的。因此函数被放在命名空间对象中,例如Object
,Reflect
,Math
和JSON
:
Object.keys()
Reflect.ownKeys()
Math.sign()
JSON.parse()
如果将这个功能放在模块中会更好。它必须通过特殊的URL访问,例如使用伪协议 std
:
1// Old:
2assert.deepEqual(
3 Object.keys({a: 1, b: 2}),
4 ['a', 'b']);
5
6// New:
7import {keys} from 'std:object';
8assert.deepEqual(
9 keys({a: 1, b: 2}),
10 ['a', 'b']);
好处是:
迭代 的好处包括按需计算和支持许多数据源。但是目前 JavaScript 只提供了很少的工具来处理 iterables。例如,如果要 过滤、映射或消除重复,则必须将其转换为数组:
1const iterable = new Set([-1, 0, -2, 3]);
2const filteredArray = [...iterable].filter(x => x >= 0);
3assert.deepEqual(filteredArray, [0, 3]);
如果 JavaScript 具有可迭代的工具函数,你可以直接过滤迭代:
1const filteredIterable = filter(iterable, x => x >= 0);
2assert.deepEqual(
3 // We only convert the iterable to an Array, so we can
4 // check what’s in it:
5 [...filteredIterable], [0, 3]);
以下是迭代工具函数的一些示例:
1// Count elements in an iterable
2assert.equal(count(iterable), 4);
3
4// Create an iterable over a part of an existing iterable
5assert.deepEqual(
6 [...slice(iterable, 2)],
7 [-1, 0]);
8
9// Number the elements of an iterable
10// (producing another – possibly infinite – iterable)
11for (const [i,x] of zip(range(0), iterable)) {
12 console.log(i, x);
13}
14// Output:
15// 0, -1
16// 1, 0
17// 2, -2
18// 3, 3
笔记:
很高兴能看到对数据的非破坏性转换有更多的支持。两个相关的库是:
JavaScript 对日期和时间的内置支持有许多奇怪的地方。这就是为什么目前建议用库来完成除了最基本任务之外的其它所有工作。
值得庆幸的是 temporal
是一个更好的时间 API:
1const dateTime = new CivilDateTime(2000, 12, 31, 23, 59);
2const instantInChicago = dateTime.withZone('America/Chicago');
一个相对流行的提议功能是 optional chaining。以下两个表达式是等效的。
1obj?.prop
2(obj === undefined || obj === null) ? undefined : obj.prop
此功能对于属性链特别方便:
1obj?.foo?.bar?.baz
但是,仍然存在缺点:
optional chaining 的替代方法是在单个位置提取一次信息:
无论采用哪种方法,都可以执行检查并在出现问题时尽早抛出异常。
进一步阅读:
目前正在为 运算符重载 进行早期工作,但是 infix 函数可能就足够了(目前还没有提案):
1import {BigDecimal, plus} from 'big-decimal';
2const bd1 = new BigDecimal('0.1');
3const bd2 = new BigDecimal('0.2');
4const bd3 = bd1 @plus bd2; // plus(bd1, bd2)
infix 函数的好处是:
下面是嵌套表达式的例子:
1a @plus b @minus c @times d
2times(minus(plus(a, b), c), d)
有趣的是,管道操作符还有助于提高可读性:
1plus(a, b)
2 |> minus(#, c)
3 |> times(#, d)
以下是我偶尔会遗漏的一些东西,但我认为不如前面提到的那些重要:
1 new ChainedError(msg, origError)
1 re`/^${RE_YEAR}-${RE_MONTH}-${RE_DAY}$/u`
.replace()
很重要):1> const re = new RegExp(RegExp.escape(':-)'), 'ug');
2> ':-) :-) :-)'.replace(re, '?')
3'? ? ?'
Array.prototype.get()
:1 > ['a', 'b'].get(-1)
2 'b'
1 function f(...[x, y] as args) {
2 if (args.length !== 2) {
3 throw new Error();
4 }
5 // ···
6 }
1 assert.equal(
2 {foo: ['a', 'b']} === {foo: ['a', 'b']},
3 false);
4 assert.equal(
5 deepEqual({foo: ['a', 'b']}, {foo: ['a', 'b']}),
6 true);
1 enum WeekendDay {
2 Saturday, Sunday
3 }
4 const day = WeekendDay.Sunday;
1 const myMap = Map!{1: 2, three: 4, [[5]]: 6}
2 // new Map([1,2], ['three',4], [[5],6])
3
4 const mySet = Set!['a', 'b', 'c'];
5 // new Set(['a', 'b', 'c'])
不会很快!当前开发时的静态类型(通过 TypeScript 或 Flow)和运行时的纯 JavaScript 之间的分离效果很好。所以没有什么合理的理由改变它。
Web 的一个关键要求是:永远不要破坏向后兼容性:
通过引入当前功能的更好版本,仍然可以修复一些错误。
有关此主题的更多信息,请参阅“针对不耐烦的程序员的 JavaScript ”。
作为一名语言设计师,无论你做什么,都会使一些人开心,而另一些人会伤心。因此,设计未来 JavaScript 功能的主要挑战不是让每个人都满意,而是让语言尽可能保持一致。
但是对于“一致”的含义,也存在分歧。因此,我们可以做到的最好的事情就是建立一致的“风格”,由一小群人(最多三人)构思和执行。不过这并不排除他们接受许多其他人的建议和帮助,但他们应该设定一个基调。
引用 Fred Brooks:
稍微回顾一下,尽管许多优秀实用的软件系统都是由委员会设计的,并且是作为一些项目的一部分而构建的,但是从本质上说,那些拥有大量激情粉丝的软件就是一个或几个设计思想的产品,——致伟大的设计师。
这些核心设计师的一个重要职责是对功能说“不”,以防止 JavaScript 变得太大。
他们还需要一个强大的支持系统,因为语言设计者往往会遭到严重的滥用(因为人们关心并且不喜欢听到“不”)。 最近的一个例子是 Guido van Rossum 辞去了首席 Python 语言设计师的工作,因为他受到了虐待。
这些想法可能也有助于设计和见证 JavaScript:
鸣谢:感谢Daniel Ehrenberg对本博文的反馈!