本文主要内容翻译自《Exploring ES6-upgrade to the next version of javascript》

模块系统

  • 定义模块接口,通过接口
  • 隐藏模块的内部实现,使模块的使用者无需关注模块内部的实现细节。同时,隐藏模块的内部实现,避免有可能产生的副作用和对bug的不必要修改

ES6之前

  • CommonJS模块规范:主要在Node.js中实现(Node中也有一些超出CommonJS的特性)。主要特征

    • 紧凑的语法(可以理解为简单的语法)
    • 为同步加载和服务端所设计
  • Asynchronous Module Definition(AMD):最流行的实现是RequireJS。主要特征:
    • 略微复杂的语法,使得AMD能够脱离eval()(或者编译步骤)运行

ES6 Modules

ES6的目标是设计能够使CommonJS和AMD用户都满意的规范:

  • 类似CommonJS,语法简单,偏好单一导出,支持循环依赖
  • 类似AMD,直接支持异步加载,支持可配置加载

内置在该语言允许ES6模块超越CommonJS和AMD(细节稍后解释):

  • 语法比CommonJS更简单
  • 结构能够被静态分析(可用于静态检查,优化等等)
  • 对于循环依赖的支持比CommonJS做得更好

ES6模块分为两部分:

  • 声明式语法(导入和导出)
  • 程序加载器API:配置如何加载模块,并有条件的加载模块

ES6模块基础

有两种导出:命名导出(每个模块可有多个)和默认导出(每个模块只一个)

命名导出

模块可以通过在声明前加上关键字export来导出多个内容。这些导出由它们的名称来区分,称为命名导出。

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
} //------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); //
console.log(diag(4, 3)); //

这是最简单的一种导出方式,当然还有其他方式(稍后详细介绍)。

如果你愿意,你也可以导入整个模块,并通过属性符号引用它的命名导出:

//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); //
console.log(lib.diag(4, 3)); //

同样的功能用CommonJS完成,如下所示

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
return x * x;
}
function diag(x, y) {
return sqrt(square(x) + square(y));
}
module.exports = {
sqrt: sqrt,
square: square,
diag: diag,
}; //------ main.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); //
console.log(diag(4, 3)); //

默认导出(每个模块一个)

只导出单个值的模块在Node.js社区中非常流行。但是它们在前端开发中也很常见,在前端开发中,您通常有用于模型和组件的类,每个模块有一个类。ES6模块可以选择默认导出,即主导出值。默认导出尤其容易导入。

下面的例子导出一个单独函数

//------ myFunc.js ------
export default function () {} // no semicolon! //------ main1.js ------
import myFunc from 'myFunc';
myFunc();

如果导出一个类

//------ MyClass.js ------
export default class {} // no semicolon! //------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();

默认导出有两种方式:

  • 标签声明导出
  • 直接导出值

标签声明导出

你可以将关键字exports放在任何function声明前(或者genarator function声明前面),从而默认导出该函数

export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!

在这种情况下,也可以省略名称。这使得default导出成为JavaScript唯一具有匿名函数声明和匿名类声明的地方:

export default function () {} // no semicolon!
export default class {} // no semicolon!

为什么是匿名函数声明而不是匿名函数表达式?

当您查看前面两行代码时,您会期望export默认操作数是表达式。它们只是出于一致性的原因而声明:操作数可以被命名为声明,将它们的匿名版本解释为表达式将会令人困惑(甚至比引入新的声明类型更令人困惑)。

如果你期望操作数被解释为表达式,可以使用括号:

export default (function () {});
export default (class {});

直接导出值

这些值是表达式产生的:

export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };

他们都遵循这样的结构

export default «expression»;

等同于

const __default__ = «expression»;
export { __default__ as default }; // (A)

为什么有两种默认导出的形式?

引入第二种默认导出风格是因为如果声明多个变量,变量声明就不能有意义地转换为默认导出:

export default const foo = 1, bar = 2, baz = 3; // not legal JavaScript!

三个变量foo、bar和baz中的哪一个是默认导出?

Imports和Exports必须在顶层

稍后将更详细地解释,ES6模块的结构是静态的,您不能有条件地导入或导出东西。这带来了很多好处。

这一限制是通过只允许在模块的顶层导入和导出来实现的:

