commonJS模块规范,的加载实现

By admin in 4858美高梅 on 2019年4月1日

烂笔头开首记录小知识点啦~

Module 的加载达成

  1. 浏览器加载
  2. ES6 模块与 CommonJS
    模块的差异
  3. Node
    加载
  4. 循环加载
  5. ES6模块的转码

上一章介绍了模块的语法,本章介绍怎样在浏览器和 Node 之中加载 ES6
模块,以及实际开发中时时碰到的有个别标题(比如循环加载)。

浏览器加载

ES6 模块与 CommonJS 模块的歧异

  1. CommonJS 模块输出的是多个值的正片,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运维时加载,ES6 模块是编写翻译时输出接口。
  3. CommonJS
    加载的是一个目的(即module.exports属性),该目的唯有在剧本运转完才会变卦。而
    ES6
    模块不是目的,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  • 在 ES6 在此以前,社区制订了一些模块加载方案,最重庆大学的有 CommonJS 和 英特尔三种。前者用于服务器,后者用于浏览器。ES6
    在言语专业的范畴上,达成了模块作用,而且完成得卓殊简单,完全能够取代
    CommonJS 和 英特尔 规范,成为浏览器和服务器通用的模块化解方案。
    ES6
    模块的安插思想,是拼命三郎的静态化,使得编写翻译时就能鲜明模块的重视关系,以及输入和输出的变量。CommonJS
    和 英特尔 模块,都只还好运维时规定那个事物。比如,CommonJS
    模块正是目的,输入时务必寻找对象属性。

  • commonJS 和 es6模块化 区别:
    es6 {
      export : ‘能够输出多个,输出形式为 {}’ ,
      export default : ‘ 只可以输出二个 ,能够与export
    同时输出,然则不提出如此做’,
      解析阶段鲜明对外输出的接口,解析阶段生成接口,
      模块不是目的,加载的不是目的,
      可以单独加载在那之中的某部接口(方法),
      静态分析,动态引用,输出的是值的引用,值改变,引用也改变,即原来模块中的值改变则该加载的值也变更,
      this 指向undefined
    }
    commonJS {
      module.exports = … : ‘只好输出1个,且前边的会覆盖上面包车型客车’ ,
      exports. … : ‘ 能够出口多少个’,
      运营阶段显明接口,运营时才会加载模块,
      模块正是指标,加载的是该目的,
      加载的是全方位模块,即将有所的接口全部加载进来,
      输出的是值的正片,即原来模块中的值改变不会影响已经加载的该值,
      this 指向当前模块
    }

  • 浏览器要加载 ES6模块,:

    <script type="module" src="./foo.js"></script>
    

     

  • 异步加载,非凡与defer属性。能够此外安装async属性。

  • ES6
    模块也同意内嵌在网页中,语法行为与加载外部脚本完全一致。

    <script type="module">
      import utils from "./utils.js";
    
      // other code
    </script>
    

     

  • CommonJS 模块输出的是贰个值的正片,ES6
    模块输出的是值的引用。

  • CommonJS 模块是运作时加载,ES6
    模块是编写翻译时输出接口。

浏览器加载

commonJS模块规范,的加载实现。观念方法


HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

下边代码中,由于浏览器脚本的默许语言是
JavaScript,因而type="application/javascript"可以不难。

暗中认可情状下,浏览器是一起加载
JavaScript
脚本,即渲染引擎遭遇<script>标签就会停下来,等到执行完脚本,再持续向下渲染。即便是外部脚本,还非得加入脚本下载的时光。

假使脚本体量相当大,下载和施行的小时就会相当短,因而成浏览器堵塞,用户会深感到浏览器“卡死”了,没有其余响应。那显然是很不佳的体验,所以浏览器允许脚本异步加载,上边就是两种异步加载的语法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

地点代码中,<script>标签打开deferasync属性,脚本就会异步加载。渲染引擎碰着这一行命令,就会起首下载外部脚本,但不会等它下载和施行,而是径直执行后边的授命。

deferasync的区分是:前者要等到全体页面平日渲染结束,才会履行;后者一旦下载完,渲染引擎就会有始无终渲染,执行那一个本子以往,再持续渲染。一句话,defer是“渲染完再实施”,async是“下载完就进行”。其余,假若有四个defer本子,会规行矩步它们在页面出现的相继加载,而三个async脚本是不能够确认保障加载顺序的。

历史观方法

在 HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

地点代码中,由于浏览器脚本的暗许语言是
JavaScript,由此type="application/javascript"能够不难。

暗中同意情形下,浏览器是联合署名加载 JavaScript
脚本,即渲染引擎碰着<script>标签就会停下来,等到执行完脚本,再持续向下渲染。就算是表面脚本,还必须进入脚本下载的时光。

假诺脚本容积十分大,下载和执行的小时就会非常长,因而成浏览器堵塞,用户会感觉到到浏览器“卡死”了,没有其余响应。那明显是很倒霉的体验,所以浏览器允许脚本异步加载,上边便是二种异步加载的语法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代码中,<script>标签打开deferasync质量,脚本就会异步加载。渲染引擎遭逢这一行命令,就会起来下载外部脚本,但不会等它下载和执行,而是一向实施前面包车型客车通令。

deferasync的界别是:前者要等到全体页面平时渲染甘休,才会进行;后者一旦下载完,渲染引擎就会半途而返渲染,执行那个剧本未来,再持续渲染。一句话,defer是“渲染完再履行”,async是“下载完就实施”。其它,假诺有八个defer剧本,会听从它们在页面出现的相继加载,而八个async脚本是不可能担保加载顺序的。

加载规则

浏览器加载
ES6 模块,也选择<script>标签,但是要参与type="module"属性。

<script type="module" src="foo.js"></script>

上面代码在网页中插入3个模块foo.js,由于type品质设为module,所以浏览器知道那是八个ES6 模块。

浏览器对于富含type="module"<script>,都以异步加载,不会招致堵塞浏览器,即等到整个页面渲染完,再实施模块脚本,等同于打开了<script>标签的defer属性。

<script type="module" src="foo.js"></script>
<!-- 等同于 -->
<script type="module" src="foo.js" defer></script>

<script>标签的async质量也足以打开,那时只要加载成功,渲染引擎就会搁浅渲染即刻执行。执行到位后,再回复渲染。

<script type="module" src="foo.js" async></script>

ES6
模块也同意内嵌在网页中,语法行为与加载外部脚本完全一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

对于外部的模块脚本(上例是foo.js),有几点须要注意。

  • 代码是在模块作用域之中运转,而不是在大局效率域运维。模块内部的顶层变量,外部不可知。
  • 模块脚本自动选拔严苛情势,不管有没有扬言use strict
  • 模块之中,能够行使import一声令下加载其余模块(.js后缀不可省略,供给提供绝对USportageL 或绝对 U智跑L),也足以选取export指令输出对外接口。
  • 模块之中,顶层的this重要字再次回到undefined,而不是指向window。也正是说,在模块顶层使用this根本字,是虚幻的。
  • 同一个模块假设加载多次,将只进行一遍。

上面是三个示范模块。

import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

delete x; // 句法错误,严格模式禁止删除变量

采取顶层的this等于undefined其一语法点,能够侦测当前代码是不是在
ES6 模块之中。

const isNotModuleScript = this !== undefined;

加载规则

浏览器加载 ES6
模块,也利用<script>标签,不过要加入type="module"属性。

<script type="module" src="foo.js"></script>

地点代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道那是二个ES6 模块。

浏览器对于富含type="module"<script>,都以异步加载,不会促成堵塞浏览器,即等到任何页面渲染完,再实践模块脚本,等同于打开了<script>标签的defer属性。

<script type="module" src="foo.js"></script>
<!-- 等同于 -->
<script type="module" src="foo.js" defer></script>

<script>标签的async属性也得以打开,那时只要加载成功,渲染引擎就会搁浅渲染马上实施。执行到位后,再苏醒渲染。

<script type="module" src="foo.js" async></script>

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

对此外部的模块脚本(上例是foo.js),有几点须要留意。

  • 代码是在模块功能域之中运营,而不是在全局成效域运营。模块内部的顶层变量,外部不可知。
  • 模块脚本自动采取严俊情势,不管有没有扬言use strict
  • 模块之中,能够动用import指令加载其余模块(.js后缀不可省略,须求提供相对U卡宴L 或相对 U安德拉L),也得以接纳export命令输出对外接口。
  • 模块之中,顶层的this重在字重返undefined,而不是指向window。也正是说,在模块顶层使用this首要字,是抽象的。
  • 同2个模块如果加载多次,将只进行2回。

下边是一个示范模块。

import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

delete x; // 句法错误,严格模式禁止删除变量

行使顶层的this等于undefined本条语法点,可以侦测当前代码是还是不是在 ES6
模块之中。

const isNotModuleScript = this !== undefined;

ES6 模块与 CommonJS 模块的异样

议论
Node 加载 ES6 模块在此之前,必须询问 ES6 模块与 CommonJS
模块完全区别。

它们有多个重点差距。

  • CommonJS
    模块输出的是四个值的正片,ES6 模块输出的是值的引用。
  • CommonJS
    模块是运作时加载,ES6 模块是编写翻译时输出接口。

第二个出入是因为
CommonJS
加载的是三个目的(即module.exports天性),该指标唯有在本子运转完才会转移。而
ES6
模块不是目的,它的对外接口只是一种静态定义,在代码静态解析阶段就会转变。

上面重点表明第①个出入。

CommonJS
模块输出的是值的正片,也正是说,一旦输出1个值,模块内部的浮动就影响不到那些值。请看上面那些模块文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

地点代码输出内部变量counter和改写这么些变量的内部方法incCounter。然后,在main.js里面加载那些模块。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上边代码表达,lib.js模块加载未来,它的中间变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成3个函数,才能博得内部变动后的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

