JavaScript 模块化 —— 从概念到原理

news2025/1/16 1:50:27

走过路过发现 bug 请指出,拯救一个辣鸡(但很帅)的少年就靠您啦!!!

1. 为什么需要 Javascipt 模块化?

1.解决命名冲突。将所有变量都挂载在到全局 global 会引用命名冲突的问题。模块化可以把变量封装在模块内部。
2.解决依赖管理。Javascipt 文件如果存在相互依赖的情况就需要保证被依赖的文件先被加载。使用模块化则无需考虑文件加载顺序。
3.按需加载。如果引用 Javascipt 文件较多,同时加载会花费加多时间。使用模块化可以在文件被依赖的时候被加载,而不是进入页面统一加载。
4.代码封装。将相同功能代码封装起来方便后续维护和复用。

2. 你知道哪几种模块化规范?

CommonJS

Node.js 采用了 CommonJS 模块规范。

CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。使用 require 方法加载模块。模块加载的顺序,按照其在代码中出现的顺序。

模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。

引入模块得到的值其实是模块输出值的拷贝,如果是复杂对象则为浅拷贝。

// a.js
let count = 1;
function inc() {count++;
}
module.exports = {count: count,inc: inc
};
// b.js
const a = require('./a.js');

console.log(a.count); // 1
a.inc();
console.log(a.count); // 1 

因为 CommonJS 输出的是值的浅拷贝,也就是说 count 在输出后就不再和原模块的 count 有关联。

在 Node 中每一个模块都是一个对象,其有一个 exports 属性,就是文件中指定的 module.exports,当我们通过 require 获取模块时,得到的就是 exports 属性。再看另一个例子:

// a.js
module.exports = 123;

setTimeout(() => {module.exports = 456;
}, 1000);

// b.js
console.log(require('./a.js')); // 123

setTimeout(() => {console.log(require('./a.js')); // 456
}, 2000); 

模块的 module.exports 值改变了,我们通过 require 获取模块的值也会发生变化。

CommonJS 使用了同步加载,即加载完成后才进行后面的操作,所以比较适合服务端,如果用在浏览器则可能导致页面假死。

AMD

AMD(Asynchronous Module Definition,异步加载模块定义)。这里异步指的是不堵塞浏览器其他任务(dom构建,css渲染等),而加载内部是同步的(加载完模块后立即执行回调)。 AMD 也采用 require 命令加载模块,但是不同于 CommonJS,它要求两个参数,依赖模块和回调:

require([module], callback); 

以 RequireJS 示例, 具体语法可以参考 requirejs.org/

简单提供一下代码示例,方便后续理解。

定义两个模块 calclog 模块

// calc.js
define(function(require, factory) {function add(...args) {return args.reduce((prev, curr) => prev + curr, 0);}return {add}
});
// log.js
define(function(require, factory) {function log(...args) {console.log('---log.js---');console.log(...args)}return log
}); 

index.js 中引用两个模块

require(['./calc.js', './log.js'], function (calc, log) {log(calc.add(1,2,3,4,5));
}); 

在 HTML 中引用

<script src="./require.js"></script>
<script src="./index.js"></script> 

可以看到在被依赖模块加载完成后会把返回值作为依赖模块的参数传入,在被加载模块全部执行完成后可以去执行加载模块。

UMD

UMD(Universal Module Definition,通用模块定义),所谓的通用,就是兼容了 CommonJS 和 AMD 规范,这意味着无论是在 CommonJS 规范的项目中,还是 AMD 规范的项目中,都可以直接引用 UMD 规范的模块使用。

原理其实就是在模块中去判断全局是否存在 exportsdefine,如果存在 exports,那么以 CommonJS 的方式暴露模块,如果存在 define 那么以 AMD 的方式暴露模块:

(function (root, factory) {if (typeof define === "function" && define.amd) {define(["jquery", "underscore"], factory);} else if (typeof exports === "object") {module.exports = factory(require("jquery"), require("underscore"));} else {root.Requester = factory(root.$, root._);}
}(this, function ($, _) {// this is where I defined my module implementationconst Requester = { // ... };return Requester;
})); 

ESM (ES6 模块)

CommonJS 和 AMD 模块,都只能在运行时确定输入输出,而 ES6 模块是在编译时就能确定模块的输入输出,模块的依赖关系。

在 Node.js 中使用 ES6 模块需要在 package.json 中指定 {"type": "module"}

在浏览器环境使用 ES6 模块需要指定 <script type="module" src="module.js"></script>

ES6 模块通过 importexport 进行导入导出。ES6 模块中 import 的值是原始值的动态只读引用,即原始值发生变化,引用值也会变化。

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

// a.js
export const obj = {a: 5
}
// b.js
console.log(obj)
import { obj } from './a.js'

// 运行 b.js 输出: { a: 5 } 

importexport 指定必须处理模块顶层,也就是说不能在 iffor 等语句内。下面这种使用方式是不合法的。

if (expr) {import val from 'some_module'; // error!
} 

UMD 通常是在 ESM 不起作用情况下备用,未来趋势是浏览器和服务器都会支持 ESM。

由于 ES6 模块是在编译阶段执行的,可以更好的在编译阶段进行代码优化,如 Tree Shaking 就是依赖 ES6 模块去静态分析代码而删除无用代码。

3. CommonJS 和 ES6 模块的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。* CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。* CommonJs 是单个值导出,ES6 Module可以导出多个* CommonJs 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层* CommonJsthis 是当前模块,ES6 Modulethisundefined4. CommonJS 和 AMD 实现原理

CommonJS

我们通过写一个简单的 demo 实现 CommonJS 来理解其原理。

1、实现文件的加载和执行

我们在用 Node.js 时都知道有几个变量和函数是不需要引入可以直接使用的,就是 require()__filename__dirnameexportsmodule。这些变量都是 Node.js 在执行文件时注入进去的。

举个栗子,我们创建一个 add.js 文件,导出一个 add() 函数:

function add(a, b) {return a + b;
}

module.exports = add; 

现在我们要加载并执行这个文件,我们可以通过 fs.readFileSync 加载文件。

const fs = require("fs");

// 同步读取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件内容 

我们要在执行时传入 require()__filename__dirnameexportsmodule 这几个参数,可以在一个函数中执行这段代码,而函数的参数就是这几个参数即可。我们简单的创建一个函数,函数的内容就是刚才我们加载的文件内容,参数名依次是规范要求注入的几个参数。

// 通过 new Function 生成函数,参数分别是函数的入参和函数的内容
const compiledWrapper = new Function("exports","require","module","__filename","__dirname",data
); 

现在我们执行这个函数,先不考虑 require__filename__dirname,只传 exportsmodule

const mymodule = {};

const myexports = (mymodule.exports = {});
// 执行函数并传入 module 和 export
compiledWrapper.call(myexports, null, myexports, mymodule, null, null); 

现在我们可以简单的了解导出变量的原理,我们把 module 传给函数,在函数中,把需要导出的内容挂在 module 上,我们就可以通过 module 获取导出内容了。

exports 只是 module.exports 的一个引用,我们可以给 module.exports 赋值,也可以通过 exports.xxx 形式赋值,这样也相当于给 module.exports.xxx 赋值。但是如果直接给 exports 赋值将不生效,因为这样 exports 就和 module 没关系了,我们本质上还是要把导出结果赋值给 module.exports

现在的完整代码贴一下:

const fs = require("fs");

// 同步读取文件
const data = fs.readFileSync("./add.js", "utf8"); // 文件内容
// 创建函数
const compiledWrapper = new Function("exports","require","module","__filename","__dirname",data
);

