ES6脚丫系列模块Module

42次阅读

共计 7873 个字符,预计需要花费 20 分钟才能阅读完成。

本文字符数 8200+,阅读时间约 16 分钟。

『ES6 知识点总结』模块 Module

第一节:Module 基本概念

【01】过去使用 CommonJS 和 AMD,前者用于服务器,后者用于浏览器。

Module 可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

【02】运行时加载和编译时加载

ES6 模块的设计思想,是尽量的静态化,在编译时就能确定模块的依赖关系,以及输入和输出的变量。

CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

这种加载称为“运行时加载”。

整体加载 fs 模块(即加载 fs 的所有方法),然后使用时用到 3 个方法。

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

ES6 模块不是对象,而是通过 export 和 import 命令显式指定输出和输入的代码。

这种加载称为“编译时加载”,即 ES6 可以在编译时就完成模块编译,效率要比 CommonJS 模块的加载方式高。

实质是从 fs 模块加载 3 个方法,其他方法不加载。

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

【03】(+ 仅了解)好处:

01、不再需要 UMD 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。

02、将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者 navigator 对象的属性。

不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供。

【04】ES6 的模块自动采用严格模式,不管有没有在模块头部加上 ”use strict”。

第二节:export 命令

【01】模块功能由两个命令构成:export 和 import。

export 命令用于规定本模块的对外接口。

import 命令用于引入其他模块的功能。

【02】一个模块就是一个的文件。

该文件内部的所有变量,外部无法获取。

如果希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。

【03】export 可以输出变量、函数、类。

可以使用多个 export。

可以 export+ 变量声明赋值一起输出。

可以输出用逗号分隔的变量集合,用花括号括起来。

可以 export+ 函数声明一起输出。

写法 1:

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

写法 2:(推荐使用)

// profile.js
var firstName = 'Michael';var lastName = 'Jackson';var year = 1958;

export {firstName, lastName, year};

写法 3:

export function multiply (x, y) {return x * y;};

【03】可以使用 as 关键字重命名输出的变量。甚至可以给一个变量取多个名字。

v1 as newName,v1 as newName2

function v1() { ...}function v2() { ...}

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

【04】export 命令可以出现在模块的任何位置,只要处于模块最外层(非某个块级作用域中)就可以。

如果处于块级作用域内,就会报错。

import 命令也是如此。

function foo () {export default 'bar' // SyntaxError}
foo()

【05】export 语句输出的值是动态绑定,绑定其所在的模块。

代码输出变量 foo,值为 bar,500 毫秒之后变成 baz。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

【06】模块之间也可以继承。

export * from “fileName”

假设有一个 circleplus 块,继承了 circle 模块。

export * 命令会忽略 circle 模块的 default 方法。

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {return Math.exp(x);}

第三节:import 命令

【01】通过 import 命令加载模块(文件)。

【02】import 命令接受用逗号分隔的要从其他模块导入的变量列表,用花括号括起来。

变量名必须与被导入模块的输出的名称相同。

import {item1,item2} from “fileUrl”;

// main.js

import {firstName, lastName, year} from './profile';function setName(element) {element.textContent = firstName + ' ' + lastName;}

【03】使用 as 关键字,将引用变量重命名。

import {lastName as surname} from './profile';

【04】import 命令具有提升效果,会提升到整个模块的头部,首先执行。

foo();

import {foo} from 'my_module';// 不会报错。

【05】如果在一个模块之中,先输入后输出同一个模块,import 语句可以与 export 语句写在一起。

但是从可读性考虑,不建议采用这种写法,而应该采用标准写法。

export {es6 as default} from './someModule';
// 等同于
import {es6} from './someModule';
export default es6;

【06】另外,ES7 有一个提案,简化先输入后输出的写法,拿掉输出时的大括号。

// 提案的写法
export v from "mod";
// 现行的写法
export {v} from "mod";

【07】import 语句会执行加载的模块。

仅仅执行 lodash 模块,但是不输入任何值。

import+ 空格 + 模块名字符串。

import 'lodash'

【08】整体加载

用星号(*)指代为一个对象,所有的引用值都加载在这个对象上面。

import+*+as+ 新变量名 +from+ 模块地址字符串。

// circle.js

export function area(radius) {return Math.PI * radius * radius;}
export function circumference(radius) {return 2 * Math.PI * radius;}

单一加载:

// main.js

import {area, circumference} from './circle';

console.log("圆面积:" + area(4));
console.log("圆周长:" + circumference(14));

整体加载:

import * as circle from './circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

【09】module 命令

module 命令可以取代 import 命令,达到整体引用模块的作用。

module 命令后面跟一个变量,表示输入的模块定义在该变量上。

module + 变量名 +from+ 模块地址字符串

// main.js

module circle from './circle';

console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));

第四节:export default 命令

【01】使用 import 命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。

但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

(zyx456:好像并不需要,直接整体加载就是了。)

【02】使用 export default 命令,为模块指定默认输出。

一个模块只能有一个默认输出,因此 export deault 命令只能使用一次。