if (Math.random()) {
import 'foo'; // SyntaxError
} // You can’t even nest `import` and `export`
// inside a simple block:
{
import 'foo'; // SyntaxError

import命令被提升

模块的imports会被提升(在内部被提升到当前作用域的最开始)。因此,import放在哪里都可以

foo();

import { foo } from 'my_module';

Imports是exports的只读视图

ES6模块的导入是导出实体上的只读视图。这意味着到模块主体中声明的变量的连接仍然是活动的,如下面的代码所示:

//------ lib.js ------
export let counter = 3;
export function incCounter() {
counter++;
} //------ main.js ------
import { counter, incCounter } from './lib'; // The imported value `counter` is live
console.log(counter); //
incCounter();
console.log(counter); //

后面的部分将解释它是如何工作的。

Imports作为视图有点如下:

  • 支持循环依赖,即使是无效的导入
  • 有效导入和无效 导入工作原理是相同的(他们都是双向的)
  • 您可以将代码分割成多个模块,它将继续工作(只要您不尝试更改导入的值)

支持循环依赖

两个模块,模块A和模块B,如果A导入B,B导入A,即形成循环依赖。应该尽量避免出现这种情况——模块A和模块B会紧密耦合,他们只能被一起使用和进化。

为什么支持循环依赖呢?因为这是一个无法绕开的问题(当模块较多的情况下,难免出现循环依赖),稍后详解说明

我们来看一下CommonJS和ES6是如何支持循环依赖的。

CommonJS的循坏依赖

下面的代码能够正确的处理两个循环依赖的模块a和b

//------ a.js ------
var b = require('b');
function foo() {
b.bar();
}
exports.foo = foo; //------ b.js ------
var a = require('a'); // (i)
function bar() {
if (Math.random()) {
a.foo(); // (ii)
}
}
exports.bar = bar;

如果在第i行,首先导入模块a,模块b会在a的exports属性被赋值之前就导出了a的exprots。因此,b无法访问a.foo方法,但是一旦a执行完毕该属性就会存在。如果bar()在后面被调用,那么第ii行中的方法调用就可以工作。

一般而言,使用循环依赖时候需要注意,不能在模块主体中访问导入。这个道理同样适用于ES6模块。

CommonJS方法的局限性:

  • Nodejs形式的的导出单个值不起作用。在这里,导出的是单个值而不是对象:(这里说的就是CommonJS导出的是值的拷贝,而不是值的引用)

     module.exports = function () { ··· };

    如果模块a向上面这样导出,模块b中的变量a一旦被赋值将不会再发生改变。始终指向最初导出的对象。

    这里说的就是CommonJS导出的是值的拷贝,而不是值的引用

  • 不能直接使用命名导出。所以,模块b不能像下面这样导入foo
     var foo = require('a').foo;

    foo将会是undefined。换句话说,你只能通过a.foo引用foo。

ES6的循环依赖

ES6自动支持循环依赖。也就是说,它们没有上一节提到的CommonJS模块的两个限制,默认导出,以及未限定名称的导入(下面第i行和第iii行)。因此,您可以实现如下循环依赖于彼此的模块。

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
bar(); // (ii)
} //------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
if (Math.random()) {
foo(); // (iv)
}
}

这段代码是有效的,因为正如前一节所解释的,导入是关于导出的视图。这意味着即使是不合格的导入(如第2行中的bar和第4行中的foo)也是引用原始数据的间接引用。因此,面对循环依赖关系,无论您是通过非限定导入访问命名导出,还是通过它的模块访问命名导出,都没有关系:在这两种情况中都涉及到间接,而且它总是有效的。

导入和导出的细节

未完待续……

参考

  1. 《javascript 忍者秘籍—https://livebook.manning.com/#!/book/secrets-of-the-javascript-ninja-second-edition/
  2. 《exploring Es6-upgrade to the next version of javascript》—https://exploringjs.com/es6/
  3. 《understanding ECMAScript》—https://github.com/nzakas/understandinges6
  4. 《You-Dont-Know-JS》—https://github.com/getify/You-Dont-Know-JS
  5. 《pritical modern javascript》—https://github.com/mjavascript/practical-modern-javascript

JS模块的更多相关文章

  1. js模块开发(一)

    现在嵌入页面里面的javascript代码越来越复杂,于是可能依赖也越来越严重,使用别人开发的js也越来越多,于是在理想情况下,我们只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块. 于是j ...

  2. 第三课:sea.js模块加载原理

    模块加载,其实就是把js分成很多个模块,便于开发和维护.因此加载很多js模块的时候,需要动态的加载,以便提高用户体验. 在介绍模块加载库之前,先介绍一个方法. 动态加载js方法: function l ...

  3. JS模块规范 前端模块管理器

    一:JS模块规范(为了将js文件像java类一样被import和使用而定义为模块, 组织js文件,实现良好的文件层次结构.调用结构) A:CommonJS就是为JS的表现来制定规范,因为js没有模块的 ...

  4. Node.js模块

    每一个Node.js都是一个Node.js模块,包括JavaScript文件(.js).JSON文本文件(.json)和二进制模块文件(.node). mymodul.js function Hell ...

  5. 使用seajs封装js模块

    //方法一:将函数绑定到原型上 define(function(require, exports, module) { $.fn.tab = function(option, callback) { ...

  6. nodejs js模块加载

    本文地址:http://www.cnblogs.com/jasonxuli/p/4381747.html nodejs的非核心模块(core module)加载主要使用的就是module.js. 项目 ...

  7. js模块开发

    js模块开发(一) 现在嵌入页面里面的javascript代码越来越复杂,于是可能依赖也越来越严重,使用别人开发的js也越来越多,于是在理想情况下,我们只需要实现核心的业务逻辑,其他都可以加载别人已经 ...

  8. 如何发布一个自定义Node.js模块到NPM(详细步骤)

    咱们闲话不多说,直接开始! 由于我从没有使用过MAC,所以我不保证本文中介绍的操作与MAC一致. 文章开始我先假定各位已经在window全局安装了Node.js,下面开始进行详细步骤介绍: 本文本着, ...

  9. 【 js 模块加载 】深入学习模块化加载(node.js 模块源码)

    一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须按照一定的格式编写.AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范.  1.AMD(Asy ...

  10. 编写原生Node.js模块

    导语:当Javascript的性能需要优化,或者需要增强Javascript能力的时候,就需要依赖native模块来实现了. 应用场景 日常工作中,我们经常需要将原生的Node.js模块做为依赖并在项 ...

随机推荐

  1. conda 里的 jupyter

    1. 安装conda https://mirrors.tuna.tsinghua.edu.cn/anaconda/ 下载并安装. 2. 安装jupyter (1)在ananconda主环境安装:pip ...

  2. HTML5视音频标签参考

    本文将介绍HTML5中的视音频标签和对应的DOM对象.是相关资料的中文化版本,可以作为编写相关应用的简易中文参考手册. 一些约定 所有浏览器:指支持HTML5的常见桌面浏览器,包括IE9+.Firef ...

  3. 文件系统(node.js学习笔记)

    根据nodejs菜鸟教程整理. 官方API文档:nodeJS文件系统API 其他整理:nodejs File System 文件系统操作函数分类 1.引用: 导入文件系统模块(fs)语句:var fs ...

  4. TVYJ1266:费解的开关

    我对状态空间的理解:https://www.cnblogs.com/AKMer/p/9622590.html 题目传送门:http://www.joyoi.cn/problem/tyvj-1266 这 ...

  5. Azure CLI下载Azure Storage Container内的所有文件

    在某些场景下,客户需要把Azure Storage的某一个container内的内容都下载到本地.当然采用PowerShell可以定时的进行下载的动作,但有时客户的环境是Linux或MacOS,这时需 ...

  6. delphi 线程教学第一节:初识多线程

    第一节:初识多线程   1.为什么要学习多线程编程?   多线程(多个线程同时运行)编程,亦可称之为异步编程. 有了多线程,主界面才不会因为耗时代码而造成“假死“状态. 有了多线程,才能使多个任务同时 ...

  7. SpringMvc之参数绑定注解详解之二

    2 consumes.produces 示例 cousumes的样例: 1 @Controller   2 @RequestMapping(value = "/pets", met ...

  8. 利用httpclient和mysql模拟搜索引擎

    数据抓取模块 package crowling1; import java.sql.CallableStatement; import java.sql.Connection; import java ...

  9. 加减 script函数初识

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. crontab简易入门

    前言 crontab是Unix和Linux用于设置周期性被执行的指令,是互联网很常用的技术,很多任务都会设置在crontab循环执行,如果不使用crontab,那么任务就是常驻程序,这对你的程序要求比 ...