const mymodule = {};
const myexports = (mymodule.exports = {});
// 执行函数并传入 module 和 export
compiledWrapper.call(myexports, null, myexports, mymodule, null, null);

console.log(mymodule, myexports, mymodule.exports(1, 2));
// { exports: [Function: add] } {} 3 

我们可以获取了 add 函数,并成功调用。

2、引用文件

我们刚才已经成功加载并执行了文件,如何在另一个文件通过 require 引用呢。其实就是把上面的操作封装一下。

不过现在我们把参数全部传进去,require__filename__dirname,分别是我们当前实现的 require 函数,加载文件的文件路径,加载文件的目录路径。

const fs = require('fs');
const path = require('path');

function _require(filename) {// 同步读取文件const data = fs.readFileSync(filename, 'utf8'); // 文件内容const compiledWrapper = new Function('exports','require','module','__filename','__dirname',data);const mymodule = {};const myexports = (mymodule.exports = {});const _filename = path.resolve(filename)const _dirname = path.dirname(_filename);compiledWrapper.call(myexports, _require, myexports, mymodule, _filename, _dirname);return mymodule.exports
}

const add = _require('./add.js')

console.log(add(12, 13)); // 25 

3、模块缓存

现在就实现了文件的加载和引用,现在还差一点,就是缓存。之前说过,一个模块只会加载一次,然后在全局缓存起来,所以需要在全局保存缓存对象。

// add.js
console.log('[add.js] 加载文件....')
function add(a, b) {return a + b;
}
module.exports = add;

// require.js
const fs = require('fs');
const path = require('path');

// 把缓存对象原型设置为null 防止通过原型链查到同名的key (比如一个模块叫 toString
const _cache = Object.create(null);

function _require(filename) {const cachedModule = _cache[filename];if (cachedModule) {// 如果存在缓存就直接返回return cachedModule.exports;}// 同步读取文件const data = fs.readFileSync(filename, 'utf8'); // 文件内容const compiledWrapper = new Function('exports','require','module','__filename','__dirname',data);const mymodule = {};const myexports = (mymodule.exports = {});const _filename = path.resolve(filename);const _dirname = path.dirname(_filename);compiledWrapper.call(myexports,_require,myexports,mymodule,_filename,_dirname);_cache[filename] = mymodule;return mymodule.exports;
}

const add1 = _require('./add.js');
const add2 = _require('./add.js');

console.log(add1(12, 13)); // [add.js] 加载文件.... 25
console.log(add2(13, 14)); // 27 

可以看到加了缓存后,引用了两次模块,但只加载了一次。

一个简单的 CommonJS 规范实现就完成了。

AMD

上面提供了 RequireJS 的示例代码,打开控制台可以发现 HTML 中被添加了两个 <script> 标签,引入了程序中依赖的两个文件。

这样我们可以推测 RequireJS 的实现原理,就是在执行程序的过程中,发现依赖文件未被引用,就在 HTML 中插入一个 <script> 节点引入文件。

这里涉及一个知识点,我们可以看到被 RequireJS 插入的标签都设置了 async 属性。

  • 如果我们直接使用 script 脚本的话,HTML 会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的 DOM 渲染。
  • 如果设置了 async,脚本会异步加载,并在加载完成后立即执行。
  • 如果设置了 defer,浏览器会异步的下载文件并且不会影响到后续 DOM 的渲染,在文档渲染完毕后,DOMContentLoaded 事件调用前执行,按照顺序执行所有脚本。

所以我们可以推测 RequireJS 原理,通过引入 <script> 标签异步加载依赖文件,等依赖文件全部加载完成,把文件的输入作为参数传入依赖文件。

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/145317.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能与python

人工智能的话题在近几年可谓是相当火热&#xff0c;前几天看快本时其中有一个环节就是关于人工智能的&#xff0c;智能家电、智能机器人、智能工具等等&#xff0c;在我的印象里&#xff0c;提到人工智能就会出现 Python&#xff0c;然后我便在网上查找了相关信息&#xff0c;并…

(第三章)OpenGL超级宝典学习:认识渲染管线

OpGL超级宝典学习&#xff1a;认识渲染管线 前言 本章作为OpenGL学习的第三章节 在本章节我们将认识OpenGL的渲染管线 对管线内各个过程有一个初步的认识 ★提高阅读体验★ &#x1f449; ♠一级标题 &#x1f448; &#x1f449; ♥二级标题 &#x1f448; &#x1…

【KG】TransE 及其实现

原文&#xff1a;https://yubincloud.github.io/notebook/pages/paper/kg/TransE/ TransE 及其实现 1. What is TransE? TransE (Translating Embedding), an energy-based model for learning low-dimensional embeddings of entities. 核心思想&#xff1a;将 relationship …

基于R的Bilibili视频数据建模及分析——建模-因子分析篇

基于R的Bilibili视频数据建模及分析——建模-因子分析篇 文章目录基于R的Bilibili视频数据建模及分析——建模-因子分析篇0、写在前面1、数据分析1.1 建模-因子分析1.2 对数线性模型1.3 主成分分析1.4 因子分析1.5 多维标度法2、参考资料0、写在前面 实验环境 Python版本&#…

防火墙命令

启动&#xff1a; systemctl start firewalld 查看状态&#xff1a; systemctl status firewalld 停止&#xff1a;systemctl stop firewalld 禁用&#xff1a;systemctl disable firewalld 怎么开启一个端口呢 添加 firewall-cmd --zonepublic --add-port80/tcp --permanent …

easyx保姆级教程---->从游戏玩家到游戏制作者

请点击这里&#xff1a;安装教程 1.头文件 #include<easyx.h> //这个是只包含最新的API(函数接口) #include<graphics.h> //这个头文件包含了上面的&#xff0c;还包含了已经不推荐使用的函数2.窗口 1.初始化绘制窗口 initgraph(width,height,flag); //窗…

Domino Web应用中的搜索功能和结果选择问题

大家好&#xff0c;才是真的好。 还有不到十天Domino多瑙河版本就将发布&#xff0c;在此之前&#xff0c;我们还是讲述一下Web中的搜索技术。 废话不多说&#xff0c;我们直接上干货。 Notes应用的视图在Web浏览器中可以直接展现&#xff0c;并且可选择。 如果这样展现的话…

【QGIS入门实战精品教程】8.1:QGIS制作地图案例教程

文章目录 一、加载矢量数据二、加载影像底图三、美化矢量数据四、切换到排版视图五、添加经纬度格网六、添加其他修饰元素七、地图输出一、加载矢量数据 加载本实验数据基础数据.gpkg中的甘肃省政区矢量数据,如下所示: 二、加载影像底图 QGIS加载在线地图案例教程参考: 【…

5、Java中的JDBCJDBCUtilsJDBC控制事务getResource中文或有空格路径处理ResourceBundle演示

JDBC&#xff1a; 1. 概念&#xff1a;Java DataBase Connectivity Java 数据库连接&#xff0c; Java语言操作数据库 * JDBC本质&#xff1a;其实是官方&#xff08;sun公司&#xff09;定义的一套操作所有关系型数据库的规则&#xff0c;即接口。各个数据库厂商去实现这…

回收租赁商城系统功能拆解04讲-商品品牌

回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 可以快速帮助企业搭建类似闲鱼回收/爱回收/爱租机/人人租等回收租赁商城。 回收租赁系统支持智能评估回收价格&#xff0c;后台调整最终回收价&#xff0c;用户同意回收后系统即刻放款&#xff0c;用户微信零…

第04章 程序控制结构

在程序中&#xff0c;程序运行的流程控制决定程序是如何执行的。 顺序控制 介绍&#xff1a; 程序从上到下的逐行的执行&#xff0c;中间没有任何判断和跳转。 使用&#xff1a;java中定义变量时&#xff0c;采用合法的前向引用。如&#xff1a; public class Test{int num…

【虚幻引擎】UE4/UE5像素流在广域网上(云)部署(多实例)

一、选择云服务器 每个云平台都提供许多预设的镜像选择&#xff0c;由于像素流技术目前只支持Windows操作系统&#xff0c;所以我们需要选择Windows Server的镜像&#xff0c;2012/2016/2019皆可。我们这里选择了Windows Server 2016 R2 简体中文版的镜像&#xff0c;之所以选择…

【SSM整合】对Spring、SpringMVC、MyBatis的整合,以及Bootstrap的使用,简单的新闻管理系统

✅作者简介&#xff1a;热爱Java后端开发的一名学习者&#xff0c;大家可以跟我一起讨论各种问题喔。 &#x1f34e;个人主页&#xff1a;Hhzzy99 &#x1f34a;个人信条&#xff1a;坚持就是胜利&#xff01; &#x1f49e;当前专栏&#xff1a;【Spring】 &#x1f96d;本文内…

代码随想录第53天|● 1143.最长公共子序列 ● 1035.不相交的线 ● 53. 最大子序和 动态规划

1143.最长公共子序列 和718.最长重复子数组类似 包括二维数组初始化这些 不同之处在于递推公式主要就是两大情况&#xff1a; text1[i - 1] 与 text2[j - 1]相同&#xff0c;text1[i - 1] 与 text2[j - 1]不相同 如果text1[i - 1] 与 text2[j - 1]相同&#xff0c;那么找到了…

Windows/Linux日志分析

Windows日志分析 Windows系统日志是记录系统中硬件、软件和系统问题的信息&#xff0c;同时还可以监视系统中发生的事件。用户可以通过它来检查错误发生的原因&#xff0c;或者寻找受到攻击时攻击者留下的痕迹。 Windows主要有以下三类日志记录系统事件&#xff1a;应用程序日志…

【链表】leetcode707.设计链表(C/C++/Java/Js)

leetcode707.设计链表1 题目2 思路3 代码3.1 C版本3.2 C版本3.3 Java版本3.3.1 单链表3.3.2 双链表3.4 JavaScript版本4 总结1 题目 题源链接 设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性&#xff1a;val 和 next。val 是当前节点的值&…

2022年地图产业研究报告

第一章 行业概况 地图是按照一定法则&#xff0c;有选择地以二维或多维形式与手段在平面或球面上表示地球&#xff08;或其它星球&#xff09;若干现象的图形或图像&#xff0c;它具有严格的数学基础、符号系统、文字注记&#xff0c;并能用地图概括原则&#xff0c;科学地反映…

canvasjs javascript-charts 3.7.3 Crack

canvasjs javascript-charts/ 3.7.3 具有 30 多种图表类型的 JavaScript 图表库 具有 10 倍性能和 30 多种图表类型的 JavaScript 图表和图形库。核心 JavaScript 图表库是独立的&#xff0c;但也带有流行框架的组件&#xff0c;如 React、Angular、Vue 等。图表响应迅速&#…

14、RH850 F1 RAM存储器介绍

前言: RAM——程序运行中数据的随机存取&#xff08;掉电后数据消失&#xff09;整个程序中&#xff0c;所用到的需要被改写的量&#xff0c;都存储在RAM中&#xff0c;“被改变的量”包括全局变量、局部变量、堆栈段&#xff0c;此专栏会有针对SPI的工作原理的详细介绍。 一、…

性能优化系列之如何选择合适的WebView内核?

文章の目录一、iOS UIWebView1、优点2、不足二、iOS WKWebView1、优势2、不足三、Android WebKit 和 Chromium四、Android 第三方1、X5 内核五、选型建议写在最后一、iOS UIWebView 1、优点 从 iOS 2 开始就作为 App 内展示 Web 内容的容器排版布局能力强 2、不足 内存泄露…