模块这块的东西确实该好好理一理,有的知识点总结了以后就不用看了,加油鸭!写了我2天-_-
本文解答这些重点问题:
1.什么是编译时加载什么是运行时加载
2.为什么import要放到顶部
3.打包之后变成了什么 import
4.CMD commonjs module的使用与区别
5.commonjs中使用export和module.exports的区别
1.模块是什么?以及作用
将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来
模块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
作用:
1.避免命名冲突(减少命名空间污染)
2.灵活架构,焦点分离,方便模块间组合、分解
3.多人协作互不干扰
4.高复用性和可维护性
2.编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
2.1分词/词法分析(Tokenizing/Lexing)
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序 var a = 2; 。这段程序通常会被分解成为下面这些词法单元: var 、 a、 = 、 2、; 。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义
2.2解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下来是一个叫作 Identifier (它的值是 a )的子节点,以及一个叫作 AssignmentExpression的子节点。 AssignmentExpression 节点有一个叫作 NumericLiteral (它的值是 2 )的子节点
2.3代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
2.4js与其他语言编译上的区别
比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它
以上总结:
在js代码执行之前会经历编译阶段(分词/词法分析->解析/语法分析->代码生成),为什么执行之前要编译?因为js语言是高级程序设计语言,容易阅读与编写,而目标语言是机器语言,即二进制代码,能够被计算机直接识别。
3.AMD(require.js)-浏览器
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
3.1 AMD的使用
目录结构
require.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./require.js" data-main="./main.js" defer async="true" ></script>
<!-- 引入require.js包,引入main.js -->
</body>
</html>
复制代码
main.js
console.log(require, 'main.js开始执行')
require.config({
baseUrl: "./common/", // 提取公共路径
paths: {
"module1": "module1",
"module2": "module2",
}
})
require(['module1', 'module2'], function (module1, module2){
// some code here
console.log(module1.add(5, 6), 'module1')
console.log(module2.addUnit(5,6),'module2')
});
复制代码
module1.js
console.log('module1')
define(function (){
const add = (x,y) => x+y
return {
add
};
});
复制代码
module2.js
console.log('module2')
define(['./module1'],function (module){
const addUnit = (x,y) => `${module.add(x,y)}元` // module2依赖于module1
return {
addUnit
};
});
复制代码
require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。
具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
AMD为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
3.2AMD解决了什么问题
1.在没有AMD的时候,我们用script的src标签引入大量的js,项目越大,引入的js越多,很丑陋,可以说AMD的使用使页面更加美观
2.上面这个案例,module2依赖于module1,那么在require(['module1', 'module2']的时候,我们应该先加载module1再加载module2,这样可以顺序加载
3.defer async="true"可以让js异步加载,避免页面script加载多个js失去响应
4.加载完了直接回调,这样能保证模块已经引入再操作
5.require()实现按需加载,有个功能只有一个页面(单页面某个组件)引入一个库,你不用按需加载就相当于每个页面都要加载这个库
4.CommonJS - 服务端(node.js)
4.1CommonJS的特点
1.CommonJS用于服务端
2.Node.js 模块系统中,每个文件都被视为独立的模块。模块中包括CommonJS规范的核心变量:exports、module.exports、require
3.块化的核心是导出与导入,在Node中通过exports与module.exports负责对模块中的内容进行导出,通过require函数导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
4.2CommonJS基本使用
文件目录
运行node文件只需要cd到同级目录 node+文件名就好
node1.js
const res = require('./node2.js');
console.log(res.obj);
复制代码
node2.js
exports.obj = {
text1: '欢迎关注公众号',
text2: '[程序媛爱唠嗑]',
}
复制代码
以上是exports + require()使用,下面是module.exports + require()使用
node1.js
const res = require('./node2.js');
console.log(res(5))
复制代码
node2.js
module.exports = (a) => a + 1
复制代码
4.3 exports 与 module.exports有什么区别,使用场景
对于这块,很多人瞎写还那么多赞,真的是怕了!所以我的理解如下,有错误望指正!!!
对
module.exports
的引用,其输入更短,它允许一个快捷方式,以便module.exports.f = ...
可以更简洁地写成exports.f = ...
。 但是,请注意,与任何变量一样,如果将新值分配给exports
,则它就不再绑定到module.exports
module.exports.hello = true; // 从模块的 require 中导出
exports = { hello: false }; // 未导出,仅在模块中可用
上面是官网的解释,其实就是exports是对 module.exports
的引用,看下面的:
const module = {
exports: {}
}
const exports = module.exports
exports.name = '欢迎关注程序媛爱唠嗑' // 虽然是对exports进行赋值,但是调用的仍然是module.exports
console.log(module.exports, '例子1'); // 这种情况下,是成立的
复制代码
const module = {
exports: {}
}
const exports = module.exports
exports = '欢迎关注程序媛爱唠嗑' // 虽然是对exports进行赋值,但是调用的任然是module.exports
console.log(module.exports, '例子2'); // 这种情况下,exports已经不在对其引用,所以不行
复制代码
所以总结下:
module.exports在任何的场景下都可用,因为最后还是对module.exports的调用,exports在不用重新赋值的时候用,也就是说module.exports=某某某的时候只能用module.exports,因为用exports相当于切断的引用,只要记住了区别,为了简单,大部分场景还是会能够简写就简写用exports
4.4 CommonJs的特点
1.所有代码都运行在模块作用域,不会污染全局作用域;
2.模块是同步加载的,即只有加载完成,才能执行后面的操作;
3.CommonJS输出是值的拷贝(即,require
返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。
看例子:
node0.js
const res = require('./node1.js');
console.log(res(4, 5))
console.log('node0')
复制代码
node1.js
const res = require('./node2.js');
module.exports = (x,y) => `${res(x, y)}元`
console.log('node1')
复制代码
node2.js
module.exports = (x,y) => x + y
console.log('node2')
复制代码
node0中有res,node1中有res,并没有影响,说明所有代码都运行在模块作用域,不会污染全局作用域
图中的打印,按照引入的顺序,这也就验证了CommonJS
中,模块是同步加载的,即只有加载完成,才能执行后面的操作
第三点CommonJS输出是值的拷贝,也就是说你用require()
引入了模块,但是你在最新的模块中怎样去改变,也不会影响你已经require()
的模块。见6.1
5. ES6 module ---(浏览器/服务端)
5.1 如何使用
export使用
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { text1, text2 } from './module1.js'
console.log(text1, '-', text2());
// import * as module from './module1.js' 也可以全部引入
// console.log(module.text1, '-', module.text2());
</script>
</body>
</html>
复制代码
module1.js
export const text1 = '欢迎关注公众号';
export const text2 = () => '[程序媛爱唠嗑]';
复制代码
这里要注意得是: 直接运行的话会出现跨域问题,比如script标签的src就没有跨域问题,所以我们要保证同源,在vscode安装live server,然后运行
2.export default
module2.js
export default function test() {
console.log('欢迎关注我的公众号-[程序媛爱唠嗑]');
}
复制代码
index.html
<script type="module">
import test from './module2.js'
test()
</script>
复制代码
5.2 export与export default的区别
1.
import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载, 用到export default
命令,为模块指定默认输出
2.页面可有多个export但是只能有一个export default
6.CommonJS,AMD,ES6中的Module的区别
CommonJS 模块的`require()`是同步加载模块,ES6 模块的`import`命令是异步加载,
有一个独立的模块依赖的解析阶段。AMD是异步加载
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
CommonJS 模块, AMD是运行时加载,ES6 模块是编译时输出接口。
复制代码
6.1 CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
其实这里有很多人搞不清楚,我也一样,但是经过一些验证,我理解的是复制一个值的时候,如果是引用数据类型,我们复制的其实是他的内存地址(也就是指针),但是本质上都是叫做值得拷贝,而不是指针的复制,比如字符串没有指针,复制都是复制值,只是看值代表的是什么.
看例子:
node1.js
var mod = require('./node2');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
复制代码
node2.js
var counter = 3
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
复制代码
上面我引入进来的值就是一个字符串,赋值之后我再对字符串操作是不生效的,这里的值就是一个字符串
等同于:
let a = 1
let b = a
++a
console.log(b); // 1
复制代码
如果是对象呢? node1.js
var mod = require('./node2');
console.log(mod.counter); // { count: 3 }
mod.incCounter();
console.log(mod.counter); // { count: 4 }
复制代码
node2.js
var counter = {
count: 3
}
function incCounter() {
counter.count++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
复制代码
上面我引入进来的值就是一个对象(内存地址),赋值之后我再对对象操作是生效的,这里的值就是一个内存地址
而在ES5module中, module1.js
export let counter = 3;
export function incCounter() {
counter++;
}
复制代码
index.html
<script type="module">
import { counter, incCounter } from './module1.js';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
</script>
复制代码
JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
我的理解就是CommonJS输出的值是什么就是对对应值的操作,而es6中的module不管是什么值(引用值还是字符串),都是形成了以key为路径,value为值(不管这个值是字符串还是对象)得map对象,始终输出得是引用,然后在堆中始终指向同一个东西。
具体请查看: 你的 import 被 webpack 编译成了什么?
我也试图查看了webpack编译得源码,感觉自己还没有到解析源码得功力~不过这张图这可以说明点什么.
针对于import的这个特性,我就需要过一个bug,当然是我的锅,当时我在一个js模块中定义了一个数组(我希望初始化是不变的,当前组件的引用改变了模块中的数据,组件切换,数据一直不是初始状态,因为页面并没有重载,我用工厂函数解决了这个问题
回放:
index.html
<script type="module">
import { imgs } from './module1.js'
console.log(imgs);
</script>
复制代码
module1.js
export const imgs = [
{
img: 'xxx',
isShow: false,
},
{
img: 'xxx',
isShow: false,
},
{
img: 'xxx',
isShow: false,
},
]
复制代码
如果你在html中对imgs有修改,是会影响module1的,所以为了避免JSON.parse(JSON.stringify(xx))的使用,我在输出时使用工厂函数返回 index.html
<script type="module">
import { imgs } from './module1.js'
console.log(imgs());
</script>
复制代码
module1.js
export const imgs = () => {
return [
{
img: 'xxx',
isShow: false,
},
{
img: 'xxx',
isShow: false,
},
{
img: 'xxx',
isShow: false,
},
]
}
复制代码
6.2 验证import是编译时加载,CommonJS 模块是运行时加载 (面试的时候我被考到过)
从上面2我们已经知道了,在执行代码之前会有一个编译的阶段,也就是要把我们代码解析成计算机认识的语言,CommonJS 模块, AMD是运行时加载,引擎处理import
语句是在编译时,如何验证,ES6解决了什么问题
这里先测试ES6 模块
module1.js
export default function test() {
console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-小橘子');
}
复制代码
module2.js
export default function test() {
console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-大橘子');
}
复制代码
index.html
<script type="module">
const box = 1
if(box === 1) {
import test from './module1.js'
} else {
import test from './module2.js'
}
test()
</script>
复制代码
报错了,对import进行编译,因为它们用到了表达式、变量和if
结构。在静态分析阶段(编译阶段),这些语法都是没法得到值的.所以证明了引擎处理import
语句是在编译时
看看CommonJS
node2.js
export default function test() {
console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-大橘子');
}
复制代码
node1.js
export default function test() {
console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-小橘子');
}
复制代码
node0.js
const box = 1
let res
if (box === 1) {
res = require('./node1.js');
} else {
res = require('./node2.js');
}
console.log(res());
复制代码
证明了 CommonJS实在运行时加载
6.3 为什么要把import提到最前面
因为在引擎处理import
语句是在编译时,这时不会去分析或执行if
语句,所以import
语句放在if
代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,import
和export
命令只能在模块的顶层,不能在代码块之中(比如,在if
代码块之中,或在函数之中)。
大白话理解就是假设一个羽毛球桶里面有很多五颜六色羽毛球,必须先把蓝色先挑出来(编译),再考虑下一步操作(执行)是不是在你放羽毛球的时候把蓝色的放上面,这样就能提高效率了~我的理解哈
6.4 引擎处理import
语句是在编译时有什么好处?
ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。
所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。
这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。
CommonJS从6.1的例子可以看出,我在运行时,可以去决定引入什么样的模块,所以动态的引入没法去做静态分析,而ES6中的module在编译阶段就确定了,可以进行分析.
7.参考与引用
require.js了解一下
ES6
你不知道的javscript上册
如有遗漏,我可以补上
8.个人公众号
最近开始写的一个公众号,主要是在前端行业发生的一些事,还有生活里温暖的小故事,打工人不易,需要去感受生活,才能热爱生活,欢迎大家关注我,关关关关关关关关关关关关关关关关关注我,谢谢谢谢!是公众号不是要你关注我掘金帐号,你想也行!