深入理解module.exports、exports、require、export、export default、import


深入理解module.exports、exports、require、export、export default、import

 

前言:说到module.exports、exports、require、export、export default、import这些,有一点我们是必须要提一下的,就是模块化编程方式。以上这些都是模块之间的导入和导出。

什么是模块化

当你的网站越来越复杂时,我们往往会遇到一下情况,导致我们生产效率低,可维护性差:

  • 恼人的命名冲突
  • 繁琐的文件依赖   

历史上,JavaScript一直没有模块(module)体系, 无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 其他语言都有这项功能,比如Ruby的 require、Python的 import , 甚至就连CSS都有 @import ,但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。 

由此,我们把模块化的概念理解为将一个大程序拆分成互相依赖的小文件,再用简单的方法拼接起来。那程序中的模块到底该具有哪些特性就满足我们的使用了呢?

  • 模块作用域
    • 模块之间不需要考虑全局命名空间冲突的问题。
  • 模块之间的通讯规则
    • 首先,各个模块之间是相互依赖,相互关联的。例如 CPU 需要读取内存中的数据来进行计算,然后把计算结果又交给了我们的操作系统。
    • 既然相互关联,那么模块之间肯定是可以通讯的。
    • 模块之间的通讯,也就意味着存在输入和输出。

模块通讯规则

ES6之前已经出现了js模块加载的方案,最主要的是CommonJS和AMD规范。commonjs主要应用于服务器,实现同步加载,如nodejs。AMD规范应用于浏览器,如requirejs,为异步加载。同时还有CMD规范,为同步加载方案如seaJS。

1、CommonJS规范

CommonJS规范规定了每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。CommonJS 规范的主要适用场景是服务器端编程,所以采用同步加载模块的策略。如果我们依赖3个模块,代码会一个一个依次加载它们。

require 模块导入

// 核心模块 var fs = require('fs') // 第三方模块 // npm install jquery var marked = require('jquery') // 用户模块(自己写的),正确的,正确的方式 // 注意:加载自己写的模块,相对路径不能省略 ./ var foo = require('./foo.js') // 用户模块(自己写的),正确的(推荐),可以省略后缀名 .js var foo = require('./foo')

node模块分类

  1. 核心模块
    1. 由 Node 本身提供,具名的,例如 fs 文件操作模块、http 网络操作模块
  2. 第三发模块
    1. 由第三方提供,使用的时候我们需要通过 npm 进行下载然后才可以加载使用,例如我们使用过的 mimeejsmarked
    2. 注意:不可能有第三方包的名字和核心模块的名字是一样的,否则会造成冲突
  3. 用户自己写的模块 
    1. 我们在文件中写的代码很多的情况下不好编写和维护,所以我们可以考虑把文件中的代码拆分到多个文件中,那这些我们自己创建的文件就是用户模块  

核心模块

  • 核心模块就是 node 内置的模块,需要通过唯一的标识名称来进行获取。
  • 每一个核心模块基本上都是暴露了一个对象,里面包含一些方法供我们使用
  • 一般在加载核心模块的时候,变量的起名最好就和核心模块的标识名同名即可
    • 例如:const fs = require('fs')
  • 核心模块本质上也是文件模块
    • 核心模块已经被编译到了 node 的可执行程序,一般看不到
    • 可以通过查看 node 的源码看到核心模块文件
    • 核心模块也是基于 CommonJS 模块规范

Node 中都以具名的方式提供了不同功能的模块,使用的时候都必须根据特定的核心模块名称来加载使用。

参考文档:https://nodejs.org/dist/latest-v9.x/docs/api/

模块名称 作用
fs 文件操作
http 网络操作
path 路径操作
url url 地址操作
os 操作系统信息
net 一种更底层的网络操作方式
querystring 解析查询字符串
util 工具函数模块
... ...

 文件模块

以 ./ 或 ../ 开头的模块标识就是文件模块,一般就是用户编写的。

第三方模块

一般就是通过 npm install 安装的模块就是第三方模块。

