/Module-demo

:memo: Module-demo analysis of commonJs & Es6Module

Primary LanguageJavaScript

Module 相关的语法

enter image description here

前言: 之前也没有去仔细研究过这个,在项目中也只是照葫芦画瓢。今天攻破下这块,不想留下模棱两可的东西。

参考文章:

分类

ES6 之前,社区制定了一些模块加载方案,最主要的有CommonJSAMD 两种。CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。

由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用于服务器,AMD用于浏览器。 ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代CommonJSAMD 规范,成为浏览器和服务器通用的模块解决方案。 但是,ES6模块的设计**是尽量的静态化,编译时加载。CommonJSAMD则是运行时加载。(下面会有具体例子)

了解了基本概念之后,我们将常见的语法分类:

  • require: nodees6都支持的引入
  • export / import : 只有es6支持的导出引入
  • module.exports / exports: 只有node支持的导出

exports 命令

Node里面的模块系统遵循的是CommonJS规范。

CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)。

在一个node执行一个文件时,会给这个文件内生成一个exportsmodule对象, 而module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域 内存结构示意图

demo

// a.js
let a = 100;
console.log(`新建时候的module.exports${JSON.stringify(module.exports)}`); 
console.log(`新建时候的exports${JSON.stringify(exports)}`); 
exports.a = 200; //这里module.exports 的内容给改成 {a : 200}
exports = '指向其他内存区'; //这里把exports的指向指走
console.log(`完成修改时候的module.exports${JSON.stringify(module.exports)}`); 
console.log(`完成修改时候的exports${JSON.stringify(exports)}`); 

// b.js
var a = require('./a');
console.log(`b文件引用到a的时候打印为${JSON.stringify(a)}`)

运行结果为:

exports只辅助module.exports操作内存中的数据,结果到最后真正被require出去的内容还是module.exports的. 为了避免糊涂,尽量都用 module.exports 导出,然后用require导入。

export 命令

先贴出阮老师文档上的几种方式

// 1:输出变量一
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

// 2: 输出变量二
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

// 3: 输出函数或类(class)。
export function multiply(x, y) {
  return x * y;
};

// 4: as 用来重命名
function v1() { }
function v2() { }
export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

2对比1来说,有点在于写在脚本尾部,一眼看清楚输出了哪些变量。

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

export 1 // 报错

// 报错
var m = 1
export m
// 1只是一个值,不是接口。
export var m = 1 // 正确方法1

var m = 1
export {m} // 正确方法2

// function和class也是一样,
function f() {}
export f; //报错

export function f() {};// 正确

function f() {}
export {f}; // 正确

export语句输出值是可以动态的

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// b.js
import {foo} from '../export/a'
console.log(foo)
setTimeout(()=>{
  console.log(foo)
},1000)

在引用500ms后值会改变, 测试结果如下。demo

最后由于Es6Module设计**是尽量的静态化,编译时加载。并非执行时加载,所以,export不能用在块级作用域内(函数和条件语句),会报错。

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

if (a) {
  export ... // error
}

import 命令

常规用法

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

export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。

本质上,export default就是输出一个接口叫做default的变量或方法,然后系统允许你为它取任意名字。所以,在导入导出的时候 允许改名。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同于export default add;

// app.js
import { default as foo } from 'modules';
// 等同于import foo from 'modules';

正是因为export default命令其实只是输出一个叫做default的变量,所以它和export不一样后面不能跟变量声明语句。

// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;

反之,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

// 正确
export default 42;
// 报错
export 42;

模块的整体加载

常规用法如下

// a.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
export default 42;

//b.js
import * as _ from 'a'
console.log(_.default) //42;
console.log(_.firstName) //Michael;

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};

模块的继承

铺垫

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

上述运行时状态是不可以改变的,但是继承的模块运行时是可以改变,具体如下:

// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}// 不需要关心circle有什么方法。
// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

上述方法 其实做到了在运行时(间接静态)改变某个引用的方法。

拓展

webpackbable对模块的兼容性处理(CommonJs/Es6Modules)的原理。

End....