前端主流的 Javascript,缺失了哪些技能?

267 篇文章 1 订阅
93 篇文章 0 订阅

 

 

最近这几年,Javascript 的使用规模有了很大的增长。在这篇博文里,本文作者将和大家探讨现在 Javascript 还缺少的内容。

以下为译文:

注:

  1. 我将只列出我自己认为最重要的功能缺失。虽然现在的 JavaScript 还有很多有用的功能没有实现,但本文中,我不想罗列太多。
  2. 我的选择是很主观的。
  3. 几乎所有在本博客中提到的内容都在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

但是,这个功能也有缺点:

  • 很难管理深度嵌套结构。
  • 在访问数据时有较高的容错性,这样会隐藏可能的问题,使它们直到很晚的时候才能被发现,并且很难调试。

可选链接的一个替代方案是在某个特定的位置提取一次信息:

  • 可以编写一个工具函数来提取数据。
  • 也可以编写一个函数,其输入是深度嵌套的数据,其输出则是简单、规范化的数据。

无论采用以上哪种方法,您都能进行检查。早一点发现存在的问题。

更多资料:

我们需要运算符重载吗?

现在,有些人正在为运算符重载做前期的准备工作,但我觉得中缀函数应用程序可能已经足够了(尽管目前没有针对它的提议):

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, '?')
'? ? ?'
  • Array.prototype.get()支持负的索引值:
> ['a', 'b'].get(-1)
'b'
  • 在匹配和破坏的表达式中对于 As 模式的支持(由 Kat Marchán提出的建议):
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);
  • 枚举(Enums):在 Javascript 中增加枚举类型的一个好处是,这将缩小 Javascript 与已经有枚举类型的 Typescript 的差距。目前有两个提案草案(都还没有到正式提案的阶段)。一个是里克·沃尔德隆提出的,另一个是由罗恩·巴克顿提出。在这两个方案中,最简单的语法如下:
enum WeekendDay {
  Saturday, Sunday
}
const day = WeekendDay.Sunday;
  • 可标记的集合字面量(由 Kat Marchán 提议后撤销):允许你这样创建 Maps 和 Sets:
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. Javascript 的明天

Javascript 是否会支持静态类型?

近期不会!现在开发过程中的静态类型(通过 typescript 或 Flow)和运行时纯 Javascript 之间的分离做得比较好。所以没有立即需要进行改变的理由。

为什么我们不能通过去掉比较怪异的行为和和过时的特性来清理 JavaScript?

对于 Web 的一个主要的要求就是永远不要破坏向后兼容性:

  • 缺点是语言具有许多遗留功能。
  • 但好处大于缺点:大型代码库保持一致;迁移到新版本很简单;语言引擎本身保持较小规模(不需要支持多个版本);等等。
  • 通过改进现有的版本,还可以修复一些错误。

 

8. 对语言设计的思考

作为一个编程语言设计师,无论你做什么,你总会让一些人高兴,让另外一些人不高兴。因此,设计未来的 JavaScript 的主要挑战不是让每个人都高兴,而是尽可能地保持语言的一致性。

然而,人们对于这个所谓的“一致性”的理解也存在着分歧。所以,我们所能做的最好的事情就是建立一个一致的“风格”,由一小群人(最多三个人)来设想和执行。这并不妨碍他们得到其他人的建议和帮助,但他们自己应该确定总体的基调。

以下语句引用自 Fred Brooks:

如果我们做一个简单的回顾,就会发现:虽然很多优秀的、被人们广为使用的软件系统是由各自的技术委员会进行设计,并且作为复杂项目的一个子项目进行的开发,但是那些拥有众多热情粉丝的软件系统却只是一个或几个伟大的设计师的杰作。

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

他们本身还需要一个强大的支持系统,因为语言设计师往往会承受各种各样的压力(因为人们确实关心技术本身并且不喜欢听到“不”)。最近的一个例子是 Guido van Rossum 因为承受不了人们的压力而辞去了 Python 首席设计师的职位。

其他想法

我下面的这些想法也许会有助于设计未来的 Javascript:

  • 创建一个路线图,用来描述 Javascript 的愿景。这样的路线图可以讲述一个完整的故事,并且将许多零散的部分连接成一个整体。我所知道的最后一个这样的路线图是 Brendan Eich 的《Harmony Of My Dreams》。
  • 记录下进行某种设计的出发点。现在,ECMAScript 规范记录了事情是如何工作的,但没有相应的原理。比如:可枚举(enumerability)的目的是什么?
  • 一个规范的解释器。这个非完全正规的规范就差不多可以执行了。如果能够像编程语言一样对待和运行它们,那就更好了。(您可能需要一个约定来区分规范的代码和非规范的辅助函数。)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值