地点代码中,输出的counter质量实际上是一个取值器函数。未来再实行main.js,就能够正确读取内部变量counter的改动了。

$ node main.js
3
4

ES6
模块的运转搭飞机制与 CommonJS 不均等。JS
引擎对台本静态分析的时候,蒙受模块加载命令import,就会生成3个只读引用。等到脚本真的实行时,再依据那几个只读引用,到被加载的不行模块里面去取值。换句话说,ES6
import有点像 Unix
系统的“符号连接”,原始值变了,import加载的值也会随之变。由此,ES6
模块是动态引用,并且不会缓存值,模块里面包车型地铁变量绑定其所在的模块。

或然举上边的事例。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

上面代码表达,ES6
模块输入的变量counter是活的,完全反应其所在模块lib.js中间的变更。

再举一个涌出在export一节中的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上边代码中,m1.js的变量foo4858美高梅 ,,在刚加载时卓殊bar,过了500微秒,又成为等于baz

让我们看看,m2.js可以还是不可以正确读取这些变化。

$ babel-node m2.js

bar
baz

上边代码注解,ES6
模块不会缓存运营结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

由于
ES6
输入的模块变量,只是1个“符号连接”,所以这么些变量是只读的,对它实行重复赋值会报错。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上边代码中,main.jslib.js输入变量obj,可以对obj添加属性,不过再次赋值就会报错。因为变量obj针对的地点是只读的,不能够再度赋值,这就好比main.js创造了二个名为objconst变量。

最后,export因而接口,输出的是同一个值。分化的剧本加载这么些接口,获得的都以千篇一律的实例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

地点的本子mod.js,输出的是三个C的实例。差异的本子加载那些模块,获得的都是同3个实例。

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

以往实践main.js,输出的是1

$ babel-node main.js
1

那就表明了x.jsy.js加载的都以C的同三个实例。

ES6 模块与 CommonJS 模块的反差

商讨 Node 加载 ES6 模块在此以前,必须领会 ES6 模块与 CommonJS 模块完全差异。

它们有三个重点差距。

  • CommonJS 模块输出的是一个值的正片,ES6 模块输出的是值的引用。
  • CommonJS 模块是运作时加载,ES6 模块是编写翻译时输出接口。

第3个不相同是因为 CommonJS
加载的是1个目标(即module.exports属性),该目的唯有在本子运维完才会生成。而
ES6
模块不是指标,它的对外接口只是一种静态定义,在代码静态解析阶段就会变卦。

下边重点表明第三个出入。

CommonJS
模块输出的是值的正片,也便是说,一旦输出二个值,模块内部的转变就影响不到那几个值。请看上边这些模块文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

下边代码输出内部变量counter和改写这么些变量的内部方法incCounter。然后,在main.js里头加载那几个模块。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上边代码表达,lib.js模块加载未来,它的中间变化就影响不到输出的mod.counter了。那是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能收获内部变动后的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

地点代码中,输出的counter属性实际上是三个取值器函数。今后再实行main.js,就能够正确读取内部变量counter的改变了。

$ node main.js
3
4

ES6 模块的运营机制与 CommonJS 分裂。JS
引擎对台本静态分析的时候,碰到模块加载命令import,就会变动七个只读引用。等到脚本真的举办时,再依据那么些只读引用,到被加载的不行模块里面去取值。换句话说,ES6
import有点像 Unix
系统的“符号连接”,原始值变了,import加载的值也会随着变。由此,ES6
模块是动态引用,并且不会缓存值,模块里面包车型地铁变量绑定其所在的模块。

只怕举上边的例证。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

上边代码表明,ES6
模块输入的变量counter是活的,完全反应其所在模块lib.js中间的更动。

再举三个涌出在export一节中的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上边代码中,m1.js的变量foo,在刚加载时非常bar,过了500微秒,又成为等于baz

让大家看看,m2.js能不能够正确读取那么些变化。

$ babel-node m2.js

bar
baz

下边代码表明,ES6
模块不会缓存运转结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

由于 ES6
输入的模块变量,只是3个“符号连接”,所以那些变量是只读的,对它举办重复赋值会报错。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上边代码中,main.jslib.js输入变量obj,可以对obj添加属性,不过再度赋值就会报错。因为变量obj针对的地址是只读的,不能够重复赋值,那就好比main.js创建了一个名为objconst变量。

最后,export由此接口,输出的是同多个值。差别的剧本加载那个接口,获得的皆以一致的实例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

地方的本子mod.js,输出的是三个C的实例。分化的本子加载那么些模块,获得的都以同3个实例。

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

前几天实践main.js,输出的是1

$ babel-node main.js
1

那就表达了x.jsy.js加载的都是C的同五个实例。

Node 加载

Node 加载

概述

Node
对 ES6 模块的拍卖相比较麻烦,因为它有投机的 CommonJS 模块格式,与 ES6
模块格式是不协作的。方今的缓解方案是,将两端分别,ES6 模块和 CommonJS
采取各自的加载方案。

