距离最好的编程语言,JavaScript还缺些什么?

Posted 前端之巅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了距离最好的编程语言,JavaScript还缺些什么?相关的知识,希望对你有一定的参考价值。

作者|Axel Rauschmayer
译者|无明

近年来,javascript 虽然经历了蓬勃发展,但仍然缺失了一些东西,这些缺失的东西是什么呢?这篇文章将带你一探究竟。

请注意:

  1. 我只列出我所发现的最重要的缺失特性。

  2. 我的选择带有一定的主观性。

  3. 本文所提及的几乎所有内容都包含在 TC39 的技术雷达中。也就是说,它们可以作为未来的 JavaScript 特性预览。

    值     
 按值比较对象

目前,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:需要一个比较两个值的操作。

 十进制计算

JavaScript 的数字是基于 IEEE 754 标准的 64 位浮点数(双精度数)。由于它们的表示形式是 base 2,在处理小数时可能会出现舍入误差:

> 0.1 + 0.2
0.30000000000000004

这在科学计算和金融技术中是个大问题。目前有一个关于 base 10 数字的提案处于 stage 0(https://github.com/tc39/proposals/blob/master/stage-0-proposals.md)。它们的最终用法可能是这样的(注意十进制数的后缀 m):

> 0.1m + 0.2
0.3m
 对值进行分类

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

  • 首先,你要决定是使用 typeof 还是 instanceof。

  • 其次,typeof 会将 null 归类为“object”,而且我认为将函数归类为“function”也有点奇怪。

> typeof null
'object'
> typeof function () {}
'function'
> typeof []
'object'
  • 第三,instanceof 不适用于来自其他域(不同的 iframe 等)的对象。

这个问题有可能可以通过一个库来解决(如果我有时间会进行 PoC)。

函数式编程
 更多的表达

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 表达式,需要立即调用箭头函数来隐藏作用域中的变量结果:

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 使得直接使用对象变得更容易,但仍然没有提供内置的基于对象结构的 switch 方法。例如(来自提案中的例子):

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。当人们使用了嵌套数据结构(例如在编译器中)时,这个功能会非常有用。

模式匹配的提案目前处于 stage 1:

https://github.com/tc39/proposal-pattern-matching

 管道操作符

目前有两个针对管道操作符的提案。我们这里要介绍的是智能管道(另一个提案称为 F# 管道)。

管道操作符的基本想法如下所示。

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

但是,这种表示法通常无法反映我们是如何看待计算步骤的。我们的直觉会认为:

  • 从值 x 开始;

  • 调用 f();

  • 针对结果调用 g();

  • 再针对结果调用 h();

  • 将最后的结果赋值给 y。

管道运算符可以更直观地表达这种想法:

const y = x |> f |> g |> h;

换句话说,以下两个表达式是等价的。

f(123)
123 |> f

此外,管道运算符支持部分函数 apply(类似于函数的.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 浏览器和 Node.js 中。

在 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 相对重量级——每个都有自己的域(全局变量等)。我希望在未来能够看到一个更轻量级的解决方案。

标准库

标准库是 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 将变得更加模块化(可以加快启动速度并减少内存消耗);

  • 调用导入的函数比调用保存在对象中的函数更快。

 处理可迭代对象的辅助函数(同步和异步)

可迭代对象的好处是可以按需计算和支持多种数据源。但 JavaScript 目前只提供了少量的工具来处理可迭代对象。例如,如果要 filter、map 或 reduce 可迭代对象,必须先将其转换为数组:

const iterable = new Set([-1, 0, -2, 3]);
const filteredArray = [...iterable].filter(x => x >= 0);
assert.deepEqual(filteredArray, [0, 3]);

如果 JavaScript 提供了处理可迭代对象的辅助函数,就可以直接 filter 可迭代对象:

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

请注意:

  • 有关迭代器工具函数的示例,请参阅 Python 的 itertools(https://docs.python.org/3/library/itertools.html)。

  • 对于 JavaScript 来说,处理可迭代对象的辅助函数应该有两个版本:一个用于同步迭代,一个用于异步迭代。

 不可变数据

如果能够对非破坏性数据转换提供更多的支持就好了。两个相关的库是:

  • Immer 相对轻量级,适用于普通对象和数组:

    https://github.com/mweststrate/immer

  • Immutable.js 更强大,更重量级,并提供了自己的数据结构:

    https://github.com/facebook/immutable-js/

可能不需要的功能
 可选链的优缺点

一个相对流行的提案功能是可选链(https://github.com/tc39/proposal-optional-chaining),以下两个表达式是等效的。

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

使用这个功能来链接属性会非常方便:

obj?.foo?.bar?.baz

不过,这个功能也有缺点:

  • 深层嵌套的结构更难管理;

  • 为访问数据而隐藏的问题会在后面暴露出来,更加难以调试。

可选链的替代方法是在一个位置提取一次信息:

  • 你可以编写一个提取数据的辅助函数;

  • 或者你可以编写一个函数,这个函数的输入是深度嵌套数据,输出是简单的标准化数据。

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

 我们需要运算符重载吗?

目前有一些有关运算符重载的工作正在进行中,不过有中缀函数可能就足够了:

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)

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

plus(a, b)
  |> minus(#, c)
  |> times(#, d)
各种小功能

我可能遗漏了一些东西,但它们可能不像之前提到的那些东西那么重要: