ECMAScript6(19):Module 模块

熟悉模块

JS 作为一位编程言语,一直以来没有模块的观点。严峻致使大型项目开辟受阻,js 文件越写越大,不方便保护。其他言语都有模块的接口,比方 Ruby 的 require,python 的 import,C++ 天生的 #include,以至 CSS 都有 @import。在 ES6 之前,有重要的2个模块化计划:CommonJS 和 AMD。前者用于服务器,后者用于浏览器。CommonJS 如许引入模块:

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

AMD 和 CommonJS 引入模块要领差不多,其代表是 require.js。这里我们重要研讨 ES6 供应的要领:

import {stat, exists, readFile} from 'fs'

这个要领比拟之前的计划,具有以下长处:

  • 最大的长处就是编译的时刻完成模块加载,称之为”编译时加载”, 而 CommonJS 运用的是 “运转时加载”。显著 ES6 效力更高
  • 不再须要 UMD 模块花样,未来服务器和浏览器肯定都能支撑这类要领
  • 未来浏览器 API 能够用模块的花样供应,不须要做成全局变量或 navigator 的属性
  • 不须要反复的封装和定义定名空间,直接以模块情势供应即可
  • 模块默许事情在严厉形式,纵然没有指定”use strict”, 关于严厉形式能够看:Javascript严厉形式特性
  • 一个模块就是一个文件,有效地减少了全局变量污染

export 和 import

模块功用重要由2个敕令组成:export 和 import。export 关键字用于划定模块的对外接口,import 关键字用于输入其他模块供应的功用。这里须要晓得的是,ES6 中模块导出的都邑组成一个对象。

  • export 导出模块的部份要领属性或类
export var a = 1;
export var b = 2;
export var c = 3;

上面导出了3个变量,和下面的下法等价:

var a = 1;
var b = 2;
var c = 3;
export {a, b, c};    //这类写法更好,在文件末端一致导出,清楚清楚明了

固然还能够导出函数和类

//导出一个函数 add
export function add(x,y){
  return x + y;
}
//导出一个类
export default class Person{}

还能够在导出时刻对参数重定名:

function foo(){}
function bar(){}

export {foo, bar as bar2, bar as bar3}     //bar 被重定名为 bar2,bar3输出了2次
  • import 导入敕令能够导入其他模块经由历程 export 导出的部份
// abc.js
var a = 1;
var b = 2;
var c = 3;
export {a, b, c}

//main.js
import {a, b, c} from './abc';      //接收的变量用大括号示意,以解构赋值的情势猎取
console.log(a, b, c);

导入的时刻也能够为变量从新取一个名字

import {a as aa, b, c};
console.log(aa, b, c)

假如想在一个模块中先输入后输出同一个模块,import语句能够和export语句写在一同。

// 一般写法
import {a, b, c} form './abc';
export {a, b, c}

// 运用简写, 可读性不好,不发起
export {a, b, c} from './abc';

//ES7 发起,在简化先输入后输出的写法。如今不能运用,也不发起运用,可读性不好
export a, b, c from './abc'

运用 import 和 export 须要注重一下几个方面:

  • export 必需写在地点模块作用于的顶层。假如写在了内部作用于会报错
  • export 输出的值是动态绑定的,绑定在其地点的模块。
// foo.js
export var foo = 'foo';

setTimeout(function() {
  foo = 'foo2';
}, 500);

// main.js
import * as m from './foo';
console.log(m.foo); // foo
setTimeout(() => console.log(m.foo), 500); //foo2            500ms 后一样会被修正
  • import 具有声明提拔,而且会提拔到悉数文件最上面
  • import 取得的变量都是只读的,修正它们会报错
  • 在 export 输出内容时,假如同时输出多个变量,须要运用大括号{},同时 import 导入多个变量也须要大括号
  • import 引入模块的默许后缀是 .js, 所以写的时刻能够疏忽 js 文件扩展名
  • import 会实行要所加载的模块。以下写法仅仅实行一个模块,不引入任何值
import './foo';    //实行 foo.js 但不引入任何值

模块团体加载

固然模块能够作为团体加载,运用*关键字,并运用 as 重定名获得一个对象,一切取得的 export 的函数、值和类都是该对象的要领:

// abc.js
export var a = 1;
export var b = 2;
export var c = 3;

// main.js
import * as abc from './abc';
console.log(abc.a, abc.b, abc.c);

上面 main.js 中的团体加载能够用 module 关键字完成:

//临时没法完成
module abc from './abc';
console.log(abc.a, abc.b, abc.c);   //1 2 3

注重,以上2种体式格局取得的接口,不包括 export default 定义的默许接口。

export default

为了使模块的用户能够不看文档,或许少看文档,输出模块的时刻运用 export default 指定默许输出的接口。运用 export defalut 输出时,不须要大括号,而 import 输入变量时,也不须要大括号(没有大括号即示意取得默许输出)

// abc.js
var a = 1, b = 2, c = 3;
export {a, b};
export default c;     //等价于 export default 3;

// main.js
import {a, b} from './abc';
import num from './abc';        // 不须要大括号, 而且能够直接更名(假如必需用原名不还得看手册么?)
console.log(a, b, num)            // 1 2 3

实质上,export default输出的是一个叫做default的变量或要领,输入这个default变量时不须要大括号。

// abc.js
var a = 20;
export {a as default};

// main.js
import a from './abc'; // 如许也是能够的
console.log(a);        // 20

// 如许也是能够的
import {default as aa} from './abc';
console.log(aa);       // 20

假如须要同时输入默许要领和其他变量能够如许写 import:

import customNameAsDefaultExport, {otherMethod}, from './export-default';

这里须要注重:一个模块只能有一个默许输出,所以 export default 只能用一次

模块的继承

所谓模块的继承,就是一个模块 B 输出了模块 A 悉数的接口,就似乎是 B 继承了 A。运用 export * 完成:

// circleplus.js
export * from 'circle';            //固然,这里也能够挑选只继承其部份接口,以至能够对接口更名
export var e = 2.71828182846;
export default function(x){        //从新定义了默许输出,假如不想从新定义能够:export customNameAsDefaultExport from 'circle';
  return Math.exp(x);
}

//main.js
import * from 'circleplus';        //加载悉数接口
import exp from 'circleplus';      //加载默许接口
//...use module here

上面这个例子 circleplus 继承了 circle。值得一提的是,export * 不会再次输出 circle 中的默许输出(export default)。

在运用和定义模块时,愿望能够做到以下几个发起:

  • Module 语法是 JavaScript 模块的规范写法,对峙运用这类写法。运用 import 庖代 require, 运用 export 庖代module.exports
  • 假如模块只需一个输出值,就运用 export default,假如模块有多个输出值,就不运用 export default
  • 只管不要 export default 与一般的 export 同时运用
  • 不要在模块输入中运用通配符。因为如许能够确保你的模块当中,有一个默许输出(export default)
  • 假如模块默许输出一个函数,函数名的首字母应当小写;假如模块默许输出一个对象,对象名的首字母应当大写

ES6 模块加载的实质

ES6 模块加载的机制是值的运用,而 CommonJS 是值的拷贝。这意味着, ES6 模块内的值的变更会影响模块外对应的值,而 CommonJS 不会。 ES6 碰到 import 时不会马上实行这个模块,只天生一个动态援用,须要用的时刻再去内里找值。有点像 Unix 中的标记链接。所以说 ES6的模块是动态援用,不会缓存值。之前的这个例子就能够申明题目:

// foo.js
export let counter = 3;
export function inc(){
  counter++;
}

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

我们看一个 CommonJS 的状况

// foo.js
let counter = 3;
function inc(){
  counter++;
}
module.exports = {
  counter: counter,
  inc: inc
}

// main.js
let foo = require('./foo')
let counter = foo.counter;
let inc = foo.inc;

console.log(counter);    //3
inc();
console.log(counter);    //3

轮回加载

不晓得你们只不晓得轮回援用,在 内存治理与渣滓接纳中提到过:假如 A 对象的一个属性值是 B 对象,而 B 对象的一个属性值是 A 对象,就会构成轮回援用,没法开释他们的内存。而模块中也会涌现轮回加载的状况:假如 A 模块的实行依靠 B 模块,而 B 模块的实行依靠 A 模块,就构成了一个轮回加载,效果递次不能事情,或许死机。然则,如许的关联很难防止,因为开辟者浩瀚,谁都邑在开辟本身的模块时运用他人的几个模块,一朝一夕,就行互联网一样,如许的依靠也织成了一个网。

