热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

翻译:疯狂的技术宅原文:http:2ality.com201901fut本文首发微信公众号:jingchengyideng欢迎关注&
翻译:疯狂的技术宅
原文:http://2ality.com/2019/01/fut...

本文首发微信公众号:jingchengyideng
欢迎关注,每天都给你推送新鲜的前端技术文章


近年来,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提案](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);

1.4 小数计算

Javascript 的数字是基于 IEEE 754 标准的64位浮点数(双精度数)。鉴于它们的表示形式是基于二进制的,在处理小数部分时可能会出现舍入误差:

> 0.1 + 0.2
0.30000000000000004

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

> 0.1m + 0.2m
0.3m

1.5 对值进行分类

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

  • 首先,你必须决定是否使用 typeofinstanceof
  • 其次,typeof 有一个众所周知的的怪癖,就是把 null 归类为“对象”。我还认为函数被归类为 'function' 同样是奇怪的。

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

  • 第三,instanceof 不适用于来自其他realm(框架等)的对象。

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

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; // cachereturn () => {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)));

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

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

4. 标准库

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

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

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

  • 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 将变得更加模块化(这可以加快启动时间并减少内存消耗)。
  • 调用导入的函数比调用存储在对象中的函数更快。

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

笔记:

  • 有关迭代器的工具函数示例,请参阅 Python 的 itertools (https://docs.python.org/3/lib...)。
  • 对于 Javascript,迭代的每个工具函数应该有两个版本:一个用于同步迭代,一个用于异步迭代。

4.3 不可变数据

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

  • Immer 相对轻量,适用于普通对象和数组。
  • Immutable.js 更强大,更重量级,并附带自己的数据结构。

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 的替代方法是在单个位置提取一次信息:

  • 你可以编写一个提取数据的辅助函数。
  • 或者你可以编写一个函数,使其输入是深层嵌套数据,但是输出更简单、标准化的数据。

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

进一步阅读:

  • “Overly defensive programming” by Carl Vitullo
  • Thread on Twitter by Cory House

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

  • 你可以创建 Javascript 以外的运算符。
  • 与普通函数相比,嵌套表达式的可读性仍然很好。

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

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`

  • 正则表达式的转义文本(对于.replace()很重要):

> const re = new RegExp(RegExp.escape(':-)'), 'ug');
> ':-) :-) :-)'.replace(re, '?')
'? ? ?'

  • 支持负索引的 Array.prototype.get():

> ['a', 'b'].get(-1)'b'

  • 匹配和解构的模式(KatMarchá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);

  • 枚举:向 Javascript 添加枚举的一个好处是可以缩小与 TypeScript 的差距。目前有两份提案草案(尚未处于正式阶段)。 一个是Rick Waldron,另一个是Ron Buckton。在两个提案中,最简单的语法如下所示:

enum WeekendDay {Saturday, Sunday}const day = WeekendDay.Sunday;

  • Tagged collection literals (由KatMayán提议 - 并撤回):允许你创建 Map 和 Set,如下所示:

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:

  • 创建描述 Javascript 未来前景的路线图。这样的路线图可以用讲故事的方式并将许多单独的部分连接成一个连贯的整体。我所知的最后一个这样的路线图是 Brendan Eich 的“和谐的梦想”。
  • 记录设计理念。现在,ECMAScript 规范只记录了 怎样 做,而没有 为什么 。举个例子:可枚举性的目的是什么?
  • 规范的解释者。半正式的规范部分几乎已经可执行。如果能够像编程语言一样对待和运行它们会很棒。 (你可能需要一个约定来区分规范代码和非规范辅助函数。)

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


本文首发微信公众号:jingchengyideng

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:

  • 12个令人惊叹的CSS实验项目
  • 世界顶级公司的前端面试都问些什么
  • CSS Flexbox 可视化手册
  • 过节很无聊?还是用 Javascript 写一个脑力小游戏吧!
  • 从设计者的角度看 React
  • CSS粘性定位是怎样工作的
  • 一步步教你用HTML5 SVG实现动画效果
  • 程序员30岁前月薪达不到30K,该何去何从
  • 第三方CSS安全吗?
  • 谈谈super(props) 的重要性




推荐阅读
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
  • 本文介绍了使用Spark实现低配版高斯朴素贝叶斯模型的原因和原理。随着数据量的增大,单机上运行高斯朴素贝叶斯模型会变得很慢,因此考虑使用Spark来加速运行。然而,Spark的MLlib并没有实现高斯朴素贝叶斯模型,因此需要自己动手实现。文章还介绍了朴素贝叶斯的原理和公式,并对具有多个特征和类别的模型进行了讨论。最后,作者总结了实现低配版高斯朴素贝叶斯模型的步骤。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文讨论了在手机移动端如何使用HTML5和JavaScript实现视频上传并压缩视频质量,或者降低手机摄像头拍摄质量的问题。作者指出HTML5和JavaScript无法直接压缩视频,只能通过将视频传送到服务器端由后端进行压缩。对于控制相机拍摄质量,只有使用JAVA编写Android客户端才能实现压缩。此外,作者还解释了在交作业时使用zip格式压缩包导致CSS文件和图片音乐丢失的原因,并提供了解决方法。最后,作者还介绍了一个用于处理图片的类,可以实现图片剪裁处理和生成缩略图的功能。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
author-avatar
叮咚叮咚_叮
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有