模块化规范简单实现(理解使用和原理)

本文介绍了模块化的概念及其在JavaScript中的重要性,详细讲解了CommonJS和AMD两种模块化规范,包括它们的使用场景、实现原理以及在浏览器环境中的应用。同时,对比了ES6模块与CommonJS的差异,强调了ES6模块的静态加载特性、运行时行为以及严格模式和只读输入变量的特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

a 1. 模块化
模块化就是指将一个复杂的系统分解到多个模块方便编码,但是在js当中是没有模块体系的,在之前我们实现模块化普遍采用的是命名空间的方式,但是通过这种方式来组织代码会造成以下不好的几点:

  • 命名空间冲突,两个库可能会使用同一个名称;
  • 无法合理的管理项目的依赖和版本;
  • 无法方便的控制依赖的加载顺序。

所以出现了几种模块加载方案,比如CommonJSAMD

2. CommonJS

CommonJS是一种使用广泛的JavaScript模块化规范,核心思想是通过require方法来同步的加载依赖的其他模块,通过module.exports导出需要暴露的接口,node的模块化就是通过CommonJS实现的。

用法如下:

// 导入
const moduleA = require('./moduleA');
// 导出
module.exports = moduleA;

下面看一下怎么使用:

// a.js
module.exports = '人生不是一蹴而就';
// b.js
let str = require('./a.js');
console.log(str);  // 人生不是一蹴而就

通过以上的导入导出方法就可以读取另一个文件中的内容,下面通过简单的实现看一下require导入的原理,先大概知道个思想,其实就是将a.js的文件内容引进来了(通过node中的fs文件模块),并且用一个闭包包起来,然后执行这个闭包:

// a.js
module.exports = '人生不是一蹴而就';
// b.js
let fs = require('fs');
function req(moduleName) {
    let content = fs.readFileSync(moduleName, 'utf8');
    // 最后一个参数是函数的内容体 也就是将文件内容用一个闭包包起来
    let fn = new Function('exports', 'module', 'require', '__dirname', '__filename', content+'\n return module.exports');
    let module = {
        exports: {}
    };
    // 执行函数 返回我们需要的结果
    return fn(module.exports, module, req, __dirname, __filename);
}
let str = req('./a.js');  
console.log(str);  // 人生不是一蹴而就

其实相当于我们在req当中构建了一个函数,函数形式如下:

    function(exports, module, require, __dirname, __filename) {
        module.exports = '人生不是一蹴而就';
        return module.exports;
    }

这样就简单的实现了一个req,最后返回结果就可以打印出文件中的内容了。commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

3. AMD

AMD是采用异步的方式去加载依赖的模块,AMD规范主要是为了解决针对浏览器环境的模块化问题,最具有代表性的实现是requirejs,可在不转化代码的情况下直接在浏览器中运行;可以加载多个依赖;代码可以运行在浏览器环境下和node环境下。

用法如下:

// define 声明模块 通过require使用模块
// define中参数分别为moduleName、依赖、处理函数
define('name', [], function() {
    return 'zl';
});

define('age', [], function() {
    return 25;
});
// 使用时可直接require然后打印其他模块的执行结果
require(['name', 'age'], function(name, age) {
    console.log(name, age);
})

那我们现在写成这样define是还没有定义的,下面来实现definerequire

// 工厂对象主要用于之后将模块名和其所对应的工厂函数关联起来,好在require中导出
let factories = {}
// 参数分别为 模块名 依赖 工厂函数
function define(moduleName, dependencies, factory) {
    // 将模块名字和工厂函数关联起来
    factories[moduleName] = factory;
}

function require(mods, callback) {
	// mods -> name age
    let result = mods.map((mod) => {  
        let factory = factories[mod];
        let exports;
        exports = factory();
        return exports;
    });
    callback.apply(null, result);
}

// define 声明模块 通过require使用模块
define('name', [], function() {
    return 'zl';
});

define('age', [], function() {
    return 25;
});

require(['name', 'age'], function(name, age) {
    console.log(name, age);  // zl 25
})

上面是一个简单的实现,我们考虑一种情况,即:

define('name', [], function() {
    return 'zl';
});

define('age', ['name'], function(name) {
    return name+25;
});

require(['age'], function(age) {
    console.log(age);
})

age模块依赖于name模块的执行结果,实际上age模块返回的是zl 25,这个时候就要考虑如果判断是否有依赖其他模块并执行的情况了:

let factories = {}
// 参数分别为 模块名 依赖 工厂函数
function define(moduleName, dependencies, factory) {
    // 将依赖记到factory上
    // 也可以写成点的形式 factory.dependencies = dependencies;
    factory['dependencies'] = dependencies;
    // 将模块名字和工厂函数关联起来
    factories[moduleName] = factory;
}

function require(mods, callback) {
    let result = mods.map((mod) => {  // name age
        let factory = factories[mod];
        let exports;
        // 将依赖取出来 然后递归
        let dependencies = factory['dependencies'];  
        // require(['name'], function(na...me) {})
        require(dependencies, function() {
            exports = factory.apply(null, arguments);
        })
        return exports;
    });
    callback.apply(null, result);
}

define('name', [], function() {
    return 'zl';
});

define('age', ['name'], function(name) {
    return name+25;
});

require(['age'], function(age) {
    console.log(age);  // zl 25
})

require的模块依赖于一个或多个其他的模块时,主要就是一个递归执行依赖组件的工厂函数的过程。

4. ES6模块和 CommonJS 模块有哪些差异?

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

ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。CommonJS 加载的是一个对象,该对象只有在脚本运行完才会生成。

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

也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。如:

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
    name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William

但是当输出的是复杂数据类型时,实际上拷贝的是栈内存中的地址,所以输出的值会发生改变:

// name.js
let name = 'William';
let hobbies = ['coding'];
setTimeout(() => { 
    name = 'Yvette';
    hobbies.push('reading');
}, 300);
module.exports = { name, hobbies };

// index.js
const { name, hobbies } = require('./name');
console.log(name); // William
console.log(hobbies);  // ['coding']
setTimeout(() => {
    console.log(name); // William
    console.log(hobbies); // ['coding', 'reading']
}, 500);

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import ,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette
  • ES6 模块自动采用严格模式,无论模块头部是否写了 "use strict";

  • require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。

  • ES6 模块的输入变量是只读的,不能对其进行重新赋值

  • 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值