Static module resolution

http://calculist.org/blog/2012/06/29/static-module-resolution/
关于ES6模块中声明式静态模块解析的基本原理,我讲得还不够多。由于纯JS中存在多模块系统,因此涉及新语法的模块概念对人们来说是陌生的。我想解释一下动机。

首先,快速解释一下这是怎么回事。在像CommonJS这样的纯JS系统中,模块只是对象,它们导出的任何定义都可以通过对象属性查找由客户机导入:

var { stat, exists, readFile } = require('fs');

相比之下,在ES6模块系统中,模块不是对象,而是代码的声明性集合。从模块导入定义也是声明性的:

import { stat, exists, readFile } from 'fs';
此导入在编译时解决,即在脚本开始执行之前。声明性模块依赖关系图的所有导入和导出都会在执行之前解析。(还有一个异步动态加载API;当然,能够将模块加载推迟到运行时是很重要的。但本文是关于声明性模块依赖关系图的解析。)

关于规范的起源

node领导人认为,我们应该采取更多的渐进式步骤,更紧密地挖掘现有的模块系统。我非常同情“铺平牛郎织女”的哲学,而且我经常为此争论不休。但迄今为止,人们为JavaScript构建的模块系统没有修改语言的选项。我们有机会将JS推向纯动态系统永远无法实现的方向。

这些方向有哪些?

快速查找

静态导入(无论是通过import还是像m.foo这样的引用)都可以像变量引用一样编译。在动态模块系统中,像m.foo这样的显式取消引用将是一个对象引用,通常需要PIC保护(PIC guards)。如果您将它们复制到局部变量中,在某些情况下它们将更容易优化,但对于静态模块,您总是可以预见得到早期绑定。保持模块引用与变量引用一样便宜,可以使模块化程序更快,并避免对模块化代码负担。

早期变量检查

根据我的经验,在脚本开始运行之前检查变量引用,包括导入和导出,对于确保程序的基本顶层结构是合理的非常有用。JavaScript几乎是静态作用域,这是我们唯一的机会。詹姆斯·伯克(James Burke)认为这是一种肤浅的类型检查 http://tagneto.blogspot.com/2012/06/es-modules-suggestions-for-improvement.html ,他声称这还不够有用。我在其他语言方面的经验正好相反——它非常有用!变量检查是一个很好的地方,在这里您仍然可以编写富有表现力的动态程序,但可以捕获真正基本的常见错误。正如Anton Kovalyov所指出的那样,无绑定变量报告是JSHint中的一个流行特性,不必运行单独的linter来捕获这些bug就更好了。

循环依赖项

允许模块之间的循环依赖关系非常重要。相互递归是编程的一个事实。有时你甚至没有注意到它。如果您尝试将程序拆分为多个模块,而系统由于无法处理周期而崩溃,那么最简单的解决方法就是将所有内容都放在一个大模块中。模块系统不应阻止程序员拆分他们的程序,尽管他们认为合适。它们不应妨碍编写模块化程序。

对于动态系统来说,这不是不可能的,但我认为这往往是替代方案事后才考虑的。这是我们为ES6仔细考虑过的事情。此外,声明性模块允许您在执行任何代码之前预初始化更多的模块结构,这样,如果有人引用尚未分配的导出,您可以给出更好的错误。例如,如果在赋值之前引用let绑定,则会抛出该绑定,并且会得到一条明确的错误消息。这比引用一个动态模块对象的属性更容易诊断,因为它还没有出现,没有定义,必须将最终错误追溯到源代码。

宏的未来兼容性

我希望在JavaScript的未来看到的一件事是程序员能够提出自己的自定义语法扩展,而不必等待TC39添加它。今天,人们通过编写自己的编译器来发明新的语法。但这是非常困难的,您不能在单个源文件中使用不同编译器的不同语法特性。

使用宏,您可以实现一种新的cond语法,它可以更好地替代链接?:条件,并通过库共享:

import cond from 'cond.js';
...
var type = cond {
    case (x === null): "null",
    case Array.isArray(x): "array",
    case (typeof x === "object"): "object",
    default: typeof x
};

第二个宏会在程序运行之前将其预处理成一系列条件。预处理不适用于纯动态模块:

var cond = require('cond.js');
...
// impossible to preprocess because we haven't evaluated the require!
var type = cond { /* etc */ };

类型的未来兼容性

我在倒霉的ES4天加入了TC39,当时委员会正在为JS制定可选的类型系统。它建在粗糙的地基上,最终瓦解了。真正缺少的东西之一是一个模块系统,在这个系统中,你可以围绕一段代码画一个边界,并说“这个部分需要进行类型检查”。否则,你永远不知道以后是否会添加更多的代码。

为什么要types?原因之一是:JS速度越来越快,但预测性能却越来越难。通过像LLJShttp://lljs.org/这样的实验,我在Mozilla的团队正在使用JS方言,它们使用类型离线预编译,并生成一些针对当前JIT优化的非常时髦的JS代码。但是,如果您可以直接用类型化的JS方言编写高性能内核,那么现代编译器可以使用它。

通过声明性解析,您可以导入和导出类型化定义,并且可以在编译时对它们进行检查。无法静态检查动态导入。

语言间模块化

有些人可能不关心或不想要宏或类型之类的功能。但是JavaScript必须为许多不同的程序员提供服务,他们有许多不同的开发实践和需求。这样做的方法之一是允许人们使用自己的语言编译成JavaScript。因此,即使宏或类型不在ECMAScript标准的未来,如果您可以使用静态类型或支持宏的离线JS方言,编译成与浏览器兼容的JS,那就太好了。如今,人们已经在使用Closure编译器的类型检查、Roy语言或ClojureScript进行这类工作。静态模块系统更普遍、更直接地与更广泛的语言兼容。

成本和收益

以上是我看到的声明性模块解析的一些好处。Isaac Schlueter说导入语法没有添加任何内容。这是不公平和错误的。它是有目的的。我不认为声明性导入语法对ES6和潜在的未来版本都有很大的好处。

PS(postscript):Python到底是怎么回事?

最后一件事:人们一直声称ES6模块系统来自Python。我甚至没有多少Python的经验。Python的模块更具可变性,其范围更具动态性。就个人而言,我从Racket中得到了灵感,它从声明性模块系统中获得了很多好处。他们利用静态模块构建宏系统、早期变量检查、优化引用、基于模块的责备报告动态契约、多语言互操作性和静态类型的方言。

我对把JavaScript变成其他语言不感兴趣。但你可以从学习其他语言的先例中学到很多东西。我亲眼看到了动态语言中的声明性模块系统的好处。

posted @ 2022-10-10 20:58  Running00  阅读(12)  评论(0编辑  收藏  举报