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

2019 年 1 月 31 日

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

近年来,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 {  // ···}
复制代码


旁白:创建值类型类的装饰器语法基于这个草案提案


将对象放入数据结构中


当对象是按引用进行比较时,将它们放入 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.20.30000000000000004
复制代码


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


> 0.1m + 0.20.3m
复制代码


对值进行分类


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


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

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


> typeof null'object'> typeof function () {}'function'> typeof []'object'
复制代码


  • 第三,instanceof不适用于来自其他域(不同的iframe等)的对象。


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


函数式编程


更多的表达


C 语言风格的编程语言会区分表达式和语句:


// Conditional expressionlet str1 = someBool ? 'yes' : 'no';
// Conditional statementlet 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


管道操作符


目前有两个针对管道操作符的提案。我们这里要介绍的是智能管道(另一个提案称为 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 iterableassert.equal(count(iterable), 4);
// Create an iterable over a part of an existing iterableassert.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

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


不可变数据


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



可能不需要的功能


可选链的优缺点


一个相对流行的提案功能是可选链。以下两个表达式是等效的。


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 dtimes(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);
复制代码


  • 枚举:向JavaScript添加枚举的一个好处是可以缩小与TypeScript(已经有枚举)之间的差距。目前有两份相关提案(还未进入正式阶段)。在两个提案中,最简单的语法如下所示:


enum WeekendDay {  Saturday, Sunday}const day = WeekendDay.Sunday;
复制代码


  • 标记集合字面量:可以像下面这样创建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'])
复制代码


关于语言设计的思考


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


不过,对于什么是“一致性”,也存在分歧。我们可以做的是建立一致性“风格”,由一小群人(最多三人)来构思和推动。当然,这并不排除他们可以接受其他人的建议和帮助,但他们至少应该先设定好基调。


引用《人月神话》作者 Fred Brooks 的话:


稍微回顾一下,虽然许多优秀的、有用的软件系统都是由委员会设计并作为大项目的一部分进行构建的,但那些让热情的粉丝感到兴奋的软件系统一般是出自一个或几个伟大的设计师之手。


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


他们还需要一个强大的支持系统,因为语言设计师通常身处舆论的风浪之中(因为人们很关心语言功能,而且不喜欢听到“不”这个字)。最近的一个例子是 Guido van Rossum 辞去了首席 Python 语言设计师的工作,因为他受够了这种嘈杂的声音。


其他想法


这些想法也可能有助于 JavaScript 的设计和文档化:


  • 创建路线图,描述JavaScript的未来前景。这样的路线图可以讲述故事,并将很多单独的部分连接成一个连贯的整体。比如Brendan Eich的“Harmony Of My Dreams”。

  • 记录设计理念。现在,ECMAScript规范记录了“what”,但没有记录“why”。例如:可枚举性的目的是什么?

  • 一个规范的解释器。规范的半正式部分几乎已经是可执行的,如果能够像编程语言一样运行它们就更好了。


英文原文:http://2ality.com/2019/01/future-js.html


更多内容,请关注前端之巅。



2019 年 1 月 31 日 09:548061
用户头像

发布了 731 篇内容, 共 359.6 次阅读, 收获喜欢 1824 次。

关注

评论 1 条评论

发布
用户头像
not too bad!
2019 年 02 月 20 日 10:52
回复
没有更多评论了
发现更多内容

Spring整合WebSocket

牛初九

看百度技术专家如何深入研究,重复使用的代码经验——设计模式

周老师

Java 编程 程序员 架构 设计模式

深入了解 Rust 异步开发模式

lipi

rust 异步

Redis常见问题--单线程

是老郭啊

nosql redis 线程

数字化转型需要低/零代码平台的支持

代码制造者

低代码 数字化转型 企业信息化 零代码 编程开发

消息队列之事务消息,RocketMQ 和 Kafka 是如何做的?

yes的练级攻略

分布式事务 RocketMQ kafak 事务消息

NodeX Component - 滴滴集团 Node.js 生态组件体系

滴滴普惠出行

Vue+Springboot项目部署

ZRK

Vue 前后端分离 springboot 部署

Redis常见问题--哈希冲突

是老郭啊

哈希表 Redis项目

一文带你深扒ClassLoader内核,揭开它的神秘面纱!

我没有三颗心脏

Java ClassLoader java基础 类加载器

【译】Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases 上篇

花里胡哨

分布式数据库 异步 Amazon Aurora 日志驱动

开发任务管理分析报告

森林

新基建迎来风口 新人才仍有缺口

CECBC区块链专委会

人工智能 新基建 数字化基础

Python 到底是强类型语言,还是弱类型语言?

Python猫

Java c++ Python 编程

易观CTO郭炜:如何构建企业级大数据Ad-hoc查询引擎

易观大数据

LeetCode题解:155. 最小栈,单个栈同时存储最小值,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

JAVA,.NET项目开发难上手?Learun敏捷开发框架解君愁

Philips

Java 敏捷开发 .net core

Spring Boot中获取配置的一些方法

Geek_416be1

Spring Boot 2

数字货币交易平台搭建,去中心化交易所开发方案

13530558032

数字资产钱包开发,深圳区块链理财钱包服务商

13530558032

10万奖金等你拿!2020第四届易观OLAP算法大赛火热开启

易观大数据

USDT承兑商软件开发,区块链支付系统源码搭建

13530558032

合约跟单软件开发,合约跟单交易所系统开发搭建

13530558032

OpenKruise:Kubernetes 核心控制器 Plus

郭旭东

Kubernetes 云原生 OpenKruise

Redis 持久化--AOF

是老郭啊

redis redis持久化 aof

向云再出发:如数据般飞驰的内蒙古

脑极体

数字人民币钱包短暂露面 金融诈骗伺机而起

CECBC区块链专委会

数字货币 钱包 货币

开发者的福音,LR.NET模块化代码生成器

Learun

Java 敏捷开发 .net core 计算机程序设计艺术 软件设计

OFD版式技术深度解读:卷首语

华宇法律科技

版式文档 OFD

controller-manager的主动驱逐

Geek_f24c45

Kubernetes k8s

人民版权 获2020中国产业区块链创新奖

CECBC区块链专委会

区块链 产业发展 版权

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