深入理解Javascript之Module

什么是模块

模块(module)是什么呢?
模块是为了软件封装,复用。当今开源运动盛行,我们可以很方便地使用别人编写好的模块,而不用自己从头开始编写。在程序设计中,我们一直强调避免重复造轮子(Don't Repeat Yourself,DRY)。

想象一下,没有模块的日子,第三库基本都是导出一个全局变量供开发者使用。例如jQuery$lodash_。这些库已经尽量避免了全局变量冲突,只使用几个全局变量。但是还是不能避免有冲突,jQuery还提供了noConflict。更遑论我们自己编写的代码。

最初,Javascript 中是没有模块的概念的。这可能与一开始 Javascript 的定位有关。Javascript 最初只是希望给网页增加动态元素,定位是简单易用的脚本。
但是,随着网页端功能越来越丰富,程序越来越庞大,软件变得越来越难以维护。特别是随着 NodeJs 的兴起,Javascript 语言进入服务端编程领域。在编写大型复杂的程序,模块更是必须品。

模块只是一个抽象概念,要想在实际编程中使用还需要规范。如果没有规范,我有这种写法,你用那种写法,岂不是乱了套。

目前,模块的规范主要有3中,CommonJS模块AMD模块和ES6模块。本文着重讲解 CommonJS 模块(以 Node 实现为代表)和ES6模块。

2.CommonJS模块

CommonJS 其实是一个通用的 Javascript 语言规范,并不仅仅是模块的规范。Node 中的模块遵循 CommonJS 标准。

基本用法

Node 中提供了一个require方法用来加载模块。例如:

var fs = require('fs');

fs.readFile('file1.txt', 'utf8', function (err, data) {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
});

导入模块之后就可以使用模块中定义的接口了,如上例中的readFile

模块类别

在 Node 中大体上有3种模块,普通模块、核心模块和第三方模块。
普通模块与核心模块的导入方式稍微有些区别。普通模块是我们自己编写的模块,核心模块是 Node 提供的模块。上面我们使用的fs就是核心模块。导入普通模块时,需要在require的参数中指定相对路径。例如:

var myModule = require('./myModule');
myModule.func1();

模块myModule的后缀.js后缀可以省略。

Node 将核心模块编译进了引擎。导入核心模块只需要指定模块名,Node 引擎直接查找核心模块字典。

第三方模块的导入也是指定模块名,但是模块的查找方式有所不同。
首先,在项目目录下的node_modules目录中查找。
如果没有找到,接着去项目目录的父目录中查找。
直到找到加载该模块,或者到根目录还未找到返回失败。

定义模块

在我们日常的编程中,经常需要将一些功能封装在一个模块中,方便自己或他人使用。在 Node 中定义模块的语法很简单。模块单独在一个文件中,文件中可以使用exports导出接口或变量。例如:

function addTwoNumber(a, b) {
    return a + b;
}

exports.addTwoNumber = addTwoNumber;

假设该模块在文件myMath.js中。在同一目录下,我们可以这样来使用:

var myMath = require('./myMath');

console.log(myMath.addTwoNumber(10, 20)); // 30

模块导出详解

函数具体是怎么导出的呢?除了exports,我们经常看到的module.exports__dirname__filename是从哪里来的?
在执行require函数的时候,我们可以理解 Node 额外做了一些处理。

  • 首先,将模块所在文件内容读出来。然后将这些内容包裹在一个函数中:
function _doRequire(module, exports, __filename, __dirname) {
    // 模块文件内容
}
  • 接下来,Node 引擎构造一个空的模块对象,给这个对象一个空的exports属性,然后推算出__filename(当前导入的这个模块的全路径文件名)和__dirname(模块文件所在路径):
var module = {};
module.exports = {}
// __filename = ...
// __dirname = ...
  • 然后,调用第一步构造的那个函数,传入参数:
_doRequire(module, module.exports, __filename, __dirname);
  • 最后require返回的是module.exports的值。

按照上面的过程,我们可以很清楚地理解模块的导出过程。并且也能很快地判断一些写法是否有问题:

错误写法:

function addTwoNumber(a, b) {
    return a + b;
}

exports = {
    addTwoNumber: addTwoNumber;
}

这种写法为什么不对?exports实际上初始时是module.exports的一个引用。给exports赋一个新值后,module.exports并没有改变,还是指向空对象。最后返回的对象是module.exports,没有addTwoNumber接口。

正确写法:

function addTwoNumber(a, b) {
    return a + b;
}

// 正确写法一
exports.addTwoNumber = addTwoNumber;

// 正确写法二
module.exports.addTwoNumber = addTwoNumber;

// 正确写法三
module.exports = {
    addTwoNumber: addTwoNumber
};

exportsmodule.exports开始指向的是同一个对象。写法一通过exports设置属性,同样对module.exports也可见。写法二通过module.exports设置属性也可以导出。
写法三直接设置module.exports就更不用说了。

建议在程序开发中,坚持一种写法。个人觉得写法三显示设置相对较容易理解。

有一点需要注意:不是只有对象可以导出,函数、类等值也可以。例如下面就导出了一个函数:

function addTwoNumber(a, b) {
    return a + b;
}

module.exports = addTwoNumber;

3.ES模块

ES6 在标准层面为 Javascript 引入了一套简单的模块系统。ES6 模块完全可以取代 CommonJS 和 AMD 规范。当前热门的开源框架 React 和 Vue 都已经使用了 ES6 模块来开发。

基本使用

ES6 模块使用export导出接口,import from导入需要使用的接口:

// myMath.js
export var pi = 3.14;

export function addTwoNumber(a, b) {
    return a + b;
}

// 或
var pi = 3.14;

function addTwoNumber(a, b) {
    return a + b;
}

export { pi, addTwoNumber };
// main.js
import { addTwoNumber } from './myMath';

console.log(addTwoNumber(10, 20));

myMath.js中通过export导出一个变量pi和一个函数addTwoNumber。上例中演示了两种导出方式。一种是一个个导出,对每一个需要导出的接口都应用一次export。第二种是在文件中某处集中导出。当然,也可以混合使用这两种方式。推荐使用第二种导出方式,因为能在一处比较清楚的看出模块导出了哪些接口。

ES6 模块特性

ES6 模块有一些需要了解和注意的特性。

静态加载

ES6 模块最重要的特性是“静态加载”,导入的接口是只读的,不能修改。NodeJS 中的模块,是动态加载的。

静态加载就是“编译”时就已经确定了模块导出,可以做到高效率,并且便于做静态代码分析。同时,静态加载也限制了模块的加载在文件中所有语句之前,并且导入语法中不能含有动态的语法结构(例如变量、if语句等)。

例如:

// 可以调用,因为模块加载是“编译”时进行的。
funcA();
import { funcA, funcB } from './myModule';

// 错误,导入语法中含有变量
var foo = './myModule';
import { funcA, funcB } from './myModule';

// 错误,在if语句中
if (foo == "myModule") {
    import { funcA, funcB } from './myModule';
} else {
    import { funcA, funcB } from './hisModule';
}


// 错误,导出的接口是只读的,不能修改
import { funcA, funcB } from './myModule';
funcA = function () {};

导出的接口与模块中定义的变量或函数必须是一一对应的。而且模块内相应的值修改了,外部也能感知到。看下面代码:

// 错误,导出值1,模块中没有对应
export 1;

// 错误,实际上也是导出1,模块中没有对应
var m = 1;
export m;

// 可以这样来导出,导出的m与模块中的变量m对应
export var m = 1;

// 可以这样导出
var m = 1;
export {m};
var foo = "bar";
setTimeout(2000, () => { foo = "baz"});

// 2s后foo变为"baz",外部能感知到
别名

在导出模块时,可以为接口指定一个别名。这样,后续可以修改内部接口而保持导出接口不变。例如:

// myModule.js
var funcA = function () {
}

var funcB = function () {
}

export {
    funcA as func1,
    funcB as func2,
    funcB as myFunc,
}

上面我们导出以别名func1导出函数funcA,以别名func2myFunc导出函数funcBfunc2myFunc都是指向同一个函数funcB的。下面看看使用这个模块:

// main.js
import { func1, func2, myFunc } from './myModule';

同样的,导入模块时也可以指定别名:

// main.js
import { func1 as func } from './myModule';
default导出

上面介绍的模块导入必须知道接口名字。有时候,用户学习一个模块时希望能够快速上手,不想去看文档(怎么会有这个懒的人🤣)。ES6 提供了default导出。例如:

// myModule.js
export default function () {
    console.log('hi');
}

// default导出方式可以看做是导出了一个别名为default的接口
var f = function () {
    console.log('hi');
}
export { f as default };

在外部导入的时候,需要省略花括号:

// main.js
import func from './myModule';
func();

也可以两种方式,同时使用:

// myModule.js
function foo() {
    console.log('foo');
}

export default foo;

function bar() {
    console.log('bar');
}

export { bar };
// main.js
import foo, { bar } from './myModule';
整体加载

ES6 还允许一种整体加载的方式导入模块。通过使用import *可以导入模块中导出的所有接口:

// myModule.js
export function funcA() {
    console.log('funcA');
}

export function funcB() {
    console.log('funcB');
}
// main.js
import * as m from './myModule';

m.funcA();
m.funcB();

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

// main.js
import * as m from './myModule';

// 错误
m.name = 'darjun';
m.func = function () {};
Node 中使用 ES6 模块

Node 由于已经有 CommonJS 的模块规范了,与 ES6 模块不兼容。为了使用 ES6 模块,Node 要求 ES6 模块采用.mjs后缀名,而且文件中只能使用importexport,不能使用require。而且该功能还在试验阶段,Node v8.5.0以上版本,指定--experimental-modules参数才能使用:

// myModule.mjs
var counter = 1;

export function incCounter() {
    console.log('counter:', counter);
    counter++;
}
// main.mjs
import { incCounter } from './myModule';

incCounter();

使用下面命令行运行程序:

$ node --experimental-modules main.mjs

4.总结

随着 Javascript 在大型项目中占用举足轻重的位置,模块的使用称为必然。Node 中使用 CommonJS 规范。ES6 中定义了简单易用高效的模块规范。ES6 规范化是个必然的趋势,所以在掌握当前 CommonJS 规范的前提下,学习 ES6 模块势在必行。

5.参考链接

  1. Javascript模块化编程(一)
  2. Javascript模块化编程(二)
  3. Javascript模块化编程(三)
  4. ES6 Module

关于我:
个人主页 简书 掘金

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容

  • 系列文章导航 模块(一) CommonJs,AMD, CMD, UMD 本文参考阮一峰 ES6入门 Module的...
    合肥黑阅读 6,104评论 0 4
  • 模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。所谓模块化主要是解决代码分...
    MapleLeafFall阅读 1,167评论 0 0
  • 上一章介绍了模块的语法,本章介绍如何在浏览器和 Node 之中加载 ES6 模块,以及实际开发中经常遇到的一些问题...
    emmet7life阅读 2,735评论 0 1
  • 當微風吹起你細長的睫毛, 兩顆星星閃爍灼人的光芒。 在我甜蜜而憂傷的夢裡, 一個秘密幽靈般徘徊複隱藏。 此刻風兒吹...
    東方朝西阅读 142评论 2 2
  • 大爱的老师,美丽智慧的班主任,亲爱的众学兄们大家好,我是来山东华夏炜烨的魏连鹏,今天2018年09月13日是我日精...
    华夏炜烨魏连鹏阅读 157评论 0 0