加载规则如下:

  • 如果不是文件模块,也不是核心模块
  • node 会去 node_modules 目录中找(找跟你引用的名称一样的目录),例如这里 require('underscore')
  • 如果在 node_modules 目录中找到 underscore 目录,则找该目录下的 package.json 文件
  • 如果找到 package.json 文件,则找该文件中的 main 属性,拿到 main 指定的入口模块
  • 如果过程都找不到,node 则取上一级目录下找 node_modules 目录,规则同上。。。
  • 如果一直找到代码文件的根路径还找不到,则报错。。。

注意:对于第三方模块,我们都是 npm install 命令进行下载的,就放到项目根目录下的 node_modules 目录。

深入模块化加载机制

  1. 从module path数组中取出第一个目录作为查找基准。
  2. 直接从目录中查找该文件,如果存在,则结束查找。如果不存在,则进行下一条查找。
  3. 尝试添加.js、.json、.node后缀后查找,如果存在文件,则结束查找。如果不存在,则进行下一条。
  4. 尝试将require的参数作为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件。
  5. 尝试查找该文件,如果存在,则结束查找。如果不存在,则进行第3条查找。
  6. 如果继续失败,则取出module path数组中的下一个目录作为基准查找,循环第1至5个步骤。
  7. 如果继续失败,循环第1至6个步骤,直到module path中的最后一个值。
  8. 如果仍然失败,则抛出异常。

整个查找过程十分类似原型链的查找和作用域的查找。所幸Node.js对路径查找实现了缓存机制,否则由于每次判断路径都是同步阻塞式进行,会导致严重的性能消耗。

 

exports 模块导出 

在node中,每个模块都有一个module 对象,在该module 对象中,有一个成员叫作exports,默认最后会return module.exports。也就是说当需要向外导出成员时,只需要将成员挂载到module.exports上。当require该模块时,就会默认导入该模块暴露出的module.exports对象,注意不是exports对象.

导出多个成员1

module.exports.a = 123; module.exports.b = 'abc'; module.exports.c = {}; 

导出多个成员2

module.exports = { a: 123, b: 'abc', c: {} } 

 导出多个成员3,使用exports挂载

// module.exports 提供了一个别名 exports,exports是module.exports的一个引用,它们共同指向一个地址。 console.log(exports === module.exports) true exports.a = 123; exports.b = 'abc'; exports.c = {};

导出单个成员,必须使用module.exports

module.exports = function(a, b){ return a + b } //错误写法(因为exports 为module.exports的一个引用, //当直接给exports 赋值后,会断开与module.exports的引用关系。而最终模块导出的module.exports) exports = function(a, b) { return a + b } 

深入理解module.exports 与exports 的区别

混合导出

exports.foo = 123; //导出 {foo:123} module.exports.a = 'a'; //导出 {foo:123,a:'a'} 
exports.a = 123; //导出 {a: 123} exports = {}; //断开与module.exports的引用关系 exports.b = 'b'; //因为引用关系已经断开,干扰 module.exports.c = 233; //导出 {a: 123, c: 233}
//直接给exports赋值,会断开与module.exports的引用关系。同理,直接给module.exports赋值,也会断开与exports的引用关系。 module.exports = 'helllo'; //导出 {'hello'} exports.a = 'a'; //干扰
exports.foo = 'hello'; //{foo: 'hello'} module.exports.a = 'a'; //{foo: 'hello',a: 'a'} exports = { //断开引用关系 a: 'b' }; module.exports.foo = 'world'; // {foo: 'world', a: 'a'} exports.c = 'c'; // 干扰 exports = module.exports; //重新建立引用关系 exports.a = 123; // {foo: 'world', a: 123} module.exports = function(){} // {function(){}}

一般导出单个模块用 module.exports = 123 ,导出多个模块使用 exports.a = 1;exports.b = 2;.....

要是实在分不清楚,建议直接使用module.exports对象导出成员。绝对不会出错,哈哈! 

 

2、AMD规范(https://github.com/amdjs/amdjs-api/wiki/AMD)

AMD 是 Asynchronous Module Definition 的简称,即“异步模块定义”,是从 CommonJS 讨论中诞生的。AMD 优先照顾浏览器的模块加载场景,使用了异步加载和回调的方式。

服务器端加载方式为同步加载,因为所有的模块都存在了本地硬盘,同步加载需要等待的时间就是硬盘的读取时间。这对于服务端来说不是什么问题。但是对于浏览器,所有的模块都存在于服务端,等待的时间多数取决于网速的快慢。网速慢的时候,浏览器就会处于“假死”状态。因此,浏览器加载模块应采用异步加载的方式,这也是AMD规范诞生的背景。

RequireJS

模块定义

通过define方法定义模块,但是按照2种情况进行书写。

  1. 该模块独立存在,不依赖其他模块(可以返回任何值):      
    define(function() { return { // 返回的接口 } })

     

  2. 该模块依赖其他模块:
    define(['module1','module2'], function(module1, module2) { return { // 返回的接口 } })

     

require模块加载 

//方法1 var module2 =require('module1'); //方法2 require(['module1'], function (module1) { module1.module1Fun(1, 3); }); require方法可以进行配置: require.config({ paths: { //为模块指定位置,可以为服务器上的地址,也可以为外部网址等等,也可以指定多个地址,防止模块加载出错。 jquery: 'module/libs/jquery-10.3', } }); require(['jquery'],function($){});

 模块导出

在Requirejs中,模块导出共有三种方式: 

  1. 通过return方式导出,优先级最高; 
  2. 通过module.exports对象赋值导出,优先级次之; 
  3. 通过exports对象赋值导出,优先级最低;

上面的三种优先级是绝对的优先级,无关代码的顺序,例如即使将exports导出放在最后,也会被module.exports覆盖,另外导出的内容只能是优先级最高的那个,而且仅仅包含其内容,绝不会它们内容的组合或并集。

//通过 return 方式导出,优先级最高,官方推荐 define(['module1','module2'], function(module1, module2) { return { // 返回的接口 } })
//将导出的成员挂载到 module.exports 对象上,写法继承CommonJS,优先级低于return define(function(require, exports, module) { // 导出模块内容 module.exports = { username : 'HJJ' } }); 
//将导出成员挂载到exports上(不可以直接给exports直接赋值),优先级最低,写法继承CommonJS define(function(require, exports, module) { exports. username = 'HJJ' }); //'exports' 仅仅是 'module.exports' 的一个引用。在 'factory' 内部给 'exports' 重新赋值时,并不会改变 'module.exports' 的值。因此给 'exports' 赋值是无效的,不能用来更改模块接口。 //还有就是如果直接将导出成员挂载到exports上,会导致实参形参傻傻分不清楚

 

3、CMD规范(https://github.com/seajs/seajs/issues/242)

CMD(Common Module Definition)通用模块定义。CMD是在AMD基础上改进的一种规范,均适用于浏览器环境,和AMD不同在于对依赖模块的执行时机处理不同,CMD是就近依赖,而AMD是前置依赖。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

// CMD define(function(require, exports, module) { var module1 = require('./module1') module1.doSomething() // 此处略去 100 行 var module2 = require('./module2') // 依赖可以就近书写 module2.doSomething() // ... }) // AMD 默认推荐的是 define(['./module1', './module2'], function(module1, module2) { // 依赖必须一开始就写好 module1.doSomething() // 此处略去 100 行 module2.doSomething() ... }) 

 CMD模块定义

define({ "foo": "bar" });
define('I am a template. My name is {{name}}.');
define(function(require, exports, module) { // 模块代码 }); define('hello', ['jquery'], function(require, exports, module) { // 模块代码 });

 

CMD模块的导入导出同AMD,请移步AMD规范。

4、ES6

import模块导入 

1、import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { firstName, lastName as surname, year } from './profile.js';

2、import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。但是,如果a是一个对象,改写a的属性是允许的。

import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only; import {a} from './xxx.js' a.foo = 'hello'; // 合法操作,建议都当成只读属性,方便排错

3、import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

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

 

import {myMethod} from 'util'; foo(); import { foo } from 'my_module';

4、由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module; // 报错 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; }

 5、import语句会执行所加载的模块,因此可以有下面的写法。

import 'lodash'; //上面代码仅仅执行lodash模块,但是不输入任何值。

6、import语句是 Singleton 模式。如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

import 'lodash'; import 'lodash'; //等同于 import 'lodash'; import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module';

7、同一个模块里面 ,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令可以同时使用。但是不建议。原因如下:

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块 let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

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

// ES6模块 import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

//import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。 require('core-js/modules/es6.symbol'); require('core-js/modules/es6.promise'); import React from 'React';

export导出模块

写法1、

export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; export function multiply(x, y) { return x * y; };

写法2(可以使用as关键字重命名)、

var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; function v1() { ... } export {firstName, lastName, year, v1 as streamV1}; //使用大括号指定所要输出的一组变量,推荐使用这种方式,简介明了

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

// 报错 export 1; // 报错 var m = 1; export m; // 报错 function f() {} export f; ----------------- export var m = 1; var m = 1; export {m}; var n = 1; export {n as m}; export function f() {}; //正确 function f() {} export {f};

 最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

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

默认导出(export default) 

每个模块支持我们导出一个没有名字的变量,使用关键语句export default来实现.

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

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

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

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

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

// 正确 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;

如果想在一条import语句中,同时输入默认方法和其他变量,可以写成下面这样。

import _, { each } from 'lodash'; //对应上面代码的export语句如下 export default function (){ //... } export function each (obj, iterator, context){ //... }

export 与 import 的复合写法 

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

export { foo, bar } from 'my_module'; // 可以简单理解为 import { foo, bar } from 'my_module'; export { foo, bar }; export { es6 as default } from './someModule'; // 等同于 import { es6 } from './someModule'; export default es6;

深入理解module.exports、exports、require、export、export default、import

时间:2019-02-27
本文章向大家介绍深入理解module.exports、exports、require、export、export default、import,主要包括深入理解module.exports、exports、require、export、export default、import使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言:说到module.exports、exports、require、export、export default、import这些,有一点我们是必须要提一下的,就是模块化编程方式。以上这些都是模块之间的导入和导出。

什么是模块化

当你的网站越来越复杂时,我们往往会遇到一下情况,导致我们生产效率低,可维护性差:

  • 恼人的命名冲突
  • 繁琐的文件依赖   

历史上,JavaScript一直没有模块(module)体系, 无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 其他语言都有这项功能,比如Ruby的 require、Python的 import , 甚至就连CSS都有 @import ,但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。 

由此,我们把模块化的概念理解为将一个大程序拆分成互相依赖的小文件,再用简单的方法拼接起来。那程序中的模块到底该具有哪些特性就满足我们的使用了呢?

  • 模块作用域
    • 模块之间不需要考虑全局命名空间冲突的问题。
  • 模块之间的通讯规则
    • 首先,各个模块之间是相互依赖,相互关联的。例如 CPU 需要读取内存中的数据来进行计算,然后把计算结果又交给了我们的操作系统。
    • 既然相互关联,那么模块之间肯定是可以通讯的。
    • 模块之间的通讯,也就意味着存在输入和输出。

模块通讯规则

ES6之前已经出现了js模块加载的方案,最主要的是CommonJS和AMD规范。commonjs主要应用于服务器,实现同步加载,如nodejs。AMD规范应用于浏览器,如requirejs,为异步加载。同时还有CMD规范,为同步加载方案如seaJS。

1、CommonJS规范

CommonJS规范规定了每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。CommonJS 规范的主要适用场景是服务器端编程,所以采用同步加载模块的策略。如果我们依赖3个模块,代码会一个一个依次加载它们。

require 模块导入

// 核心模块 var fs = require('fs') // 第三方模块 // npm install jquery var marked = require('jquery') // 用户模块(自己写的),正确的,正确的方式 // 注意:加载自己写的模块,相对路径不能省略 ./ var foo = require('./foo.js') // 用户模块(自己写的),正确的(推荐),可以省略后缀名 .js var foo = require('./foo')

node模块分类

  1. 核心模块
    1. 由 Node 本身提供,具名的,例如 fs 文件操作模块、http 网络操作模块
  2. 第三发模块
    1. 由第三方提供,使用的时候我们需要通过 npm 进行下载然后才可以加载使用,例如我们使用过的 mimeejsmarked
    2. 注意:不可能有第三方包的名字和核心模块的名字是一样的,否则会造成冲突
  3. 用户自己写的模块 
    1. 我们在文件中写的代码很多的情况下不好编写和维护,所以我们可以考虑把文件中的代码拆分到多个文件中,那这些我们自己创建的文件就是用户模块  

核心模块

  • 核心模块就是 node 内置的模块,需要通过唯一的标识名称来进行获取。
  • 每一个核心模块基本上都是暴露了一个对象,里面包含一些方法供我们使用
  • 一般在加载核心模块的时候,变量的起名最好就和核心模块的标识名同名即可
    • 例如:const fs = require('fs')
  • 核心模块本质上也是文件模块
    • 核心模块已经被编译到了 node 的可执行程序,一般看不到
    • 可以通过查看 node 的源码看到核心模块文件
    • 核心模块也是基于 CommonJS 模块规范

Node 中都以具名的方式提供了不同功能的模块,使用的时候都必须根据特定的核心模块名称来加载使用。

参考文档:https://nodejs.org/dist/latest-v9.x/docs/api/

模块名称 作用
fs 文件操作
http 网络操作
path 路径操作
url url 地址操作
os 操作系统信息
net 一种更底层的网络操作方式
querystring 解析查询字符串
util 工具函数模块
... ...

 文件模块

以 ./ 或 ../ 开头的模块标识就是文件模块,一般就是用户编写的。

第三方模块

一般就是通过 npm install 安装的模块就是第三方模块。

加载规则如下:

  • 如果不是文件模块,也不是核心模块
  • node 会去 node_modules 目录中找(找跟你引用的名称一样的目录),例如这里 require('underscore')
  • 如果在 node_modules 目录中找到 underscore 目录,则找该目录下的 package.json 文件
  • 如果找到 package.json 文件,则找该文件中的 main 属性,拿到 main 指定的入口模块
  • 如果过程都找不到,node 则取上一级目录下找 node_modules 目录,规则同上。。。
  • 如果一直找到代码文件的根路径还找不到,则报错。。。

注意:对于第三方模块,我们都是 npm install 命令进行下载的,就放到项目根目录下的 node_modules 目录。

深入模块化加载机制

  1. 从module path数组中取出第一个目录作为查找基准。
  2. 直接从目录中查找该文件,如果存在,则结束查找。如果不存在,则进行下一条查找。
  3. 尝试添加.js、.json、.node后缀后查找,如果存在文件,则结束查找。如果不存在,则进行下一条。
  4. 尝试将require的参数作为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件。
  5. 尝试查找该文件,如果存在,则结束查找。如果不存在,则进行第3条查找。
  6. 如果继续失败,则取出module path数组中的下一个目录作为基准查找,循环第1至5个步骤。
  7. 如果继续失败,循环第1至6个步骤,直到module path中的最后一个值。
  8. 如果仍然失败,则抛出异常。

整个查找过程十分类似原型链的查找和作用域的查找。所幸Node.js对路径查找实现了缓存机制,否则由于每次判断路径都是同步阻塞式进行,会导致严重的性能消耗。

 

exports 模块导出 

在node中,每个模块都有一个module 对象,在该module 对象中,有一个成员叫作exports,默认最后会return module.exports。也就是说当需要向外导出成员时,只需要将成员挂载到module.exports上。当require该模块时,就会默认导入该模块暴露出的module.exports对象,注意不是exports对象.

导出多个成员1

module.exports.a = 123; module.exports.b = 'abc'; module.exports.c = {}; 

导出多个成员2

module.exports = { a: 123, b: 'abc', c: {} } 

 导出多个成员3,使用exports挂载

// module.exports 提供了一个别名 exports,exports是module.exports的一个引用,它们共同指向一个地址。 console.log(exports === module.exports) true exports.a = 123; exports.b = 'abc'; exports.c = {};

导出单个成员,必须使用module.exports

module.exports = function(a, b){ return a + b } //错误写法(因为exports 为module.exports的一个引用, //当直接给exports 赋值后,会断开与module.exports的引用关系。而最终模块导出的module.exports) exports = function(a, b) { return a + b } 

深入理解module.exports 与exports 的区别

混合导出

exports.foo = 123; //导出 {foo:123} module.exports.a = 'a'; //导出 {foo:123,a:'a'} 
exports.a = 123; //导出 {a: 123} exports = {}; //断开与module.exports的引用关系 exports.b = 'b'; //因为引用关系已经断开,干扰 module.exports.c = 233; //导出 {a: 123, c: 233}
//直接给exports赋值,会断开与module.exports的引用关系。同理,直接给module.exports赋值,也会断开与exports的引用关系。 module.exports = 'helllo'; //导出 {'hello'} exports.a = 'a'; //干扰
exports.foo = 'hello'; //{foo: 'hello'} module.exports.a = 'a'; //{foo: 'hello',a: 'a'} exports = { //断开引用关系 a: 'b' }; module.exports.foo = 'world'; // {foo: 'world', a: 'a'} exports.c = 'c'; // 干扰 exports = module.exports; //重新建立引用关系 exports.a = 123; // {foo: 'world', a: 123} module.exports = function(){} // {function(){}}

一般导出单个模块用 module.exports = 123 ,导出多个模块使用 exports.a = 1;exports.b = 2;.....

要是实在分不清楚,建议直接使用module.exports对象导出成员。绝对不会出错,哈哈! 

 

2、AMD规范(https://github.com/amdjs/amdjs-api/wiki/AMD)

AMD 是 Asynchronous Module Definition 的简称,即“异步模块定义”,是从 CommonJS 讨论中诞生的。AMD 优先照顾浏览器的模块加载场景,使用了异步加载和回调的方式。

服务器端加载方式为同步加载,因为所有的模块都存在了本地硬盘,同步加载需要等待的时间就是硬盘的读取时间。这对于服务端来说不是什么问题。但是对于浏览器,所有的模块都存在于服务端,等待的时间多数取决于网速的快慢。网速慢的时候,浏览器就会处于“假死”状态。因此,浏览器加载模块应采用异步加载的方式,这也是AMD规范诞生的背景。

RequireJS

模块定义

通过define方法定义模块,但是按照2种情况进行书写。

  1. 该模块独立存在,不依赖其他模块(可以返回任何值):      
    define(function() { return { // 返回的接口 } })

     

  2. 该模块依赖其他模块:
    define(['module1','module2'], function(module1, module2) { return { // 返回的接口 } })

     

require模块加载 

//方法1 var module2 =require('module1'); //方法2 require(['module1'], function (module1) { module1.module1Fun(1, 3); }); require方法可以进行配置: require.config({ paths: { //为模块指定位置,可以为服务器上的地址,也可以为外部网址等等,也可以指定多个地址,防止模块加载出错。 jquery: 'module/libs/jquery-10.3', } }); require(['jquery'],function($){});

 模块导出

在Requirejs中,模块导出共有三种方式: 

  1. 通过return方式导出,优先级最高; 
  2. 通过module.exports对象赋值导出,优先级次之; 
  3. 通过exports对象赋值导出,优先级最低;

上面的三种优先级是绝对的优先级,无关代码的顺序,例如即使将exports导出放在最后,也会被module.exports覆盖,另外导出的内容只能是优先级最高的那个,而且仅仅包含其内容,绝不会它们内容的组合或并集。

//通过 return 方式导出,优先级最高,官方推荐 define(['module1','module2'], function(module1, module2) { return { // 返回的接口 } })
//将导出的成员挂载到 module.exports 对象上,写法继承CommonJS,优先级低于return define(function(require, exports, module) { // 导出模块内容 module.exports = { username : 'HJJ' } }); 
//将导出成员挂载到exports上(不可以直接给exports直接赋值),优先级最低,写法继承CommonJS define(function(require, exports, module) { exports. username = 'HJJ' }); //'exports' 仅仅是 'module.exports' 的一个引用。在 'factory' 内部给 'exports' 重新赋值时,并不会改变 'module.exports' 的值。因此给 'exports' 赋值是无效的,不能用来更改模块接口。 //还有就是如果直接将导出成员挂载到exports上,会导致实参形参傻傻分不清楚

 

3、CMD规范(https://github.com/seajs/seajs/issues/242)

CMD(Common Module Definition)通用模块定义。CMD是在AMD基础上改进的一种规范,均适用于浏览器环境,和AMD不同在于对依赖模块的执行时机处理不同,CMD是就近依赖,而AMD是前置依赖。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

// CMD define(function(require, exports, module) { var module1 = require('./module1') module1.doSomething() // 此处略去 100 行 var module2 = require('./module2') // 依赖可以就近书写 module2.doSomething() // ... }) // AMD 默认推荐的是 define(['./module1', './module2'], function(module1, module2) { // 依赖必须一开始就写好 module1.doSomething() // 此处略去 100 行 module2.doSomething() ... }) 

 CMD模块定义

define({ "foo": "bar" });
define('I am a template. My name is {{name}}.');
define(function(require, exports, module) { // 模块代码 }); define('hello', ['jquery'], function(require, exports, module) { // 模块代码 });

 

CMD模块的导入导出同AMD,请移步AMD规范。

4、ES6

import模块导入 

1、import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { firstName, lastName as surname, year } from './profile.js';

2、import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。但是,如果a是一个对象,改写a的属性是允许的。

import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only; import {a} from './xxx.js' a.foo = 'hello'; // 合法操作,建议都当成只读属性,方便排错

3、import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

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

 

import {myMethod} from 'util'; foo(); import { foo } from 'my_module';

4、由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module; // 报错 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; }

 5、import语句会执行所加载的模块,因此可以有下面的写法。

import 'lodash'; //上面代码仅仅执行lodash模块,但是不输入任何值。

6、import语句是 Singleton 模式。如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

import 'lodash'; import 'lodash'; //等同于 import 'lodash'; import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module';

7、同一个模块里面 ,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令可以同时使用。但是不建议。原因如下:

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块 let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

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

// ES6模块 import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

//import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。 require('core-js/modules/es6.symbol'); require('core-js/modules/es6.promise'); import React from 'React';

export导出模块

写法1、

export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; export function multiply(x, y) { return x * y; };

写法2(可以使用as关键字重命名)、

var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; function v1() { ... } export {firstName, lastName, year, v1 as streamV1}; //使用大括号指定所要输出的一组变量,推荐使用这种方式,简介明了

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

// 报错 export 1; // 报错 var m = 1; export m; // 报错 function f() {} export f; ----------------- export var m = 1; var m = 1; export {m}; var n = 1; export {n as m}; export function f() {}; //正确 function f() {} export {f};

 最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

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

默认导出(export default) 

每个模块支持我们导出一个没有名字的变量,使用关键语句export default来实现.

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

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

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

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

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

// 正确 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;

如果想在一条import语句中,同时输入默认方法和其他变量,可以写成下面这样。

import _, { each } from 'lodash'; //对应上面代码的export语句如下 export default function (){ //... } export function each (obj, iterator, context){ //... }

export 与 import 的复合写法 

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

export { foo, bar } from 'my_module'; // 可以简单理解为 import { foo, bar } from 'my_module'; export { foo, bar }; export { es6 as default } from './someModule'; // 等同于 import { es6 } from './someModule'; export default es6;

 

参考资料:

ECMAScript 6 入门 --阮一峰

CMD模块定义规范

AMD规范

 

上一页 下一页

原文地址:http://www.manongjc.com/article/62801.html

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM