熟悉模块
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 在运转时候处置惩罚模块要效力更高,体验更好。