JavaScript 中的 require / exports 和 import / export

Node.js 的基础教学 之 exports module.exportshttps://zhuanlan.zhihu.com/p/82057593

1、为什么有模块概念

理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。

但是,Javascript 不是一种模块化编程语言,在es6以前,它是不支持 ""(class),所以也就没有 "模块"(module)了。

2、require时代

Javascript 社区做了很多努力,在现有的运行环境中,实现 "模块" 的效果。

原始写法

模块就是实现特定功能的一组方法。只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。

function m1(){
  //...
}
function m2(){
  //...  
}

上面的函数 m1() 和 m2(),组成一个模块。使用的时候,直接调用就行了。

这种做法的缺点很明显:"污染" 了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。

对象写法

为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面

var module1 = new Object({
    _count: 0,
    m1: function () {
        //...
    },
    m2: function () {
        //...
    }
});

上面的函数 m1()m2(),都封装在 module1 对象里。使用的时候,就是调用这个对象的属性:module1.m1();

这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值:module1._ count = 1;

立即执行函数写法

使用 "立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的

var module = (function () {
    var _count = 0;
    var m1 = function () {
        alert(_count)
    }
    var m2 = function () {
        alert(_count + 1)
    }

    return {
        m1: m1,
        m2: m2
    }
})()

使用上面的写法,外部代码无法读取内部的 _count 变量:console.info(module._count);  // undefined

module 就是 Javascript 模块的基本写法。

nodejs 模块机制

Nodejs 的模块机制 及 require 用法https://www.cnblogs.com/typttxs/p/12196649.html

Nodejs 中提供了 exports require 两个对象,

  • exports 模块公开的接口
  • require 用于 从外部获取一个模块的接口,即 获取模块的 exports 对象

有关 exports module.exports 的区别

  • 正常对外暴露 属性或者方法,使用 exports
  • 如需要暴露 对象(类似class,包含了很多属性和方法),就使用 module.exports

具体可以参考:https://www.runoob.com/nodejs/nodejs-module-system.html

hello.js

exports.world = function() {
    console.log('Hello World');
}

main.js,代码 require 引入了当前目录下的 hello.js 文件,使用了模块 exports 出来的对象或者方法

var hello = require('./hello');
hello.world();

hello.js 中通过 exports 对象将 world 作为模块的访问接口,在 main.js 内通过 require 加载这个模块,然后就可以直接访问 hello.js 里的 exports 对象的成员函数了。

修改 hello.js 和 main.js

//hello.js 
function Hello() { 
    var name; 
    this.setName = function(thyName) { 
        name = thyName; 
    }; 
    this.sayHello = function() { 
        console.log('Hello ' + name); 
    }; 
}; 
module.exports = Hello;

直接获取对象

//main.js 
var Hello = require('./hello'); 
hello = new Hello(); 
hello.setName('BYVoid'); 
hello.sayHello(); 

模块接口的唯一变化是使用 module.exports = Hello 代替了exports.world = function(){}。

在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。

exports 可以理解为 module.exports 的一个 快捷方式。。。

require 查找机制

Node.js 的 require 方法中的文件查找策略如下:

从文件模块缓存中加载

尽管原生模块与文件模块的优先级不同,但是都会优先从文件模块的缓存中加载已经存在的模块。

从原生模块加载

原生模块的优先级仅次于文件模块缓存的优先级。require 方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个 http/http.js/http.node/http.json 文件,require("http") 都不会从这些文件中加载,而是从原生模块中加载。

原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。

从文件加载

当文件模块缓存中不存在,而且不是原生模块的时候,Node.js 会解析 require 方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前一节中已经介绍过,这里我们将详细描述查找文件模块的过程,其中,也有一些细节值得知晓。

require方法接受以下几种参数的传递:

  • http、fs、path等,原生模块。
  • ./mod或../mod,相对路径的文件模块。
  • /pathtomodule/mod,绝对路径的文件模块。
  • mod,非原生模块的文件模块。

下面是一个完整的,在路径 Y 下执行 require(X) 语句执行顺序:

1. 如果 X 是内置模块
   a. 返回内置模块
   b. 停止执行
2. 如果 X 以 '/' 开头
   a. 设置 Y 为文件根路径
3. 如果 X 以 './' 或 '/' or '../' 开头
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. 抛出异常 "not found"

LOAD_AS_FILE(X)
1. 如果 X 是一个文件, 将 X 作为 JavaScript 文本载入并停止执行。
2. 如果 X.js 是一个文件, 将 X.js 作为 JavaScript 文本载入并停止执行。
3. 如果 X.json 是一个文件, 解析 X.json 为 JavaScript 对象并停止执行。
4. 如果 X.node 是一个文件, 将 X.node 作为二进制插件载入并停止执行。

LOAD_INDEX(X)
1. 如果 X/index.js 是一个文件,  将 X/index.js 作为 JavaScript 文本载入并停止执行。
2. 如果 X/index.json 是一个文件, 解析 X/index.json 为 JavaScript 对象并停止执行。
3. 如果 X/index.node 是一个文件,  将 X/index.node 作为二进制插件载入并停止执行。

LOAD_AS_DIRECTORY(X)
1. 如果 X/package.json 是一个文件,
   a. 解析 X/package.json, 并查找 "main" 字段。
   b. let M = X + (json main 字段)
   c. LOAD_AS_FILE(M)
   d. LOAD_INDEX(M)
2. LOAD_INDEX(X)

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. DIRS = DIRS + DIR
   d. let I = I - 1
5. return DIRS

3、主流模块规范

在es6以前,还没有提出一套官方的规范,从社区和框架推广程度而言,目前通行的 javascript 模块规范有两种:CommonJS 和 AMD

CommonJS 规范

2009年,美国程序员Ryan Dahl创造了node.js 项目,将 javascript 语言用于服务器端编程。

这标志 "Javascript模块化编程" 正式诞生。前端的复杂程度有限,没有模块也是可以的,但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

node 编程中最重要的思想之一就是模块,而正是这个思想,让JavaScript的大规模工程成为可能。模块化编程在js界流行,也是基于此,随后在浏览器端,requirejs 和 seajs 之类的工具包也出现了,可以说在对应规范下,require 统治了ES6之前的所有模块化编程,即使现在,在 ES6 module 被完全实现之前,还是这样。

在 CommonJS 中, 暴露模块使用 module.exportsexports ( exports 可以理解为 module.exports 的一个 快捷方式

很多人不明白暴露对象为什么会有两个,后面会介绍区别

在 CommonJS 中,有一个全局性方法 require(),用于加载模块。假定有一个数学模块 math.js,就可以像下面这样加载。

var math = require('math');

然后,就可以调用模块提供的方法:

var math = require('math');
math.add( 2,3); // 5

正是由于 CommonJS 使用的 require 方式的推动,才有了后面的 AMD、CMD 也采用的 require 方式来引用模块的风格

AMD 规范

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是,由于一个重大的局限,使得 CommonJS 规范不适用于浏览器环境。还是上面的代码,如果在浏览器中运行,会有一个很大的问题

var math = require('math');
math.add( 2,3); // 5

第二行 math.add(2, 3),在第一行 require(‘math’) 之后运行,因此必须等 math.js 加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。因此,浏览器端的模块,不能采用 "同步加载"(synchronous),只能采用 "异步加载"(asynchronous)。这就是AMD规范诞生的背景。

AMD是 "Asynchronous Module Definition" 的缩写,意思就是 "异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

模块必须采用特定的 define() 函数来定义。用法:define(id?, dependencies?, factory)

  • id:字符串,模块名称 (可选)
  • dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
  • factory: 工厂方法,返回一个模块函数

如果一个模块不依赖其他模块,那么可以直接定义在 define() 函数之中。示例:

// math.js
define(function () {
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add
    };
});

如果这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。

define(['Lib'], function (Lib) {
    function foo() {
        Lib.doSomething();
    }

    return {
        foo: foo
    };
});

上面代码中,当 require() 函数加载上面这个模块的时候,就会先加载 Lib.js 文件。

AMD 也采用 require() 语句加载模块,但是不同于 CommonJS,它要求两个参数:require([module], callback);

  • 第一个参数 [module],是一个数组,里面的成员就是要加载的模块;
  • 第二个参数 callback,则是加载成功之后的回调函数。

如果将前面的代码改写成AMD形式,就是下面这样:

require(['math'], function (math) {
    math.add(2, 3);
});

math.add() 与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。

目前,主要有两个 Javascript 库实现了AMD规范:require.js 和 curl.js

CMD 规范

CMD (Common Module Definition), 是 seajs 推崇的规范,CMD 则是依赖就近,用的时候再 require。它写起来是这样的:

define(function (require, exports, module) {
    var clock = require('clock');
    clock.start();
});

CMD 与 AMD一样,也是采用特定的 define() 函数来定义,用 require 方式来引用模块:define(id?, dependencies?, factory)

  • id:字符串,模块名称(可选)
  • dependencies: 是我们要载入的依赖模块(可选),使用相对路径。注意是数组格式
  • factory: 工厂方法,返回一个模块函数
define('hello', ['jquery'], function (require, exports, module) {
    // 模块代码
});

如果一个模块不依赖其他模块,那么可以直接定义在 define() 函数之中。

define(function (require, exports, module) {
    // 模块代码
});

注意:带 id 和 dependencies 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。

CMD 与 AMD 区别

AMD 和 CMD 最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。

  • AMD 依赖前置,js可以方便知道依赖模块是谁,立即加载;
  • CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

4、现阶段的标准

  • ES6 标准发布后,module 成为标准,标准使用是以 export 指令导出接口,以 import 引入模块
  • 但是在我们一贯的 node 模块中,我们依然采用的是 CommonJS 规范,使用 require 引入模块,使用 module.exports 导出接口

export 导出模块

export 语法声明用于导出函数、对象、指定文件(或模块)的原始值。

注意:

  • node 中使用的是 exports 不是 export。不要混淆了
  • ES6 标准发布后,module 成为标准,标准使用是以 export 导出接口,以 import 引入模块

export 有两种模块导出方式:

  • 命名式导出(名称导出),命名式导出每个模块可以多个
  • 默认导出(定义式导出),默认导出每个模块仅一个。
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN;         // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
 
export default expression;
export default function (…) { … }      // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
 
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
  • name1… nameN-导出的“标识符”。导出后,可以通过这个“标识符”在另一个模块中使用import引用
  • default-设置模块的默认导出。设置后import不通过“标识符”而直接引用默认导入
  • -继承模块并导出继承模块所有的方法和属性
  • as-重命名导出“标识符”
  • from-从已经存在的模块、脚本文件…导出

命名式导出

模块可以通过 export 前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出。

export { myFunction };           // 导出一个已定义的函数
export const foo = Math.sqrt(2); // 导出一个常量

还可以使用 * 和 from 关键字来实现的模块的继承:

export * from 'article';

模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有对象,而非导出成员可以认为是类中的私有对象:

var name = 'IT笔录';
var domain = 'http://itbilu.com';
 
export {name, domain}; // 相当于导出 {name:name,domain:domain}

模块导出时,我们可以使用 as 关键字对导出成员进行重命名:

var name = 'IT笔录';
var domain = 'http://itbilu.com';

export {name as siteName, domain};

注意,下面的语法有严重错误的情况:

// 错误演示
export 1; // 绝对不可以
 
var a = 100;
export a;

export 在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出1没有任何意义,也不可能在 import 的时候有一个变量与之对应

export a 虽然看上去成立,但是 a 的值是一个数字,根本无法完成解构,因此必须写成 export {a} 的形式。即使 a 被赋值为一个function,也是不允许的。而且,大部分风格都建议,模块中最好在末尾用一个 export 导出所有的接口,例如:

export { fun as default,a,b,c};

默认导出

默认导出也被称做定义式导出。命名式导出可以导出多个值,但在在 import 引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块 import 导入时也会很容易引用。

export default function() {}; // 可以导出一个函数
export default class(){};     // 也可以出一个类

命名式导出默认导出

默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了 default 名称的命名导出。

下面两种导出方式是等价的:

const D = 123;
 
export default D;
export { D as default };

export 使用示例

使用名称导出一个模块时:

// "my-module.js" 模块
export function cube(x) {
    return x * x * x;
}

const foo = Math.PI + Math.SQRT2;
export {foo};

在另一个模块(脚本文件)中,我们可以像下面这样引用:

import {cube, foo} from 'my-module';

console.log(cube(3)); // 27
console.log(foo);     // 4.555806215962888

使用默认导出一个模块时:

// "my-module.js"模块
export default function (x) {
    return x * x * x;
}

在另一个模块(脚本文件)中,我们可以像下面这样引用,相对名称导出来说使用更为简单:

// 引用 "my-module.js"模块
import cube from 'my-module';

console.log(cube(3)); // 27

5、import 引入模块

import 语法声明用于从已导出的模块、脚本中,导入函数、对象、指定文件(或模块)的原始值。

import 模块导入与 export 模块导出功能相对应,也存在两种模块导入方式:

  • 命名式导入(名称导入)
  • 默认导入(定义式导入)。

import 的语法跟 require 不同,而且 import 必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言风格一致。

import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";
  • name-从将要导入模块中收到的导出值的名称
  • member, memberN-从导出模块,导入指定名称的多个成员
  • defaultMember-从导出模块,导入默认导出成员
  • alias, aliasN-别名,对指定导入成员进行的重命名
  • module-name-要导入的模块。是一个文件名
  • as-重命名导入成员名称(“标识符”)
  • from-从已经存在的模块、脚本文件等导入

命名式导入

我们可以通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员:

注意,花括号里面的变量与 export 后面的变量一一对应

import {myMember} from "my-module";
import {foo, bar} from "my-module";

通过 * 符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块("my-module.js")所有的导出绑定内容,插入到当前模块(’myModule’)的作用域中:

import * as myModule from "my-module";

导入模块对象时,也可以使用as对导入成员重命名,以方便在当前模块内使用:

import {reallyReallyLongModuleMemberName as shortName} from "my-module";

导入多个成员时,同样可以使用别名:

import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";

导入一个模块,但不进行任何绑定:import "my-module";

默认导入

在模块导出时,可能会存在默认导出。同样的,在导入时可以使用 import 指令导出这些默认值。

直接导入默认值:import myDefault from "my-module";

也可以在命名空间导入和名称导入中,同时使用默认导入:

import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用
//或 
import myDefault, {foo, bar} from "my-module";    // 指定成员导入

import 使用示例

// --file.js--
function getJSON(url, callback) {
    let xhr = new XMLHttpRequest();
    xhr.onload = function () {
        callback(this.responseText)
    };
    xhr.open("GET", url, true);
    xhr.send();
}

export function getUsefulContents(url, callback) {
    getJSON(url, data => callback(JSON.parse(data)));
}

// --main.js--
import {getUsefulContents} from "file";

getUsefulContents("http://itbilu.com", data => {
    doSomethingUseful(data);
});

6、default 关键字

// d.js
export default function () {
}

// 等效于:
function a() {
};
export {a as default};

在 import 的时候,可以这样用:

import a from './d';

// 等效于,或者说就是下面这种写法的简写,是同一个意思
import {default as a} from './d';

这个语法糖的好处就是 import 的时候,可以省去花括号{}。

简单的说,如果 import 的时候,你发现某个变量没有花括号括起来(没有*号),那么你在脑海中应该把它还原成有花括号的 as 语法。

所以,下面这种写法你也应该理解了吧:import $,{each,map} from 'jquery';

import 后面第一个 $ 是 {defalut as $} 的替代写法。

7、as关键字

as 简单的说就是取一个别名,export 中可以用,import 中其实可以用:

// a.js
var a = function () {
};
export {a as fun};

// b.js
import {fun as a} from './a';

a();

上面这段代码,export 的时候,对外提供的接口是 fun,它是 a.js 内部 a 这个函数的别名,但是在模块外面,认不到 a,只能认到 fun。

import 中的 as 就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。之所以是这样,是因为有的时候不同的两个模块可能通过相同的接口,比如有一个 c.js 也通过了 fun 这个接口:

// c.js
export function  fun() {};

如果在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。

8、CommonJS 中 module.exportsexports 的区别

Module.exports

The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Note that assigning the desired object to exports will simply rebind the local exports variable, which is probably not what you want to do.

译文:module.exports对象是由模块系统创建的。 有时这是难以接受的;许多人希望他们的模块成为某个类的实例。 为了实现这个,需要将期望导出的对象赋值给module.exports。 注意,将期望的对象赋值给exports会简单地重新绑定到本地exports变量上,这可能不是你想要的。

Module.exports

The exports variable is available within a module’s file-level scope, and is assigned the value of module.exports before the module is evaluated. It allows a shortcut, so that module.exports.f = … can be written more succinctly as exports.f = …. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:

译文:exports变量是在模块的文件级别作用域内有效的,它在模块被执行前被赋于 module.exports 的值。它有一个快捷方式,以便 module.exports.f = … 可以被更简洁地写成exports.f = …。 注意,就像任何变量,如果一个新的值被赋值给exports,它就不再绑定到module.exports(其实是exports.属性会自动挂载到没有命名冲突的module.exports.属性)

从 Api文档 上面的可以看出,从 require 导入方式去理解,关键有两个变量(全局变量 module.exports,局部变量 exports )、一个返回值( module.exports )

function require(...) {
    var module = {exports: {}};
    ((module, exports) => {
        // 你的被引入代码 Start
        // var exports = module.exports = {}; (默认都有的)
        function some_func() {
        };
        exports = some_func;
        // 此时,exports不再挂载到module.exports,
        // export将导出{}默认对象
        module.exports = some_func;
        // 此时,这个模块将导出some_func对象,覆盖exports上的some_func
        // 你的被引入代码 End
    })(module, module.exports);
    // 不管是 exports还是module.exports,最后返回的还是module.exports
    return module.exports;
}

demo.js:

console.log(exports); // {}
console.log(module.exports); // {}
console.log(exports === module.exports); // true
console.log(exports == module.exports); // true
console.log(module);
/**
 Module {
id:  '.',
exports: {},
parent:  null,
filename:  '/Users/larben/Desktop/demo.js',
loaded:  false,
children: [],
paths:
[  '/Users/larben/Desktop/node_modules',
'/Users/larben/node_modules',
'/Users/node_modules',
'/node_modules' ] }
 */

注意

  1. 每个 js 文件一创建,都有一个 var exports = module.exports = {}; 使 exports 和 module.exports 都指向一个空对象。
  2. module.exports 和 exports 所指向的内存地址相同

每一个 node.js 执行文件,都自动创建一个 module 对象,同时,module对象会创建一个叫 exports 的属性,初始化的值是 {}

require 是值的拷贝,不是值引用,所以不用担心异步导致的数据污染。

区别主要有3点:

  1. module.exports 初始值为一个空对象 {}
  2. require() 返回的是 module.exports 而不是 exports
  3. exports 是指向的 module.exports 的值的引用。

前两条很好理解,第三条,意思是:

  • 如果 exports = newObject,那么 exports 断开了与 module.exports 的引用,两者就不再指向一个内存地址了,所有才有了如下的写法:
            exports = module.exports = somethings
        等价于
            module.exports = somethings
            exports = module.exports
  • 如果 module.exports = newObject,exports 跟 module.exports 不再指向同一个值(或者叫内存地址),那么 exports 就失效了。

总结:

  • 如果不使用 module.exports,仅使用 exports,那么 exports 只能得到属性,不能重新赋值对象。建议使用 exports.x 等写法。
  • 如果使用了module.exports = newObject,那么这时候 exports 就失效了,必须使用 exports = module.exports = newObject 建立联系


 

猜你喜欢

转载自blog.csdn.net/freeking101/article/details/116761828