所以,import 命令后面才不用加大括号,因为只可能对应一个方法。

写法 1:

export default 匿名函数。

其他模块加载该模块时,import 命令可以为该匿名函数指定任意名字。

这时就不需要知道原模块输出的函数名。

这时 import 命令后面,不使用大括号。

// export-default.js
export default function () {  console.log('foo');}
// import-default.js
import customName from './export-default';customName(); // 'foo' 

写法 2:

export default 函数声明

export default 命令用在非匿名函数前,也是可以的。函数的函数名,在模块外部是无效的。加载的时候,视同匿名函数加载。

// export-default.js
export default function foo() {  console.log('foo');}
// 或者写成
function foo() {  console.log('foo');}

export default foo;

写法 3:

export default value

如果要输出默认的值,只需将值跟在 export default 之后即可。

export default 42; 写法 4:

export default 也可以用来输出类。

// MyClass.js
export default class {...}
// main.js
import MyClass from 'MyClass'let o = new MyClass();

【04】下面比较一下默认输出和正常输出。

// 输出
export default function crc32() { // ...}
// 输入
import crc32 from 'crc32';
// 输出
export function crc32() { // ...};
// 输入
import {crc32} from 'crc32';

【05】本质上,export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。

所以,下面的写法是有效的。

// modules.js
function add(x, y) {return x * y;};
export {add as default};
// app.js
import {default as xxx} from 'modules';

【06】如果想在一条 import 语句中,同时输入默认方法和其他变量。

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

【】例子:

import $ from 'jquery';

第五节:ES6 模块加载的实质

【01】Moduel 模块加载的机制,与 CommonJS 模块完全不同。

CommonJS 模块输出的是一个值的拷贝,而 Module 模块输出的是值的引用。

CommonJS 模块输入的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

例子。

下面是一个模块文件 lib.js。

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

加载上面的模块。

counter 输出以后,lib.js 模块内部的变化就影响不到 counter 了。

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

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

ES6 模块的运行机制与 CommonJS 不一样,它遇到模块加载命令 import 时,不会去执行模块,而是只生成一个动态的只读引用。

等到真的需要用到时,再到模块里面去取值,换句话说,ES6 的输入有点像 Unix 系统的”符号连接“,原始值变了,输入值也会跟着变。

因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

还是举上面的例子。

// lib.js
export let counter = 3;
export function incCounter() {  counter++;}
// main1.js
import {counter, incCounter} from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

【】由于 ES6 输入的模块变量,只是一个”符号连接“,所以这个变量是只读的,对它重新赋值会报错。

因为变量 obj 指向的地址是只读的,不能重新赋值,这就好比 main.js 创造了一个名为 obj 的 const 变量。

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

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

第六节:循环加载

【01】“循环加载”(circular dependency)指的是,a 脚本的执行依赖 b 脚本,而 b 脚本的执行又依赖 a 脚本。

通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现 a 依赖 b,b 依赖 c,c 又依赖 a 这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。

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

【02】对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。

【03】CommonJS 模块的加载原理

CommonJS 的一个模块,就是一个脚本文件。

require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

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

上面代码中,该对象的 id 属性是模块名,exports 属性是模块输出的各个接口,loaded 属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。

以后需要用到这个模块的时候,就会到 exports 属性上面取值。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存之中取值。

CommonJS 模块的循环加载

CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。

CommonJS 的做法是,一旦出现某个模块被 ” 循环加载 ”,就只输出已经执行的部分,还未执行的部分不会输出。

让我们来看,Node 官方文档里面的例子。

脚本文件 a.js 代码如下。

a.js 脚本先输出一个 done 变量,然后加载另一个脚本文件 b.js。注意,此时 a.js 代码就停在这里,等待 b.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 执行完毕');

再看 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 执行到第二行时,不会再次执行 b.js,而是输出缓存的 b.js 的执行结果,即它的第四行。

exports.done = true;

总之,CommonJS 输入的是被输出值的拷贝,不是引用。

ES6 模块的循环加载

ES6 处理“循环加载”与 CommonJS 有本质的不同。

ES6 模块是动态引用,遇到模块加载命令 import 时,不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

ES6 模块中的值属于【动态只读引用】。

对于只读来说,即不允许修改引入变量的值,import 的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

对于动态来说,原始值发生变化,import 加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。

循环加载时,ES6 模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。

zyx456:也就是说 2 个文件本身都是可以加载的,然后在运行时去找需要用的值,这时也是可以找到的。

不需要加载依赖关系。

例子

// a.js
import {bar} from './b.js';

export function foo() {bar();
    console.log('执行完毕');
}

foo();

// b.js
import {foo} from './a.js';

export function bar() {if (Math.random() > 0.5) {foo();
    }
}

按照 CommonJS 规范,上面的代码是没法执行的。a 先加载 b,然后 b 又加载 a,这时 a 还没有任何执行结果,所以输出结果为 null,即对于 b.js 来说,变量 foo 的值等于 null,后面的 foo() 就会报错。

但是,ES6 可以执行上面的代码。

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

正文完
 0