在静态分析阶段,2个模块脚本只要有一行importexport言语,Node
就会觉得该脚本为 ES6 模块,不然就为 CommonJS
模块。就算不出口任何接口,然而希望被 Node 认为是 ES6
模块,能够在本子中加一行语句。

export {};

地点的吩咐并不是出口贰个空对象,而是不出口任何接口的
ES6 标准写法。

怎么不钦定相对路径,Node
加载 ES6 模块会相继寻找以下脚本,与require()的规则一样。

import './foo';
// 依次寻找
//   ./foo.js
//   ./foo/package.json
//   ./foo/index.js

import 'baz';
// 依次寻找
//   ./node_modules/baz.js
//   ./node_modules/baz/package.json
//   ./node_modules/baz/index.js
// 寻找上一级目录
//   ../node_modules/baz.js
//   ../node_modules/baz/package.json
//   ../node_modules/baz/index.js
// 再上一级目录

ES6
模块之中,顶层的this指向undefined;CommonJS
模块的顶层this本着当前模块,那是二者的三个要害差距。

概述

Node 对 ES6 模块的处理相比费心,因为它有投机的 CommonJS 模块格式,与 ES6
模块格式是不匹配的。近年来的解决方案是,将二者分别,ES6 模块和 CommonJS
选用各自的加载方案。

在静态分析阶段,多个模块脚本只要有一行importexport言语,Node
就会认为该脚本为 ES6 模块,不然就为 CommonJS
模块。假使不出口任何接口,不过指望被 Node 认为是 ES6
模块,能够在剧本中加一行语句。

export {};

地方的吩咐并不是出口2个空对象,而是不出口任何接口的 ES6 标准写法。

什么不点名相对路径,Node 加载 ES6
模块会相继寻找以下脚本,与require()的条条框框平等。

import './foo';
// 依次寻找
//   ./foo.js
//   ./foo/package.json
//   ./foo/index.js

import 'baz';
// 依次寻找
//   ./node_modules/baz.js
//   ./node_modules/baz/package.json
//   ./node_modules/baz/index.js
// 寻找上一级目录
//   ../node_modules/baz.js
//   ../node_modules/baz/package.json
//   ../node_modules/baz/index.js
// 再上一级目录

ES6 模块之中,顶层的this指向undefined;CommonJS
模块的顶层this针对当前模块,那是二者的3个最首要差距。

import 命令加载 CommonJS 模块

Node
选用 CommonJS
模块格式,模块的输出都定义在module.exports那一个天性上边。在 Node
环境中,使用import一声令下加载 CommonJS 模块,Node
会自动将module.exports属性,当作模块的私下认可输出,即一律export default

下边是一个CommonJS 模块。

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// 等同于
export default {
  foo: 'hello',
  bar: 'world'
};

import命令加载下边包车型客车模块,module.exports会被视为暗中同意输出。

// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};

// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};

万一应用全体输入的写法(import * as xxx from someModule),default会取代module.exports,作为输入的接口。

import * as baz from './a';
// baz = {
//   get default() {return module.exports;},
//   get foo() {return this.default.foo}.bind(baz),
//   get bar() {return this.default.bar}.bind(baz)
// }

地方代码中,this.default取代了module.exports。须要专注的是,Node
会自动为baz添加default属性,通过baz.default拿到module.exports

// b.js
module.exports = null;

// es.js
import foo from './b';
// foo = null;

import * as bar from './b';
// bar = {default:null};

下面代码中,es.js利用第二种写法时,要由此bar.default这样的写法,才能获得module.exports

上面是另3个事例。

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function

上面代码中,bar小编是3个目的,无法同日而语函数调用,只可以通过bar.default调用。

CommonJS
模块的输出缓存机制,在 ES6 加载方式下依然有效。

// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);

地点代码中,对于加载foo.js的脚本,module.exports将间接是123,而不会变成null

出于
ES6 模块是编写翻译时规定输出接口,CommonJS
模块是运行时规定输出接口,所以使用import指令加载 CommonJS
模块时,分化意选取上边包车型客车写法。

import {readfile} from 'fs';

地点的写法不科学,因为fs
CommonJS
格式,唯有在运营时才能分明readfile接口,而import命令要求编写翻译时就明确这些接口。消除方法正是改为完整输入。

import * as express from 'express';
const app = express.default();

import express from 'express';
const app = express();

import 命令加载 CommonJS 模块

Node 采取 CommonJS
模块格式,模块的出口都定义在module.exports本条特性上面。在 Node
环境中,使用import一声令下加载 CommonJS 模块,Node
会自动将module.exports属性,当作模块的默许输出,即一律export default

上边是2个 CommonJS 模块。

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// 等同于
export default {
  foo: 'hello',
  bar: 'world'
};

import命令加载上边包车型地铁模块,module.exports会被视为暗中同意输出。

// 写法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};

// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};

假定运用完全输入的写法(import * as xxx from someModule),default会取代module.exports,作为输入的接口。