ES6 和 CommonJS 处置惩罚轮回加载又不一样,从 CommonJS 最先研讨

  • CommonJS

CommonJS 每次实行完一个模块对应的 js 文件后在内存中就天生一个对象:

{
  id: '...',           //示意属性的模块名
  exports: {...};      //模块输出的各个接口
  loaded: true,        //示意是不是加载终了
  //...内容许多,不一一列举了
}

以后运用这个模块,纵然在写一遍 requrie,都不会再实行对应 js 文件了,会直接在这个对象中取值。
CommonJS 假如碰到轮回加载,就输出已实行的部份,以后的不再实行,实行递次以解释序号为准(从0最先):

// a.js
exports.done = false;         //1. 先输出 done
var b = require('./b.js');    //2. 进入 b.js 实行 b.js    //5. 发明 a.js 没实行完,那就反复不实行 a.js,返回已实行的 exports
console.log(`In a.js, b.done = ${b.done}`);     //10. 第2步的 b.js 实行完了,继承实行 a.js 获得控制台输出:'In a.js, b.done = true'
exports.done = true;          //11
console.log('a.js executed');  //12. 获得控制台输出:"a.js executed"

// b.js
exports.done = false;         //3. 先输出 done
var a = require('./a.js');    //4. 实行到这里发作轮回加载,去 a.js 实行 a.js     //6. 只获得了 a.js 中的 done 为 false
console.log(`In b.js, a.done = ${a.done}`);       //7. 获得控制台输出:"In b.js, a.done = false"
exports.done = true;     //8. 输出 done, 覆蓋了第3步的输出
console.log('b.js executed');     //9. 获得控制台输出:"b.js executed"

//main.js
var a = require("./a.js");    //0. 去 a.js 实行 a.js
var b = require("./b.js");    //13. b.js 已实行过了,直接去内存中的对象取值
console.log(`In main,a.done = ${a.done}, b.done = ${b.done}`)    //获得控制台输出:'In main,a.done = true, b.done = true'
  • ES6

因为 ES6 运用的是动态援用,碰到 import 时不会实行模块。所以和 CommonJS 有实质的区分。一样我们看个例子:

// a.js
import {bar} from './b.js';
export function foo(){
  bar();
  console.log("finished")
}

// b.js
import {foo} from './a.js';
export function bar(){
  foo();
}

//main.js
import * from './a.js';
import * from './b.js';
//...

上面这段代码写成 CommonJS 情势是没法实行的,应为 a 输出到 b 的接口为空(null), 所以在 b 中挪用 foo() 要报错的。然则 ES6 能够实行,获得控制台输出”finished”

另一个例子是如许的。实行递次以解释序号为准(从0最先):

// even.js
import {odd} from './odd';         //2. 获得 odd.js 动态援用,但不实行
export var counter = 0;            //3. 输出 counter 的援用
export function even(n){           //4. 输出 even 函数的援用
  counter++;                       //6
  return n === 0 || odd(n - 1);    //7. n 不是 0, 去 odd.js 找 odd() 函数    //10. 实行 odd 函数,传入9
}

// odd.js
import {even} from './even';       //8. 获得 even.js 动态援用,但不实行
export function odd(n){            //9. 输出 odd 函数
  return n !== 0 && even(n - 1);   //11. 回到第2步,找到 even 函数,返来实行,传入8,直到 n 为 0 完毕
}

// main.js
import * as m from './even';    //0. 获得 even.js 动态援用,但不实行
console.log(m.even(10));     //1. 去 even.js 找 even 函数。 //5. 实行函数,传入10   //终究获得控制台输出:true
console.log(m.counter);      //因为 ES6 模块传值是动态绑定的(下同),所以获得控制台输出:6
console.log(m.even(20));     //剖析同上,获得控制台输出:true
console.log(m.counter);      //获得控制台输出:17

上面写了11步,以后是一个轮回,没有继承写。但不难看出 ES6 基础不怕轮回援用,只需模块文件的动态援用在,就能够盘算完成。不过,别看这个历程比 CommonJS 庞杂,每次都有从新运转模块文件,而不直接读取缓存,但 ES6 的这些事情在编译时期就完成了,比 CommonJS 在运转时候处置惩罚模块要效力更高,体验更好。

    原文作者:Faremax
    原文地址: https://segmentfault.com/a/1190000016286366
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