最初接触到模块规范的时候,脑子里面并没有这个概念,不明白什么是模块,模块导入与导出原理。更傻傻分不清什么时候require、什么时候import,以及CommonJS规范module.exportsexports的区别,ES6模块export defaultexport的不同。最近学习了一下,记录一下自己的理解!如有不对,还请各位大佬及时指出!

CommonJS规范

module.exports属性

Node内部提供一个Module构建函数,所有模块都是Module的实例。

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是Module的实例,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX。

require方法用于加载模块:

var example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

exports变量

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令var exports = module.exports;

我的理解是这样的:

//module.exports默认是一个对象
let defaultVal = Object.create( null );
module.exports = defaultVal;

//最初exports指向module.exports
let exports = module.exports;

//因此我们可以通过下面的方式,向exports对象添加方法,扩充导出的值!
exports.area = function (r) {
  return Math.PI * r * r;
};
exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

模块导出的值未必是一个对象,可以是任意类型值!

//1、exports无效,导出默认空对象defaultVal
exports = function(x) {console.log(x)};

//2、exports.hello无效,导出字符串"Hello world"
exports.hello = function() {
  return 'hello';
};
module.exports = 'Hello world';

//3、exports.text无效,导出对象obj
exports.text = "Hello world";
const obj = {
    info: "我还是曾经那个少年!"
}
module.exports = obj;

//4、exports.text有效,导出对象 { text: "Hello world", info: "我还是曾经那个少年" }
exports.text = "Hello world"
module.exports.info = "我还是曾经那个少年!"

//5、有效,导出函数 function (x){ console.log(x) }
module.exports = function (x){ console.log(x) };

上面前四种通过exports导出的值都是无效的,这是为什么呢?

因为模块对外导出的是module.exports,而非exports,前三种情况都切断了exports与module.exports的联系!

也就是说exports === module.exportsfalse,不再指向同一个值,而我们只会对外导出module.exports,这句话很重要!

第四种情况,exportsmodule.exports依然指向同一个值,都指向默认的对象!

为了防止出错,最简单的处理方法,就是放弃使用exports,只使用module.exports!

require命令

Node使用CommonJS模块规范,内置的require命令用于加载模块文件。

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

加载规则

require命令用于加载文件,后缀名默认为.js。

var foo = require('foo');
//  等同于
var foo = require('foo.js');

根据参数的不同格式,require命令去不同路径寻找模块文件。

  • 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js。
  • 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js。
  • 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。

    举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件。

    • /usr/local/lib/node/bar.js
    • /home/user/projects/node_modules/bar.js
    • /home/user/node_modules/bar.js
    • /home/node_modules/bar.js
    • /node_modules/bar.js
    这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
  • 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
  • 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
  • 如果想得到require命令加载的确切文件名,使用require.resolve()方法。
目录的加载规则

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。

在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。

// package.json
{ 
  "name" : "some-library",
  "main" : "./lib/some-library.js" 
}

require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。

模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。

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

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

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。

然后,加载上面的模块。

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

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

上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。

ES6 Module

ES6模块不是对象,一个模块就是一个文件,通过export命令显示指定输入代码,通过import命令输入。

export

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

//写法一:
export const num = 123;
export const name = "xqs";
export function fn(){
    console.log(obj.text);
}
export const obj = {
    text: "我还是曾经那个少年!"
}

//写法二
export const num = 123;
export const name = "xqs";
function fn(){
    console.log(obj.text);
}
const obj = {
    text: "我还是曾经那个少年!"
}
export { fn,  obj }

//写法三
const num = 123;
const name = "xqs";
function fn(){
   console.log(obj.text);
}
const obj = {
    text: "我还是曾经那个少年!"
}
export { num, name }
export { fn, obj }

//写法四
const num = 123;
const name = "xqs";
function fn(){
    console.log(obj.text);
}
const obj = {
    text: "我还是曾经那个少年!"
}
export { num, name, fn, obj }

上面的写法是等价的,我们一般采用最后一种写法,因为输出的变量更直观的可以看到!
仅通过export命令输出变量,当我们通过import命令接收时,后面必须有一对大括号{},或者整体加载通过*

    //test.js
    const num = 123;
    const name = "xqs";
    function fn(){
        console.log(obj.text);
    }
    const obj = {
        text: "我还是曾经那个少年!"
    }
    export { num, name, fn, obj }
    
    //main.js
    //写法一:
    import { name, fn } from "./test.js"
    fn();
    console.log( `My name is ${name}` );
    
    //写法二
    import * as res from "./test.js"
    res.fn();
    console.log( `My name is ${res.name}` );

通常采用第一种写法,因为ES6模块是“编译时加载”,在上面的例子用我们仅需要namefn,所以我们仅需要加载namefn即可,而不需要全部加载,这样有利于提高加载效率!

export default

我们也通过export default命令为模块指定默认输出!

    //test-default.js
    export default function foo(){
        console.log( "没有一丝丝改变!" )
    }
    
    //main-default.js
    import fooFuntion from "./test-default.js"
    fooFuntion();
    
    //test-default-1.js
    const nameObj = {
        name: "蒙奇·D·路飞"
    };
    export default nameObj
    
    //main-default-2.js
    import obj from "./test-default-1.js"
    console.log( obj.name );
    
    

就像上面的例子,通过export default默认输出一个函数,但当我们通过import接收时后面不再需要大括号,并且可以随意指定接收的名字,例如obj

同为模块输出,那么exportexport default有什么不同呢?

    //export.js
    export function fn(){
        console.log( "我是路飞!" )
    }

    //export-default.js
    export default function foo(){
        console.log( "艾斯是我的哥哥!" )
    }
    
    //main.js
    import { fn } from "./export.js"
    import foo from "./export-default.js"
    
    fn();
    foo();

首先在通过import输入的方式上略有不同;其次,一个模块只有一默认输出,那么export default命令只能使用一次,而export可以使用多次!

如果说与CommonJs规范比较的话,个人觉得export更像exports,因为exports也只能输出一种格式,那就是对象,而export输出的类似对象格式的一组变量!export default像是module.exports可以输出任意的格式,不同的是,export default只能使用一次,而module.exports可以在一个模块中多次使用,当然了仅限于在不改变默认输出的那个对象的情况下。

言归正传,export default本质上就是输出一个叫作default的变量,系统允许我们为它取任何名字!

    //test.js
    const name = "xqs";
    export { name as default }
    //等同于
    //export default name;
    
    //main.js
    import { default as testName } from "./test.js";
    //等同于
    import testName from "./test.js";

正因为export default命令其实是输出一个叫作default的变量,因此只能使用一次。

export与export default同时使用:

    //test.js
    export function fn(){
        console.log( "我是路飞!" )
    }

    export default function foo(){
        console.log( "艾斯是我的哥哥!" )
    }
    
    //main.js
    //写法一:
    import foo, { fn } from "./test.js"
    fn();     
    foo();  //default
    
    //写法二:
    import * as res from "./test.js"
    res.fn();
    res.default();  //default
    
    //写法三:
    import { default as foo, fn } from "./test.js"
    fn();
    foo();  //default

写法二、三,更可以直观的看出export default输出的是一个叫default变量!

import()

我们知道ES6 module静态加载,那么就是说,在代码执行之前,就已经通过import拿到了输入的变量!那么就不像require一样实现动态加载!幸运的是现在引用了import()函数来完成动态加载!

    //test.js
    export function fn(){
        console.log( "我是路飞!" )
    }

    export default function foo(){
        console.log( "艾斯是我的哥哥!" )
    }
    
    //main.js
    import( "./test.js" )
    .then( ({ default : foo , fn }) => {
      fn();
      foo();
    })
    .catch( error => {
        
    });

通过import()可以像require一样实现动态加载。两者的区别在于,import()返回的是Promise实例是异步加载,而require是同步加载!

ES6 模块与 CommonJS 模块的差异

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

CommonJS输出的值的拷贝,第一次加载模块时会缓存该模块,并且加载该模块时,即使改变输入的值,也会对原模块内部的变量造成影响。而ES6 模块输出的是值的引用,引入该模块修改输入的变量时,会对原模块内部的变量有影响!

  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    CommonJS 模块是运行时加载,只有执行到require时才会加载该模块,而import引入ES6 模块是在编译阶段执行,在代码执行之前就已经拿到输入变量。

    参考资料


xqslsm
21 声望0 粉丝