import * as baz from './a';
// baz = {
//   get default() {return module.exports;},
//   get foo() {return this.default.foo}.bind(baz),
//   get bar() {return this.default.bar}.bind(baz)
// }

上边代码中,this.default取代了module.exports。须要小心的是,Node
会自动为baz添加default属性,通过baz.default拿到module.exports

// b.js
module.exports = null;

// es.js
import foo from './b';
// foo = null;

import * as bar from './b';
// bar = {default:null};

下面代码中,es.js利用第两种写法时,要透过bar.default诸如此类的写法,才能获得module.exports

上面是另贰个事例。

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function

下面代码中,bar自个儿是二个对象,无法同日而语函数调用,只好通过bar.default调用。

CommonJS 模块的出口缓存机制,在 ES6 加载格局下依旧有效。

// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);

下边代码中,对于加载foo.js的脚本,module.exports将直接是123,而不会化为null

出于 ES6 模块是编写翻译时规定输出接口,CommonJS
模块是运作时规定输出接口,所以使用import一声令下加载 CommonJS
模块时,不允许选拔上面包车型大巴写法。

import {readfile} from 'fs';

地方的写法不科学,因为fs是 CommonJS
格式,唯有在运作时才能分明readfile接口,而import指令须要编写翻译时就明确这么些接口。消除格局正是改为完整输入。

import * as express from 'express';
const app = express.default();

import express from 'express';
const app = express();

require 命令加载 ES6 模块

采用require指令加载
ES6 模块时,ES6 模块的富有出口接口,会化为输入对象的属性。

// es.js
let foo = {bar:'my-default'};
export default foo;
foo = null;

// cjs.js
const es_namespace = require('./es');
console.log(es_namespace.default);
// {bar:'my-default'}

地方代码中,default接口变成了es_namespace.default质量。此外,由于存在缓存机制,es.jsfoo的再次赋值没有在模块外部反映出来。

上面是另贰个例子。

// es.js
export let foo = {bar:'my-default'};
export {foo as bar};
export function f() {};
export class c {};

// cjs.js
const es_namespace = require('./es');
// es_namespace = {
//   get foo() {return foo;}
//   get bar() {return foo;}
//   get f() {return f;}
//   get c() {return c;}
// }

require 命令加载 ES6 模块

采用require一声令下加载 ES6 模块时,ES6
模块的有着出口接口,会变成输入对象的质量。

// es.js
let foo = {bar:'my-default'};
export default foo;
foo = null;

// cjs.js
const es_namespace = require('./es');
console.log(es_namespace.default);
// {bar:'my-default'}

上边代码中,default接口变成了es_namespace.default特性。此外,由于存在缓存机制,es.jsfoo的再一次赋值没有在模块外部反映出来。

下边是另1个例证。

// es.js
export let foo = {bar:'my-default'};
export {foo as bar};
export function f() {};
export class c {};

// cjs.js
const es_namespace = require('./es');
// es_namespace = {
//   get foo() {return foo;}
//   get bar() {return foo;}
//   get f() {return f;}
//   get c() {return c;}
// }

循环加载

“循环加载”(circular
dependency)指的是,a本子的进行依赖b脚本,而b本子的履行又凭借a脚本。

// a.js
var b = require('b');

// b.js
var a = require('a');

家常便饭,“循环加载”表示存在强耦合,假若拍卖不好,还只怕引致递归加载,使得程序不可能推行,由此相应防止出现。

不过事实上,那是很难幸免的,越发是依靠关系复杂的大门类,很不难并发a依赖bb依赖cc又依赖a如此这般的情景。那意味着,模块加运载飞机制必须考虑“循环加载”的场馆。

对于JavaScript语言来说,近年来最广大的二种模块格式CommonJS和ES6,处理“循环加载”的点子是不平等的,再次来到的结果也不均等。

循环加载

“循环加载”(circular
dependency)指的是,a剧本的进行依赖b脚本,而b剧本的推行又凭借a脚本。

// a.js
var b = require('b');

// b.js
var a = require('a');

常备,“循环加载”表示存在强耦合,假设拍卖不佳,还大概引致递归加载,使得程序不可能实施,因而相应幸免出现。

不过实际上,那是很难幸免的,尤其是凭借关系复杂的大门类,很简单出现a依赖bb依赖cc又依赖a这么的情形。那表示,模块加运载飞机制必须考虑“循环加载”的情景。

对此JavaScript语言来说,如今最广泛的二种模块格式CommonJS和ES6,处理“循环加载”的艺术是不雷同的,再次回到的结果也不相同。

CommonJS模块的加载原理

介绍ES6哪些处理”循环加载”从前,先介绍方今最风靡的CommonJS模块格式的加载原理。

CommonJS的三个模块,正是三个本子文件。require一声令下第2遍加载该脚本,就会实施总体脚本,然后在内部存款和储蓄器生成贰个对象。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

下面代码正是Node内部加载模块后生成的二个目的。该指标的id性子是模块名,exports品质是模块输出的逐条接口,loaded品质是二个布尔值,表示该模块的本子是不是执行完结。别的还有不少性质,那里都不难了。

此后须求用到这一个模块的时候,就会到exports性子上边取值。固然再一次实施require指令,也不会重新实施该模块,而是到缓存之中取值。也正是说,CommonJS模块无论加载多少次,都只会在率先次加载时运转2回,现在再加载,就回去第③次运维的结果,除非手动清除系统缓存。

CommonJS模块的加载原理

介绍ES6什么处理”循环加载”在此以前,先介绍如今最流行的CommonJS模块格式的加载原理。

CommonJS的3个模块,就是贰个本子文件。require命令第2遍加载该脚本,就会实施总体脚本,然后在内存生成三个对象。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

上面代码正是Node内部加载模块后生成的三个目的。该指标的id属性是模块名,exports性格是模块输出的逐条接口,loaded天性是1个布尔值,表示该模块的本子是或不是执行实现。其余还有众多性质,那里都不难了。

之后要求用到那一个模块的时候,就会到exports属性下边取值。就算再度实施require一声令下,也不会另行实施该模块,而是到缓存之中取值。也正是说,CommonJS模块无论加载多少次,都只会在第二遍加载时运营三遍,今后再加载,就回去第一遍运转的结果,除非手动清除系统缓存。

CommonJS模块的循环加载

CommonJS模块的要紧特征是加载时实施,即脚本代码在require的时候,就聚会场全体推行。一旦出现有些模块被”循环加载”,就只输出已经履行的片段,还未举办的一部分不会输出。

让大家来看,Node法定文书档案个中的事例。脚本文件a.js代码如下。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

下边代码之中,a.js剧本先输出2个done变量,然后加载另3个剧本文件b.js。注意,此时a.js代码就停在此处,等待b.js执行达成,再往下实施。

再看b.js的代码。

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

上边代码之中,b.js举办到第三行,就会去加载a.js,那时,就发出了“循环加载”。系统会去a.js模块对应对象的exports个性取值,不过因为a.js还不曾执行完,从exports属性只可以取回已经实施的一部分,而不是终极的值。

a.js现已履行的部分,只有一行。

exports.done = false;

因此,对于b.js来说,它从a.js只输入叁个变量done,值为false

然后,b.js随着往下实行,等到一切进行完成,再把执行权交还给a.js。于是,a.js随后往下进行,直到执行达成。我们写一个剧本main.js,验证这么些进程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运营结果如下。

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

下面的代码注解了两件事。一是,在b.js之中,a.js尚无履行实现,只进行了第贰行。二是,main.js实施到第1行时,不会再也实施b.js,而是输出缓存的b.js的执行结果,即它的第6行。

exports.done = true;

一句话来说,CommonJS输入的是被输出值的正片,不是援引。

别的,由于CommonJS模块境遇循环加载时,重回的是当下曾经施行的有些的值,而不是代码全体实践后的值,两者或然会有反差。所以,输入变量的时候,必须丰盛小心。

var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一个部分加载时的值
};

地方代码中,如若产生循环加载,require('a').foo的值很或然前面会被改写,改用require('a')会更保障一点。

CommonJS模块的循环加载

CommonJS模块的显要特征是加载时实施,即脚本代码在require的时候,就聚会场全数实施。一旦出现有些模块被”循环加载”,就只输出已经进行的片段,还未举行的片段不会输出。

让我们来看,Node法定文档中间的事例。脚本文件a.js代码如下。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

地方代码之中,a.js剧本先输出1个done变量,然后加载另3个剧本文件b.js。注意,此时a.js代码就停在那里,等待b.js实施完成,再往下实施。

再看b.js的代码。

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

下面代码之中,b.js履行到第③行,就会去加载a.js,那时,就生出了“循环加载”。系统会去a.js模块对应对象的exports品质取值,但是因为a.js还没有实行完,从exports脾气只好取回已经推行的局地,而不是最终的值。

a.js曾经执行的一部分,唯有一行。

exports.done = false;

因此,对于b.js来说,它从a.js只输入叁个变量done,值为false

然后,b.js跟着往下实施,等到一切履行完成,再把执行权交还给a.js。于是,a.js紧接着往下实施,直到执行达成。我们写三个剧本main.js,验证这一个过程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运转结果如下。

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

地点的代码表明了两件事。一是,在b.js之中,a.js尚未执行完成,只实行了第叁行。二是,main.js举办到第2行时,不会另行实施b.js,而是输出缓存的b.js的施行结果,即它的第肆行。

exports.done = true;

总的说来,CommonJS输入的是被输出值的正片,不是引用。

其余,由于CommonJS模块蒙受循环加载时,再次来到的是时下曾经实施的一部分的值,而不是代码全部进行后的值,两者大概会不完全一样。所以,输入变量的时候,必须不大心。

var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一个部分加载时的值
};

上边代码中,即使产生循环加载,require('a').foo的值很可能前边会被改写,改用require('a')会更有限支撑一点。

ES6模块的循环加载

ES6甩卖“循环加载”与CommonJS有实质的两样。ES6模块是动态引用,就算选拔import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为贰个针对被加载模块的引用,必要开发者自个儿童卫生保健险,真正取值的时候能够取到值。

请看上面那一个例子。

// a.js如下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

上面代码中,a.js加载b.jsb.js又加载a.js,构成循环加载。执行a.js,结果如下。

$ babel-node a.js
b.js
undefined
a.js
bar

地方代码中,由于a.js的首先行是加载b.js,所以先实施的是b.js。而b.js的首先行又是加载a.js,那时由于a.js早就开端施行了,所以不会再也执行,而是继续往下实施b.js,所以率先行输出的是b.js

接着,b.js要打字与印刷变量foo,这时a.js还没执行完,取不到foo的值,导致打印出来是undefinedb.js进行完,初叶进行a.js,这时就一切平常了。

再看三个多少复杂的事例(摘自
Dr. Axel Rauschmayer 的《Exploring
ES6》)。

// a.js
import {bar} from './b.js';
export function foo() {
  console.log('foo');
  bar();
  console.log('执行完毕');
}
foo();

// b.js
import {foo} from './a.js';
export function bar() {
  console.log('bar');
  if (Math.random() > 0.5) {
    foo();
  }
}

遵守CommonJS规范,上边的代码是无奈执行的。a先加载b,然后b又加载a,这时a还未曾别的履行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

只是,ES6方可实施上边的代码。

$ babel-node a.js
foo
bar
执行完毕

// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕

地方代码中,a.js故而能够推行,原因就在于ES6加载的变量,都以动态引用其所在的模块。只要引用存在,代码就能实施。

下边,大家详细分析那段代码的运维进度。

// a.js

// 这一行建立一个引用,
// 从`b.js`引用`bar`
import {bar} from './b.js';

export function foo() {
  // 执行时第一行输出 foo
  console.log('foo');
  // 到 b.js 执行 bar
  bar();
  console.log('执行完毕');
}
foo();

// b.js

// 建立`a.js`的`foo`引用
import {foo} from './a.js';

export function bar() {
  // 执行时,第二行输出 bar
  console.log('bar');
  // 递归执行 foo,一旦随机数
  // 小于等于0.5,就停止执行
  if (Math.random() > 0.5) {
    foo();
  }
}

咱俩再来看ES6模块加载器SystemJS交付的一个例子。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
  return n != 0 && even(n - 1);
}

地点代码中,even.js里面包车型地铁函数even有贰个参数n,只要不等于0,就会减去1,传入加载的odd()odd.js也会做类似操作。

运维方面那段代码,结果如下。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

上边代码中,参数n从10变为0的进度中,even()总共会执行七次,所以变量counter特出6。第2遍调用even()时,参数n从20变为0,even()共计会执行十二回,加上前边的5次,所以变量counter等于17。

其一例子假若改写成CommonJS,就根本不可能执行,会报错。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
var even = require('./even').even;
module.exports = function(n) {
  return n != 0 && even(n - 1);
}

下边代码中,even.js加载odd.js,而odd.js又去加载even.js,形成“循环加载”。那时,执行引擎就会输出even.js现已施行的一对(不设有任何结果),所以在odd.js之中,变量even等于null,等到前边调用even(n-1)就会报错。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function

ES6模块的循环加载

ES6拍卖“循环加载”与CommonJS有真相的不等。ES6模块是动态引用,假诺利用import从三个模块加载变量(即import foo from 'foo'),那么些变量不会被缓存,而是变成一个对准被加载模块的引用,供给开发者本人保险,真正取值的时候能够取到值。

请看下边这些例子。

// a.js如下
import {bar} from './b.js';
console.log('a.js');
console.log(bar);
export let foo = 'foo';

// b.js
import {foo} from './a.js';
console.log('b.js');
console.log(foo);
export let bar = 'bar';

地方代码中,a.js加载b.jsb.js又加载a.js,构成循环加载。执行a.js,结果如下。

$ babel-node a.js
b.js
undefined
a.js
bar

地方代码中,由于a.js的率先行是加载b.js,所以先实施的是b.js。而b.js的第①行又是加载a.js,那时由于a.js一度初步实施了,所以不会重复执行,而是继续往下执行b.js,所以首先行输出的是b.js

接着,b.js要打字与印刷变量foo,这时a.js还没实施完,取不到foo的值,导致打字与印刷出来是undefinedb.js推行完,开端履行a.js,这时就一切寻常了。

再看几个不怎么复杂的例证(摘自 Dr. Axel Rauschmayer 的《Exploring
ES6》)。

// a.js
import {bar} from './b.js';
export function foo() {
  console.log('foo');
  bar();
  console.log('执行完毕');
}
foo();

// b.js
import {foo} from './a.js';
export function bar() {
  console.log('bar');
  if (Math.random() > 0.5) {
    foo();
  }
}

依照CommonJS规范,上面的代码是不得已执行的。a先加载b,然后b又加载a,这时a还尚未另外履行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

