翻译:疯狂的技术宅
原文: http://2ality.com/2019/01/fut...
本文首发微信公众号:jingchengyideng
欢迎关注,天天都给你推送新鲜的前端技术文章javascript
近年来,JavaScript 的功能获得了大幅度的增长,本文探讨了其仍然缺失的东西。html
说明:前端
有关前两个问题的更多想法,请参阅本文第8节:语言设计部分。java
目前,JavaScript 只能对原始值(value)进行比较,例如字符串的值(经过查看其内容):node
> 'abc' === 'abc' true
相反,对象则经过身份ID(identity)进行比较(对象仅严格等于自身):python
> {x: 1, y: 4} === {x: 1, y: 4} false
若是有一种可以建立按值进行比较对象的方法,那将是很不错的:git
> #{x: 1, y: 4} === #{x: 1, y: 4} true
另外一种可能性是引入一种新的类(确切的细节还有待肯定):程序员
@[ValueType] class Point { // ··· }
旁注:这种相似装饰器的将类标记为值类型的的语法基于草案提案。github
若是对象经过身份ID进行比较,将它们放入 ECMAScript 数据结构(如Maps)中并无太大意义:web
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 keys 的管理。 例如:
JavaScript 的数字老是64位的(双精度),它能为整数提供53位二进制宽度。这意味着若是超过53位,就很差使了:
> 2 ** 53 9007199254740992 > (2 ** 53) + 1 // can’t be represented 9007199254740992 > (2 ** 53) + 2 9007199254740994
对于某些场景,这是一个至关大的限制。如今有[BigInts提案](http://2ality.com/2017/03/es-...),这是真正的整数,其精度能够随着须要而增加:
> 2n ** 53n 9007199254740992n > (2n ** 53n) + 1n 9007199254740993n
BigInts还支持 casting,它为你提供固定位数的值:
const int64a = BigInt.asUintN(64, 12345n); const int64b = BigInt.asUintN(64, 67890n); const result = BigInt.asUintN(64, int64a * int64b);
JavaScript 的数字是基于 IEEE 754 标准的64位浮点数(双精度数)。鉴于它们的表示形式是基于二进制的,在处理小数部分时可能会出现舍入偏差:
> 0.1 + 0.2 0.30000000000000004
这在科学计算和金融技术(金融科技)中尤为成问题。基于十进制运算的提案目前处于阶段0。它们可能最终被这样使用(注意十进制数的后缀 m
):
> 0.1m + 0.2m 0.3m
目前,在 JavaScript 中对值进行分类很是麻烦:
typeof
或 instanceof
。typeof
有一个众所周知的的怪癖,就是把 null
归类为“对象”。我还认为函数被归类为 'function'
一样是奇怪的。> typeof null 'object' > typeof function () {} 'function' > typeof [] 'object'
instanceof
不适用于来自其余realm(框架等)的对象。也许可能经过库来解决这个问题(若是我有时间,就会实现一个概念性的验证)。
不幸的是C风格的语言在表达式和语句之间作出了区分:
// 条件表达式 let str1 = someBool ? 'yes' : 'no'; // 条件声明 let str2; if (someBool) { str2 = 'yes'; } else { str2 = 'no'; }
特别是在函数式语言中,一切都是表达式。 Do-expressions 容许你在全部表达式上下文中使用语句:
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; }; };
switch
JavaScript 使直接使用对象变得容易。可是根据对象的结构,没有内置的切换 case 分支的方法。看起来是这样的(来自提案的例子):
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阶段。
管道操做目前有两个竞争提案 。在本文,咱们研究其中的 智能管道(另外一个提议被称为 F# Pipelines)。
管道操做的基本思想以下。请考虑代码中的嵌套函数调用。
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 ;
一直以来 JavaScript 对并发性的支持颇有限。并发进程的事实标准是 Worker API,能够在 web browsers 和 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); }
唉,相对来讲 Workers 是重量级的 —— 每一个都有本身的 realm(全局变量等)。我想在将来看到一个更加轻量级的构造。
JavaScript 仍然明显落后于其余语言的一个领域是它的标准库。固然保持最小化是有意义的,由于外部库更容易进化和适应。可是有一些核心功能也是有必要的。
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。例如,若是要 过滤、映射或消除重复,则必须将其转换为数组:
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
笔记:
很高兴能看到对数据的非破坏性转换有更多的支持。两个相关的库是:
JavaScript 对日期和时间的内置支持有许多奇怪的地方。这就是为何目前建议用库来完成除了最基本任务以外的其它全部工做。
值得庆幸的是 temporal
是一个更好的时间 API:
const dateTime = new CivilDateTime(2000, 12, 31, 23, 59); const instantInChicago = dateTime.withZone('America/Chicago');
一个相对流行的提议功能是 optional chaining。如下两个表达式是等效的。
obj?.prop (obj === undefined || obj === null) ? undefined : obj.prop
此功能对于属性链特别方便:
obj?.foo?.bar?.baz
可是,仍然存在缺点:
optional chaining 的替代方法是在单个位置提取一次信息:
不管采用哪一种方法,均可以执行检查并在出现问题时尽早抛出异常。
进一步阅读:
目前正在为 运算符重载 进行早期工做,可是 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 函数的好处是:
下面是嵌套表达式的例子:
a @plus b @minus c @times d times(minus(plus(a, b), c), d)
有趣的是,管道操做符还有助于提升可读性:
plus(a, b) |> minus(#, c) |> times(#, d)
如下是我偶尔会遗漏的一些东西,但我认为不如前面提到的那些重要:
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'
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);
enum WeekendDay { Saturday, Sunday } const day = WeekendDay.Sunday;
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'])
不会很快!当前开发时的静态类型(经过 TypeScript 或 Flow)和运行时的纯 JavaScript 之间的分离效果很好。因此没有什么合理的理由改变它。
Web 的一个关键要求是:永远不要破坏向后兼容性:
经过引入当前功能的更好版本,仍然能够修复一些错误。
有关此主题的更多信息,请参阅“针对不耐烦的程序员的 JavaScript ”。
做为一名语言设计师,不管你作什么,都会使一些人开心,而另外一些人会伤心。所以,设计将来 JavaScript 功能的主要挑战不是让每一个人都满意,而是让语言尽量保持一致。
可是对于“一致”的含义,也存在分歧。所以,咱们能够作到的最好的事情就是创建一致的“风格”,由一小群人(最多三人)构思和执行。不过这并不排除他们接受许多其余人的建议和帮助,但他们应该设定一个基调。
引用 Fred Brooks):
稍微回顾一下,尽管许多优秀实用的软件系统都是由委员会设计的,而且是做为一些项目的一部分而构建的,可是从本质上说,那些拥有大量激情粉丝的软件就是 一个或几个设计思想的产品,——致伟大的设计师。
这些核心设计师的一个重要职责是对功能说“不”,以防止 JavaScript 变得太大。
他们还须要一个强大的支持系统,由于语言设计者每每会遭到严重的滥用(由于人们关心而且不喜欢听到“不”)。 最近的一个例子是 Guido van Rossum 辞去了首席 Python 语言设计师的工做,由于他受到了虐待。
这些想法可能也有助于设计和见证 JavaScript:
鸣谢:感谢Daniel Ehrenberg对本博文的反馈!
欢迎继续阅读本专栏其它高赞文章: