模块化

163 阅读6分钟

一、什么是模块化

  • 模块化就是将程序分成一个个的小模块来进行开发,然后再将其组合起来。

  • 开发模块时,每个模块都拥有独立的作用域,避免了全局变量污染,不会影响到其他模块的开发

  • 同时,也可以通过暴露出自己的某些变量、方法和对象,来让其他模块引入使用

  • 也可以通过引入其他模块的暴露部分,来开发自己的模块

  • 通过暴露和引入,可以实现对模块的组装从而开发出程序

二、CommonJs模块化规范

Module类源码

module.id            //带有绝对路径的模块文件名
module.filename      //模块的文件名,带有绝对路径
module.loaded        //表示模块是否已经完成加载
module.parent          //返回一个对象,表示调用该模块的模块。
module.children      //返回一个数组,表示该模块要用到的其他模块。
module.exports         //模块对外输出的值。需要打破模块封装性曝露的方法和属性,都要挂载到module.exports上。其它文件加载该模块,实际上就是读取module.exports属性

// 在 /Users/computer/Desktop/ccc/lib.js 文件中 console.log(module);
Module {
  id: '.',
  path: '/Users/computer/Desktop/ccc',
  exports: { name: 'test' },
  parent: null,
  filename: '/Users/computer/Desktop/ccc/main.js',
  loaded: false,``
  children: [
    Module {...}
  ],
  paths: [ //查找路径
    '/Users/computer/Desktop/ccc/node_modules',
    '/Users/computer/Desktop/node_modules',
    '/Users/computer/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}

模块化开发概要图

CommonJs模块化开发

2.1 module.exports 和 exports

测试代码:

index.js
var a = 3
var bb = function(a){
 console.log(a)
}
//exports实际上是对module.exports的引用,相当于是exports=module.exports,此时exports module.exports指向同一个对象空间
exports.a = a
exports.bb = bb
// 验证:用myexports=exports对exports进行引用,此时myexports exports module.exports 实际上指向同一个对象空间
var myexports = exports
myexports.c = 5
//但最终这个模块导出的是 module.exports 所指向的对象空间
//若修改 module.exports 则会创建另外一个对象空间,也就表示之前所有的引用都会失效,因为他们指向的是之前的对象空间
// module.exports = {
//     a: 4,
//     bb: function(a){
//         console.log(exports.a)
//     }
// }
//test.js
//实际上也是对对象的引用
const index = require("./index.js")

index.bb(index.a)
index.a = 5
console.log (require("./index.js"))

测试结果:

2.2 require

require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象。

而通过require来引入在node中的模块分成三种情形,

第一种是node自带的核心模块,引入该模块的方式是直接使用require(X),X为模块名称,

第二种是自定义模块以及本地文件,前提是X是一个路径,如"./"或者“../”等。

第一步:将X当做一个文件在对应的目录下查找;

  • 如果有后缀名,按照后缀名的格式查找对应的文件
  • 如果没有后缀名,会按照如下顺序:
    1. 直接查找文件X
    2. 查找X.js文件:当做JavaScript脚本文件解析
    3. 查找X.json文件:以JSON格式解析。
    4. 查找X.node文件:以编译后的二进制文件解析。.node文件通常是c/c++写的一些扩展模块

第二步:没有找到对应的文件,将X作为一个目录。查找目录下面的index文件

  1. 查找X/index.js文件
  2. 查找X/index.json文件
  3. 查找X/index.node文件

第三种是第三方模块,他首先得通过node i X,将X安装在node_module文件夹下,然后通过require(X),将其引入

查找X路径的流程是通过module对象中的paths数组依次查找

2.3 注意点

因为CommonJs导出的是一个对象(module.exports),所以其他模块引用的都是同一个对象,如果修改其中一个引用的值,所有的值都会改变。

三、ES6模块化规范

3.1 ES6 模块与 CommonJS 模块的差异

  • ​ CommonJs输出的是一个值的浅拷贝,ES6模块输出的是值的引用s

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

    第一个差异是指当CommonJs输出值后,内部模块值的改变不会影响到已经输出的值。而由于ES6模块是对值的引用,因此他可以实时获取到模块内的已经输出的值的变化,是一种动态取值。

    CommonJs模块下:

    //index.js
    //在模块内部修改已经输出的值,已经输出的值不会在改变
    var z =3
    exports.z =z
    var dd = function(z){
        z++;
    }
    exports.dd = dd
    //test.js
    const index = require("./index.js")
    
    console.log("第一次获取的z:"+index.z)   //获取已输出对象值
    index.dd            //调用修改值的方法
    console.log("第二次获取的z:"+index.z)   //再次获取输出值
    

    输出结果:image-20210802163900730

    ES6模块下:

    // index.js
    export let counter = 3;
    export function incCounter() {
      counter++;
    }
    
    // test.js
    import { counter, incCounter } from './index';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    

    由此可看出CommonJs在第一次js脚本运行时对输出的内容进行了缓存,而ES6只是提供一个获取值的接口,去动态取值而不是缓存。

    第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。因此我们在ES6模块下可以只导入模块中的某些方法或者变量,但是不需要全部导入,而CommonJs则会将整个模块脚本给运行完,会将对象内容全部缓存。

3.2 export 和 export default

export导出方式
export+语句声明
export const name = 'coderwhy';
export const age = 18;
export let message = "my name is why";

export function sayHello(name) {
  console.log("Hello " + name);
}
export {}
const name = 'coderwhy';
const age = 18;
let message = "my name is why";

function sayHello(name) {
  console.log("Hello " + name);
}

export {
  name,
  age,
  message,
  sayHello
}
export as 别名
export {
  name as fName,
  age as fAge,
  message as fMessage,
  sayHello as fSayHello
}
export default用法
  • 默认导出export时可以不需要指定名字;
  • 在导入时不需要使用 {},并且可以自己来指定名字;
  • 一个模块只能有一个export default

3.3 import

import引入方式
import ‘ ’
import 'lodash';

会执行引入模块,但是不引入值。

import {<> as <> } from ' '
import { name as wName, age as wAge, sayHello as wSayHello } from './modules/foo.js';

导入时给标识符起别名。

import * as <> from ' '
import * as foo from './modules/foo.js';

console.log(foo.name);
console.log(foo.age);
foo.sayHello("Kobe");

整体导入,并将导入模块功能放在自己定义的模块功能对象

export import 结合

从一个模块中导入的内容如果想直接导出去,那么可以用下面的语句

// foo.js 导入,但是只是做一个中转
export { sum } from './bar.js';

// 接口改名
export { sum as barSum } from './bar.js'; // 甚至在foo.js中导出时,我们可以变化它的名字

// 整体导入和导出
export * from './bar.js';
    // 相当于实现了模块之间的继承。注意,`export *`命令会忽略后面模块的`default`接口。