然则,ES6得以推行上边的代码。

$ babel-node a.js
foo
bar
执行完毕

// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕

地点代码中,a.js所以能够实施,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用存在,代码就能履行。

上面,大家详细分析那段代码的周转进程。

// a.js

// 这一行建立一个引用,
// 从`b.js`引用`bar`
import {bar} from './b.js';

export function foo() {
  // 执行时第一行输出 foo
  console.log('foo');
  // 到 b.js 执行 bar
  bar();
  console.log('执行完毕');
}
foo();

// b.js

// 建立`a.js`的`foo`引用
import {foo} from './a.js';

export function bar() {
  // 执行时,第二行输出 bar
  console.log('bar');
  // 递归执行 foo,一旦随机数
  // 小于等于0.5,就停止执行
  if (Math.random() > 0.5) {
    foo();
  }
}

大家再来看ES6模块加载器SystemJS付给的2个例证。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
  return n != 0 && even(n - 1);
}

地点代码中,even.js内部的函数even有3个参数n,只要不等于0,就会减去1,传入加载的odd()odd.js也会做类似操作。

运维方面那段代码,结果如下。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

下边代码中,参数n从10变为0的经过中,even()一起会进行玖回,所以变量counter等于6。第贰次调用even()时,参数n从20变为0,even()总结会实行10遍,加上前面包车型地铁4遍,所以变量counter等于17。

本条例子即便改写成CommonJS,就根本不可能执行,会报错。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
var even = require('./even').even;
module.exports = function(n) {
  return n != 0 && even(n - 1);
}

上边代码中,even.js加载odd.js,而odd.js又去加载even.js,形成“循环加载”。那时,执行引擎就会输出even.js已经履行的部分(不设有任何结果),所以在odd.js之中,变量even等于null,等到前边调用even(n-1)就会报错。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function

ES6模块的转码

浏览器近日还不接济ES6模块,为了以往就能选用,能够将转为ES5的写法。除了Babel能够用来转码之外,还有以下四个办法,也足以用来转码。

ES6模块的转码

浏览器最近还不帮助ES6模块,为了未来就能动用,能够将转为ES5的写法。除了Babel能够用来转码之外,还有以下多个法子,也得以用来转码。

ES6 module transpiler

ES6
module transpiler是
square 公司开源的3个转码器,可以将 ES6 模块转为 CommonJS 模块或 AMD模块的写法,从而在浏览器中利用。

首先,安装这一个转玛器。

$ npm install -g es6-module-transpiler

然后,使用compile-modules convert一声令下,将
ES6 模块文件转码。

$ compile-modules convert file1.js file2.js

-o参数能够钦命转码后的文件名。

$ compile-modules convert -o out.js file1.js

ES6 module transpiler

ES6 module
transpiler是 square
集团开源的1个转码器,能够将 ES6 模块转为 CommonJS 模块或 AMD模块的写法,从而在浏览器中央银行使。

先是,安装这些转玛器。

$ npm install -g es6-module-transpiler

然后,使用compile-modules convert指令,将 ES6 模块文件转码。

$ compile-modules convert file1.js file2.js

-o参数能够钦命转码后的文件名。

$ compile-modules convert -o out.js file1.js

SystemJS

另一种缓解措施是运用 SystemJS。它是二个垫片库(polyfill),能够在浏览器内加载
ES6 模块、英特尔 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是
谷歌(Google) 的 Traceur 转码器。

动用时,先在网页内载入system.js文件。

<script src="system.js"></script>

然后,使用System.import艺术加载模块文件。

<script>
  System.import('./app.js');
</script>

上面代码中的./app,指的是当前目录下的app.js文件。它能够是ES6模块文件,System.import会自行将其转码。

内需留意的是,System.import选拔异步加载,重临一个Promise 对象,能够针对这一个目的编制程序。上边是3个模块文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}

接下来,在网页内加载那么些模块文件。

<script>

System.import('app/es6-file').then(function(m) {
  console.log(new m.q().es6); // hello
});

</script>

地方代码中,System.import形式重回的是3个Promise 对象,所以能够用then办法钦命回调函数。

SystemJS

另一种缓解方法是选用 SystemJS。它是八个垫片库(polyfill),能够在浏览器内加载
ES6 模块、英特尔 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是
谷歌 的 Traceur 转码器。

使用时,先在网页内载入system.js文件。

<script src="system.js"></script>

然后,使用System.import主意加载模块文件。

<script>
  System.import('./app.js');
</script>

地方代码中的./app,指的是当前目录下的app.js文件。它能够是ES6模块文件,System.import会自行将其转码。

亟待小心的是,System.import运用异步加载,重临3个 Promise
对象,能够本着那几个目的编制程序。下边是二个模块文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}

接下来,在网页内加载这一个模块文件。

<script>

System.import('app/es6-file').then(function(m) {
  console.log(new m.q().es6); // hello
});

</script>

上边代码中,System.import主意重回的是一个 Promise
对象,所以能够用then格局内定回调函数。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有