前端先锋前端先锋

JavaScript 的未来:它还少些什么?

2019-02-22  本文已影响26人  前端先锋

翻译:疯狂的技术宅
原文:http://2ality.com/2019/01/future-js.html

近年来,JavaScript 的功能得到了大幅度的增加,本文探讨了其仍然缺失的东西。

说明:

  1. 我只列出了我所发现的最重要的功能缺失。当然还有很多其它有用的功能,但同时也会增加太多的风险。
  2. 我的选择是主观的。
  3. 本文中提及的几乎所有内容都在 TC39 的技术雷达上。 也就是说,它还可以作为未来可能的 JavaScript 的预览。

有关前两个问题的更多想法,请参阅本文第8节:语言设计部分。

1. 值

1.1 按值比较对象

目前,JavaScript 只能对原始值(value)进行比较,例如字符串的值(通过查看其内容):

> 'abc' === 'abc'
true

相反,对象则通过身份ID(identity)进行比较(对象仅严格等于自身):

> {x: 1, y: 4} === {x: 1, y: 4}
false

如果有一种能够创建按值进行比较对象的方法,那将是很不错的:

> #{x: 1, y: 4} === #{x: 1, y: 4}
true

另一种可能性是引入一种新的类(确切的细节还有待确定):

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

旁注:这种类似装饰器的将类标记为值类型的的语法基于草案提案

1.2 将对象放入数据结构中

如果对象通过身份ID进行比较,将它们放入 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 keys 的管理。 例如:

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还支持 casting,它为你提供固定位数的值:

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位浮点数(双精度数)。鉴于它们的表示形式是基于二进制的,在处理小数部分时可能会出现舍入误差:

> 0.1 + 0.2
0.30000000000000004

这在科学计算和金融技术(金融科技)中尤其成问题。基于十进制运算的提案目前处于阶段0。它们可能最终被这样使用(注意十进制数的后缀 m):

> 0.1m + 0.2m
0.3m

1.5 对值进行分类

目前,在 JavaScript 中对值进行分类非常麻烦:

> typeof null
'object'
> typeof function () {}
'function'
> typeof []
'object'

也许可能通过库来解决这个问题(如果我有时间,就会实现一个概念性的验证)。

2. 函数式编程

2.1 更多表达式

不幸的是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;
  };
};

2.2 匹配:解构 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阶段。

2.3 管道操作

管道操作目前有两个竞争提案 。在本文,我们研究其中的 智能管道(另一个提议被称为 F# Pipelines)。

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

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

但是,这种表示方法通常不能体现我们对计算步骤的看法。在直觉上,我们将它们描述为:

管道运算符能让我们更好地表达这种直觉:

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 browsersNode.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(全局变量等)。我想在未来看到一个更加轻量级的构造。

4. 标准库

JavaScript 仍然明显落后于其他语言的一个领域是它的标准库。当然保持最小化是有意义的,因为外部库更容易进化和适应。但是有一些核心功能也是有必要的。

4.1 用模块替代命名空间对象

JavaScript 标准库是在其语言具有模块之前创建的。因此函数被放在命名空间对象中,例如ObjectReflectMathJSON

如果将这个功能放在模块中会更好。它必须通过特殊的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']);

好处是:

4.2 可迭代工具 (sync 与 async)

迭代 的好处包括按需计算和支持许多数据源。但是目前 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 不可变数据

很高兴能看到对数据的非破坏性转换有更多的支持。两个相关的库是:

4.4 更好地支持日期和时间

JavaScript 对日期和时间的内置支持有许多奇怪的地方。这就是为什么目前建议用库来完成除了最基本任务之外的其它所有工作。

值得庆幸的是 temporal 是一个更好的时间 API:

const dateTime = new CivilDateTime(2000, 12, 31, 23, 59);
const instantInChicago = dateTime.withZone('America/Chicago');

5. 可能不需要的功能

5.1 optional chaining 的优缺点

一个相对流行的提议功能是 optional chaining。以下两个表达式是等效的。

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

此功能对于属性链特别方便:

obj?.foo?.bar?.baz

但是,仍然存在缺点:

optional chaining 的替代方法是在单个位置提取一次信息:

无论采用哪种方法,都可以执行检查并在出现问题时尽早抛出异常。

进一步阅读:

5.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 函数的好处是:

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

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

有趣的是,管道操作符还有助于提高可读性:

plus(a, b)
  |> minus(#, c)
  |> times(#, d)

6. 各种小东西

以下是我偶尔会遗漏的一些东西,但我认为不如前面提到的那些重要:

  new ChainedError(msg, origError)
  re`/^${RE_YEAR}-${RE_MONTH}-${RE_DAY}$/u`
> const re = new RegExp(RegExp.escape(':-)'), 'ug');
> ':-) :-) :-)'.replace(re, '🙂')
'🙂 🙂 🙂'
  > ['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'])

7. FAQ:未来的JavaScript

7.1 JavaScript 会不会支持静态类型?

不会很快!当前开发时的静态类型(通过 TypeScript 或 Flow)和运行时的纯 JavaScript 之间的分离效果很好。所以没有什么合理的理由改变它。

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

Web 的一个关键要求是:永远不要破坏向后兼容性:

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

有关此主题的更多信息,请参阅“针对不耐烦的程序员的 JavaScript ”。

8. 关于语言设计的思考

作为一名语言设计师,无论你做什么,都会使一些人开心,而另一些人会伤心。因此,设计未来 JavaScript 功能的主要挑战不是让每个人都满意,而是让语言尽可能保持一致。

但是对于“一致”的含义,也存在分歧。因此,我们可以做到的最好的事情就是建立一致的“风格”,由一小群人(最多三人)构思和执行。不过这并不排除他们接受许多其他人的建议和帮助,但他们应该设定一个基调。

引用 Fred Brooks:

稍微回顾一下,尽管许多优秀实用的软件系统都是由委员会设计的,并且是作为一些项目的一部分而构建的,但是从本质上说,那些拥有大量激情粉丝的软件就是一个或几个设计思想的产品,——致伟大的设计师。

这些核心设计师的一个重要职责是对功能说“不”,以防止 JavaScript 变得太大。

他们还需要一个强大的支持系统,因为语言设计者往往会遭到严重的滥用(因为人们关心并且不喜欢听到“不”)。 最近的一个例子是 Guido van Rossum 辞去了首席 Python 语言设计师的工作,因为他受到了虐待。

8.1 其他想法

这些想法可能也有助于设计和见证 JavaScript:

鸣谢:感谢Daniel Ehrenberg对本博文的反馈!


本文首发微信公众号:jingchengyideng
关注微信公众号,每天推送最新前端趋势和技术文章

微信扫一扫关注公众号
上一篇下一篇

猜你喜欢

热点阅读