本文是Dave Herman的《Static module resolution》一文的编译。Dave Herman是TC39的成员,ES6 module系统的champion。【ES6 spec太大了,所以分成许多可相对独立的特性集合,分别交给一个或几个主导人负责,TC39委员会则会定期开会进行审阅和讨论。主导人就称之为champion。】
在纯JS环境下已经有多种模块系统。比如CommonJS。所谓纯JS系统,就是不依赖其他机制如预处理之类的。纯JS系统中的模块都是一个个对象。客户代码导入模块所导出的定义,实际上是查找module对象上的属性:
var { stat, exists, readFile } = require('fs');
ES6模块系统则相反,模块不是对象,而是声明式的代码集合。从模块导入定义也是声明式的:
import { stat, exists, readFile } from 'fs';
这个import是在编译时resolve的——即在脚本开始执行之前。事实上,各个模块之间的依赖关系图所涉及的所有imports和exports都是在执行之前resolve好了。当然,我们也有lazy loading或按需加载的需求,即在运行时才进行模块加载。对此,ES6也有异步的模块动态加载API。不过本文只讨论声明式模块依赖关系图的解析。
NodeJS的作者认为我们应该走渐进的、改良式的道路,认为ES6的模块系统应该更接近今天已经存在着的模块系统。我也相当赞同“pave the cowpaths”哲学(遵循事实标准),并常以此立论,但是必须注意到,现有JS模块系统的作者们从未有过从语言层面做修改的可能性,而我们现在却有机会改变JS,选择在纯动态系统中没可能走的道路,包括:
快速查找
静态import(无论是通过import还是如m.foo的引用)可以编译为如同简单的变量引用一样。在动态模块系统中,像m.foo这样的显式用引(dereference)会得到一个对象引用,通常需要PIC才能优化(Polymorphic Inline Caching,多态内联缓存,JavaScript引擎在执行时动态修改JIT代码的高级优化技术)。如果是复制到局部变量,相对来说会较容易进行优化。但是对于静态模块来说,总是早期绑定,也就是始终和变量引用一样高效。这使得模块化的程序能运行更快,避免了因为模块化导致额外性能成本。
早期变量检查
依我的经验,在脚本执行前若能对变量引用——包括imports和exports——进行检查,非常有助于确保程序顶层的基础结构是健全的。JavaScript基本上是静态作用域的,因此可以进行静态作用域检查,这也是唯一可以做的检查。James Burke认为这只是shallow type checking(浅类型检查,而不是强类型检查),不够有用。但我在其他语言的经验表明正相反——这超级有用!变量检查是一个最佳平衡点,你可以写出富有表达力的动态程序,同时又能捕捉到那些真的很常见的错误。如Anton Kovalyov指出的,报告未绑定的变量是JSHint的最常用特性,如果不必借助额外的lint工具就能捕捉这些bug那就再好不过了。
循环依赖
允许模块间循环依赖是非常重要的。现实情况是编程中可能出现相互间的递归调用——有时你甚至都没注意到。如果你将程序拆分模块后,由于不能处理循环依赖结果系统挂了,那最简单的workaround就是继续把所有东西都堆到一个大模块中。这肯定有问题。无论如何,模块系统不应该阻止程序员拆分程序,不应该挫伤程序员模块化的积极性。
不是说动态系统就不可能支持循环依赖,但是我觉得在那些提案中看起来都像是事后补丁。ES6的静态模块系统则仔细的考虑了循环依赖问题。声明式的模块让你可以在执行任何代码前预初始化更多的模块结构,这样如果引用尚未赋值的export,能得到更好的错误信息。例如,一个let绑定会扔出异常——如果你在它被赋值之前就引用它的话——你可以得到清晰的错误信息。而一个动态模块对象上的属性如果还未赋值就被引用,得到的是undefined,最终错误可能发生在客户代码中,必须跟踪这个错误直到源头——这比异常要难调试太多了。
兼容未来的macro特性
我非常期待JavaScript未来能让程序员可以发展他们自己的定制语法扩展,而不必等待TC39。今天,人们自个儿写编译器来弄新语法。但是这个极难,而且你不能在同一个源文件里使用不同编译器提供的不同语法特性。
有了macro,你就可以实现,比如说一个新的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
};
cond这个macro会在程序运行前进行预处理,将这段代码转换为连续的条件分支。而纯动态模块是无法实现预处理的:
var cond = require('cond.js');
...
// impossible to preprocess because we haven't evaluated the require!
var type = cond { /* etc */ };
兼容未来的类型系统
在悲剧的ES4时代我就加入了TC39,当时委员会在搞一个可选的类型系统。这系统基础不全最终废弃。其中一个重要缺失就是模块系统,通过模块系统可以将代码划定边界并说“这部分需要类型检查”。否则你永远不知道是否有更多后续代码会影响类型检查。
为什么要有类型系统?一个原因是:JS很快且越来越快,但是也更难准确预测性能。通过类似LLJS的试验性系统,我在Mozilla的团队使用带有类型的JS方言进行预编译,生成相当独特的为当前JIT优化的JS代码。如果你可以直接用带类型系统的JS方言写出高性能核心,现代编译器可以做得更好而不用如此曲折。
通过声明性的解析,你可以导入和导出带有类型信息的定义,并可进行编译时检查。动态导入不可能进行静态检查。
跨语言的模块性
一些人不care或者不想要像macro或类型这样的特性。但是JavaScript必须适应许多不同的程序员的各种不同的开发实践和需求。其中一种方式是让人们使用他们自己的语言,并编译为JavaScript。所以即使未来的ECMAScript标准没有macro和类型,若你可以使用静态类型或带有macro的JS方言并编译为浏览器可执行的JS,也是相当好的。实际上人们已经这样干了,比如用Closure compiler的类型检查、Roy语言、ClojureScript等。静态模块系统可以更一致更直接的兼容更多的语言。
成本和收益
以上是一些我看到的声明性模块解析的收益。Isaac Schlueter(NodeJS的作者)说import语法无甚意义。这是不公正和错误的。它是有意义的。我也不认为声明性的import语法会给ES6和未来的JS版本增加很高的成本。
分享到:
相关推荐
1. **静态分析**:Rollup 可以在编译时完全解析模块依赖关系,因为 ES6 模块是静态的,这使得它能够进行更高效的代码优化。 2. **Tree Shaking**:通过分析模块间的引用,Rollup 能够移除未被引用的代码,这在大型...
3. **ES6模块**:ECMAScript 6引入了新的模块系统,使用`import`和`export`关键字进行导入和导出。Webpack可以理解这种语法,并据此进行代码分割。 4. **CSS和静态资源处理**:Webpack不仅处理JavaScript,还可以...
转译过程的一部分是验证和解析过程,这是 ES6 模块的好处之一,如果您尝试导入的模块不可用,或者您引用的命名导出不可用,则报告静态错误存在。 默认情况下,转译器实现了一个相对路径解析过程,这意味着只有公共...
ES6的模块系统支持静态加载和静态解析,这与CommonJS(Node.js中使用)和AMD(RequireJS中使用)的动态加载有所不同。 3. **Node.js**:Node.js是一个基于Chrome V8引擎的JavaScript运行环境,它让JavaScript可以在...
2. **静态性**:ES6模块是静态的,可以在编译时确定所有依赖,利于优化。 3. **命名导入与导出**:允许按需导入特定的函数或变量,避免污染全局命名空间。 4. **默认与星号导入**:支持默认导出和`*`通配符导入,...
**ES6 模块**,也称为静态模块,使用 `import` 和 `export` 关键字进行导入和导出。与 CommonJS 不同,ES6 模块的加载是**异步的**,并且是**静态的**。这意味着在解析时,`import` 语句会被转换成指向模块的引用,...
ES6模块是静态引用,这意味着在编译时就会确定模块的依赖关系,而且模块的值是实时绑定的,模块内部的改变会影响到外部引用。与CommonJS不同,ES6模块不支持动态导入,但可以通过Babel等工具将ES6模块转换为CommonJS...
2. **ES6模块**:ECMAScript 6(简称ES6)引入的新的模块系统,通过 `import` 和 `export` 关键字实现代码的组织和复用。 3. **Git**:分布式版本控制系统,用于跟踪代码的变更和协作开发。 4. **npm**:Node.js ...
它可以加载各种模块格式,包括 CommonJS、AMD 和 ES6 模块,还支持模块转换和插件系统。 6. **Webpack** 和 **Rollup**: 这些是现代前端构建工具,它们可以将多种模块格式转换为统一的输出格式,通常是 ES6 模块...
3. **ES6模块**:ECMAScript 6引入了模块系统,使得JavaScript代码可以组织成可重用的模块,通过import和export关键字进行导入和导出。 4. **npm**:Node.js包管理器,用于安装和管理项目依赖。在这个项目中,"npm ...
ES6(ECMAScript 2015)引入了模块系统,使得 JavaScript 开发者能够更清晰地组织和导入/导出代码。在 "restaurantPage" 项目中,开发者可能使用了 `import` 和 `export` 关键字来分割代码,使得每个功能或组件独立...
2. 运行分析:使用命令`plato -d report -r es6 -l .`,其中`-d`指定输出报告的目录,`-r`指定解析的语法(这里为ES6),`-l`指定分析的源代码目录。 3. 查看报告:生成的HTML报告可以在浏览器中打开,以图表形式...
**ES6 深度解析** ES6,全称ECMAScript 2015,是JavaScript语言的一个重要版本更新,引入了许多新特性,极大地提升了开发者的工作效率和代码质量。这个资源包“ES6-in-depth”包含了一份详细的PDF文档,旨在深入...
gulp是一个基于Node.js的自动化任务运行器,它允许开发者自定义一系列的任务来简化前端开发流程,例如编译Sass、转换ES6到ES5、压缩静态资源以及自动添加浏览器兼容性前缀。 标题中的“支持编译Sass”指的是使用...
5. **模块系统**:通过export和import关键字,ES6实现了模块化,使得代码组织更加有序,避免了全局变量污染,支持按需导入和导出。 6. **解构赋值**:允许从数组或对象中提取数据,赋值给不同的变量,如`[a, b] = ...
但ES6模块使用的是静态导入,与这两种规范不同,所以直接在HTML中引入转换后的ES5文件仍会出现错误。 为了解决这个问题,我们可以使用模块打包工具,如Webpack。Webpack是一个强大的模块打包工具,它能够将各种类型...
在项目中,Webpack负责处理ES6模块、样式、图片等静态资源的加载和优化。它还可以通过插件和loader系统扩展其功能,例如使用Babel进行转译。Webpack配置文件(通常是webpack.config.js)定义了资源如何被处理和打包...
ESX Transpile 库通过解析 ES6 模块的语法树,然后将 `import` 和 `export` 语句替换为相应的 AMD 语法。例如,对于上面的 ES6 模块示例,转换后的 AMD 代码可能是: ```javascript // 导出 define('myModule', [],...
ES6 的模块化机制则是静态加载,更利于编译时优化,同时也更符合静态语言的模块系统。 ### 2. `export` 命令 `export` 是用于导出模块中公有成员的关键字。它可以在模块的任何位置使用,可以导出变量、函数、类...
`json2module`是一个实用的前端工具,它有效地解决了JSON数据在ES6模块系统中的应用问题。通过这个库,开发者可以更加高效地处理JSON数据,提高开发效率,同时保持代码的整洁和模块化。在实际项目中,尤其是在数据...