最近这几年,Javascript 的使用规模有了很大的增长。在这篇博文里,本文作者将和大家探讨现在 Javascript 还缺少的内容。
▲小编这里有一份web前端学习资料,直接加我的web前端直播学习qun:585928911免费领取,你敢来我就敢送。
以下为译文:
注:
-
我将只列出我自己认为最重要的功能缺失。虽然现在的 JavaScript 还有很多有用的功能没有实现,但本文中,我不想罗列太多。
-
我的选择是很主观的。
-
几乎所有在本博客中提到的内容都在TC39的候选列表中。也就是说,您也可以将本博文作为未来Javascript内容的预览。
有关前两个问题的更多想法,请参阅语言设计部分
(http://2ality.com/2019/01/future-js.html#language-design)。
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 {
// ···
}
注:这种方法是通过修饰符来说明该类是基于数值来进行比较的类。该方法是根据这个建议草案提出的(https://github.com/littledan/proposal-reserved-decorator-like-syntax)。
将对象放入数据结构中
由于对象是按引用进行比较的,因此像下面这样将它们放入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键值的管理方法来实现。例如:
-
通过哈希表定义的Map:这需要一个检查元素是否相等的方法,同时也需要一个创建哈希代码的方法。当你使用哈希代码的时候,需要对象保持不变。否则,就很容易破坏数据结构。
-
通过排序树定义的 Map:这需要一个检查元素是否相等的方法,以便管理 Map 中存储的值。
-
大整数(BigInt)
Javascript 中的 Number 类型都是 64 位(双精度)的。对于整数来说,有53位来表示整数的数值还有1位的符号位来表示正负。也就是,只能精确表示53位以内的整数,对于超出53位的整数,没有办法精确区分它们:
> 2 ** 53
9007199254740992
> (2 ** 53) + 1 // can’t be represented
9007199254740992
> (2 ** 53) + 2
9007199254740994
在某些情况下,这是一个很大的限制。现在,有一个对于大整数(BigInt)的建议。根据该建议,整数的精度可以根据需要进行扩展:
> 2n ** 53n
9007199254740992n
> (2n ** 53n) + 1n
9007199254740993n
大整数(BigInt)也支持类型转换,这样就可以得到固定位数的整数:
const int64a = BigInt.asUintN(64, 12345n);
const int64b = BigInt.asUintN(64, 67890n);
const result = BigInt.asUintN(64, int64a * int64b);
十进制运算
基于 IEEE 754 的规范,Javascript 中的 Number 类型都是64位(双精度)的。因为这些数是以二进制来表示,所以在进行小数运算的时候,你不可避免地会遇到四舍五入的问题:
> 0.1 + 0.2
0.30000000000000004
在科学计算和金融领域,这是一个比较突出的问题。现在,有一个正处在初始阶段的关于10进制数值的表示方法的提议。如果该提议获得通过,数值可能会以如下方式表示,请注意在小数后面的m用于表示十进制数值:
> 0.1m + 0.2m
0.3m
数值分类
现在,在 javascript 中,对数值进行分类是一件非常麻烦的事:
-
首先,你需要决定是否使用 typeof 或 instanceof 方法。
-
其次, typeof 方法有一个众所周知的奇特实现,它认为空值 null 是对象(object)。我个人认为它把函数(function)归结为对象(object)也是比较奇特的。
> typeof null
'object'
> typeof function () {}
'function'
> typeof []
'object'
-
第三,instanceof 方法对于其它领域的对象(如 frame 对象)不能正常工作。
对于上述的这些问题,可以通过创建特定的 Javascript 库来解决。以后,如果有时间,我自己会试着实现。
2. 函数式编程
更多的表达式
和 C 语言风格类似的编程语言都对于表达式和程序语句做了明确的区分:
// Conditional expression
let str1 = someBool ? 'yes' : 'no';
// Conditional statement
let str2;
if (someBool) {
str2 = 'yes';
} else {
str2 = 'no';
}
在函数式语言中,任何对象都是表达式。使用表达式将使你能在任何表达式的上下文中使用表达式:
let str3 = do {
if (someBool) {
'yes'
} else {
'no'
}
};
以下的代码是一个更实际的例子。如果没有 do 的表达式,为了避免作用域内的变量 result,就需要立即调用 arrow 函数来实现:
const func = (() => {
let result; // cache
return () => {
if (result === undefined) {
result = someComputation();
}
return result;
}
})();
有了 do 表达式,你的代码会更简洁:
const func = do {
let result;
() => {
if (result === undefined) {
result = someComputation();
}
return result;
};
};
匹配:析构 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 语句,但使用析构函数来处理 case 命令。在使用嵌套数据结构时(例如在编译器中),这种功能都很有用。模式匹配方案现在处于第一阶段。
管道(Pipeline)操作
对于管道(pipeline)操作,有两个互相竞争的提案。现在,我们先看一下"智能管道"提案;另外一个提案是"F# 管道"。
管道(pipeline)操作的基本思想可以用下面的递归调用来说明:
const y = h(g(f(x)));
但是,管道的概念没有反映出来我们思考的逻辑顺序。我们可以把我们对于上面公式的思考逻辑归纳为下面几步:
-
以变量X开始;
-
调用函数F()处理变量X;
-
对于前面的结果调用函数G()来处理;
-
对于前面的结果调用函数H()来处理;
-
把计算结果赋值给变量Y。
使用管道操作,可以使我们更好地描述我们的思考逻辑:
const y = x |> f |> g |> h;
换而言之,下面两个表达式是一样的:
f(123)
123 |> f
另外,管道操作还支持部分应用的功能。该功能类似于 .bind()函 数,下面两个表达式是一样的:
123 |> f(#)
123 |> (x => f(x))
管道操作的一个重要优势在于你可以像使用方法一样使用函数,而不用进行类型转换:
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 中,对于 Worker API 可以像下面这样使用:
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 的方式是比较重量级的实现。每一种都需要有自己的领域(全局变量等)。我希望在将来能看到一个更量级的实现。
4. 标准
现在,Javascript 明显落后于其他语言的一个方面是它的标准库支持。最小的标准库支持确实有合理的一面,因为它们依赖的外部库会很快地发展和变化。但是,还是有一些非常有用的核心功能需要实现。
用模块替代命名空间(namespace)对象
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 将会变得更加模块化(这可以加快启动时间并减少内存消耗)。
-
调用导入的函数比调用存储在对象中的函数速度更快。
支持 iterables(同步和异步)的工具函数(Helper)
iterables 能提供很多便利,包括只有在需要的时候才计算数值和支持许多数据源。但是,目前 Javascript 对 iterables 提供的工具函数却很少。比如,现在如果你要筛选、映射或减少 iterable,则只能将它转换为数组:
const iterable = new Set([-1, 0, -2, 3]);
const filteredArray = [...iterable].filter(x => x >= 0);
assert.deepEqual(filteredArray, [0, 3]);
如果 Javascript 能有对 iterables 支持的工具函数,你就可以直接过滤 iterables:
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]);
下面是更多可能的 iterables 工具函数的示例:
// 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
请注意:
-
更多 iterators 的工具函数示例,请参考 Python 的 itertools。
-
在 Javascript 中,支持 iterables 的每个工具函数都应该有两个版本:一个用于支持同步 iterables,另一个用于支持异步 iterables。
不可改变的数据
希望 Javascript 最好能对非破坏性数据转换提供更多支持。两个相关的库是:
-
Immer 是比较轻量级的,它可与普通对象和队列一起使用。
-
相对而言,immutable.js 功能更强大也更重量级,它有自己的数据结构。
对于日期和时间的更好支持
Javascript 内置的对日期和时间的支持有许多奇怪的地方。也正因为如此,才建议大家在最基本的任务之外的其他任务中才使用系统的日期和时间库。
值得庆幸的是,现在人们正在研究日期和时间方面的更好的 API:
const dateTime = new CivilDateTime(2000, 12, 31, 23, 59);
const instantInChicago = dateTime.withZone('America/Chicago');
5. 可能不太需要的功能
可选链接(chaining)的优缺点
当前一个比较普遍的提议是对于可选链接的支持。根据该提议,以下两个表达式是等效的:
obj?.prop
(obj === undefined || obj === null) ? undefined : obj.prop
这一功能尤其适用于属性链:
obj?.foo?.bar?.baz
但是,这个功能也有缺点:
-
很难管理深度嵌套结构。
-
在访问数据时有较高的容错性,这样会隐藏可能的问题,使它们直到很晚的时候才能被发现,并且很难调试。
可选链接的一个替代方案是在某个特定的位置提取一次信息:
-
可以编写一个工具函数来提取数据。
-
也可以编写一个函数,其输入是深度嵌套的数据,其输出则是简单、规范化的数据。
无论采用以上哪种方法,您都能进行检查。早一点发现存在的问题。
更多资料:
-
Carl Vitullo 著“过度防御编程”
(https://medium.com/@vcarl/overly-defensive-programming-e7a1b3d234c2)
-
Cory House 著 Twitter 里的线程
(https://twitter.com/housecor/status/1088419498846244864)
我们需要运算符重载吗?
现在,有些人正在为运算符重载做前期的准备工作,但我觉得中缀函数应用程序可能已经足够了(尽管目前没有针对它的提议):
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. 其他内容
这些是我偶尔会错过的一些事情,我也不认为它们像我之前提到的内容那样重要:
-
连锁的异常(Chained exceptions):能够捕获错误,添加更多的信息,然后再次抛出错误;
new ChainedError(msg, origError)
-
可组合正则表达式:
re`/^${RE_YEAR}-${RE_MONTH}-${RE_DAY}$/u`
-
转义正则表达式的文本(对.replace()很重要):
> const re = new RegExp(RegExp.escape(':-)'), 'ug');
> ':-) :-) :-)'.replace(re, '