本文将为你们透彻的介绍关于Node的模块化——CommonJS的一切。html
看完本文能够掌握,如下几个方面:前端
module.exports
是如何导出变量的,值类型和引用类型导出间的差别;module.exports
和exports
又有怎样的区别和联系;PS:本篇文章为「Node.js系列」的第二篇,完全搞懂JS和Node中的模块化机制;
以后会保持每周1~2篇的Node.js文章,欢迎你们和我一块儿学习大前端进阶系列。node
在不少开发的状况下,咱们都知道要使用模块化开发,那为何要使用它呢?webpack
而事实上,模块化开发
最终的目的是将程序划分红一个个小的结构
;web
本身的逻辑代码
,有本身的做用域
,不会影响到其余的结构;变量
、函数
、对象
等导出给其结构使用;变量
、函数
、对象
等;上面说提到的结构
,就是模块
;面试
按照这种结构
划分开发程序的过程,就是模块化开发
的过程;ajax
在网页开发的早期,因为JavaScript仅仅做为一种脚本语言
,只能作一些简单的表单验证或动画实现等,它仍是具备不少的缺陷问题的,好比:算法
但随着前端和JavaScript的快速发展,JavaScript代码变得愈来愈复杂了;编程
先后端开发分离
,意味着后端返回数据后,咱们须要经过JavaScript进行前端页面的渲染
;前端路由
、状态管理
等等一系列复杂的需求须要经过JavaScript来实现;复杂的后端程序
,没有模块化是致命的硬伤;因此,模块化已是JavaScript一个很是迫切的需求:后端
可是JavaScript自己,直到ES6
(2015)才推出了本身的模块化方案;
在此以前,为了让JavaScript支持模块化,涌现出了不少不一样的模块化规范:AMD、CMD、CommonJS
等;
到此,咱们明白了为何要用模块化开发?
那若是没有模块化会带来什么问题呢?
当咱们在公司面对一个大型的前端项目时,一般是多人开发的,会把不一样的业务逻辑分步在多个文件夹当中。
小豪开发的bar.js
文件
var name = "小豪";
console.log("bar.js----", name);
复制代码
小豪开发的baz.js
文件
console.log("baz.js----", name);
复制代码
小红开发的foo.js
文件
var name = "小红";
console.log("foo.js----", name);
复制代码
引用路径以下:
<body>
<script src="./bar.js"></script>
<script src="./foo.js"></script>
<script src="./baz.js"></script>
</body>
复制代码
最后当我去执行的时候,却发现执行结果:
当咱们看到这个结果,有的小伙伴可能就会惊讶,baz.js
文件不是小豪写的么?为何会输出小红的名字呢?
究其缘由,咱们才发现,其实JavaScript
是没有模块化的概念(至少到如今为止尚未用到ES6规范),换句话说就是每一个.js
文件并非一个独立的模块,没有本身的做用域
,因此在.js
文件中定义的变量,都是能够被其余的地方共享的,因此小豪开发的baz.js
里面的name,其实访问的是小红从新声明的。
可是共享也有一点很差就是,项目的其余协做人员也能够随意的改变它们,显然这不是咱们想要的。
因此,随着前端的发展,模块化变得必不可少,那么在早期是如何解决的呢?
在早期,由于函数是有本身的做用域,因此能够采用当即函数调用表达式(IIFE),也就是自执行函数,把要供外界使用的变量做为函数的返回结果。
小豪——bar.js
var moduleBar = (function () {
var name = "小豪";
var age = "18";
console.log("bar.js----", name, age);
return {
name,
age,
};
})();
复制代码
小豪——baz.js
console.log("baz.js----", moduleBar.name);
console.log("baz.js----", moduleBar.age);
复制代码
小红——foo.js
(function () {
var name = "小红";
var age = 20;
console.log("foo.js----", name, age);
})();
复制代码
来看一下,解决以后的输出结果,原调用顺序不变;
可是,这又带来了新的问题:
模块中返回对象的命名
,才能在其余模块使用过程当中正确的使用;杂乱无章
,每一个文件中的代码都须要包裹在一个匿名函数中来编写;没有合适的规范
状况下,每一个人、每一个公司均可能会任意命名、甚至出现模块名称相同的状况;因此如今急需一个统一的规范,来解决这些缺陷问题,就此CommonJS规范
问世了。
CommonJS是一个规范,最初提出来是在浏览器之外的地方使用,而且当时被命名为ServerJS,后来为了体现它的普遍性,修改成CommonJS规范。
Node是CommonJS在服务器端一个具备表明性的实现;
Browserify是CommonJS在浏览器中的一种实现;
webpack打包工具具有对CommonJS的支持和转换;
正是由于Node中对CommonJS
进行了支持和实现,因此它具有如下几个特色;
每个js文件都是一个单独的模块
;CommonJS规范的核心变量
: exports、module.exports、require;模块化
开发;无疑,模块化的核心是导出和导入,Node中对其进行了实现:
对模块中的内容进行导出
;导入其余模块(自定义模块、系统模块、第三方库模块)中的内容
;假设如今有两个文件:
bar.js
const name = "时光屋小豪";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
复制代码
main.js
console.log(name);
console.log(age);
复制代码
执行node main.js以后,会看到
这是由于在当前main.js
模块内,没有发现name
这个变量;
这点与咱们前面看到的明显不一样,由于Node中每一个js文件都是一个单独的模块。
那么若是要在别的文件内访问bar.js
变量
bar.js
须要导出本身想要暴露的变量、函数、对象等等;main.js
从bar.js
引入想用的变量、函数、对象等等;exports是一个对象,咱们能够在这个对象中添加不少个属性,添加的属性会导出。
bar.js
文件导出:
const name = "时光屋小豪";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
复制代码
main.js
文件导入:
const bar = require('./bar');
console.log(bar.name); // 时光屋小豪
console.log(bar.age); // 18
复制代码
其中要注意的点:
bar
变量等于exports
对象;bar = exports
复制代码
因此咱们经过bar.xxx
来使用导出文件内的变量,好比name,age;
require
实际上是一个函数
,返回值是一个对象,值为“导出文件”的exports
对象;
在Node中,有一个特殊的全局对象,其实exports
就是其中之一。
若是在文件内,再也不使用exports.xxx
的形式导出某个变量的话,其实exports
就是一个空对象。
模块之间的引用关系
main.js
中require导入的时候,它会去自动查找特殊的全局对象exports
,而且把require
函数的执行结果赋值给bar
;bar
和exports
指向同一个引用(引用地址相同);exports
上有变量,则会放到bar
对象上,正由于这样咱们才能从bar
上读取想用的变量;为了进一步论证,bar
和exports
是同一个对象:
咱们加入定时器看看
因此综上所述,Node
中实现CommonJS规范
的本质就是对象的引用赋值
(浅拷贝本质)。
把exports
对象的引用赋值bar
对象上。
CommonJS规范的本质就是对象的引用赋值
可是Node中咱们常用module.exports
导出东西,也会遇到这样的面试题:
module.exports
和exports
有什么关系或者区别呢?
require本质就是一个函数,能够帮助咱们引入一个文件(模块)中导入的对象。
require的查找规则nodejs.org/dist/latest…
结论一: 模块在被第一次引入时,模块中的js代码会被运行一次
// aaa.js
const name = 'coderwhy';
console.log("Hello aaa");
setTimeout(() => {
console.log("setTimeout");
}, 1000);
复制代码
// main.js
const aaa = require('./aaa');
复制代码
aaa.js中的代码在引入时会被运行一次
结论二:模块被屡次引入时,会缓存,最终只加载(运行)一次
// main.js
const aaa = require('./aaa');
const bbb = require('./bbb');
复制代码
/// aaa.js
const ccc = require("./ccc");
复制代码
// bbb.js
const ccc = require("./ccc");
复制代码
// ccc.js
console.log('ccc被加载');
复制代码
ccc中的代码只会运行一次。
为何只会加载运行一次呢?
结论三:若是有循环引入,那么加载顺序是什么?
若是出现下面模块的引用关系,那么加载顺序是什么呢?
如下是经过维基百科对CommonJS规范的解析:
CommonJS中是没有module.exports的概念的;
可是为了实现模块的导出,Node中使用的是Module
的类,每个模块都是Module
的一个实例module
;
因此在Node中真正用于导出的其实根本不是exports
,而是module.exports
;
exports
只是module
上的一个对象
可是,为何exports也能够导出呢?
这是由于module
对象的exports
属性是exports
对象的一个引用;
等价于module.exports = exports = main中的bar
(CommonJS内部封装);
联系:module.exports = exports
进一步论证module.exports = exports
// bar.js
const name = "时光屋小豪";
exports.name = name;
setTimeout(() => {
module.exports.name = "哈哈哈";
console.log("bar.js中1s以后", exports.name);
}, 1000);
复制代码
// main.js
const bar = require("./bar");
console.log("main.js", bar.name);
setTimeout((_) => {
console.log("main.js中1s以后", bar.name);
}, 2000);
复制代码
在上面代码中,只要在bar.js
中修改exports
对象里的属性,导出的结果都会变,由于即便真正导出的是 module.exports
,而module.exports
和exports
是都是相同的引用地址,改变了其中一个的属性,另外一个也会跟着改变。
注意:真正导出的模块内容的核心实际上是module.exports,只是为了实现CommonJS的规范,恰好module.exports对exports对象使用的是同一个引用而已
区别:有如下两点
那么若是,代码这样修改了:
module.exports
也就和 exports
没有任何关系了;
exports
怎么改,都不会影响最终的导出结果;由于module.exports = { xxx }
这样的形式,会在堆内存中新开辟出一块内存空间,会生成一个新的对象,用它取代以前的exports
对象的导出
require
导入的对象是新的对象;讲完它们两个的区别,来看下面这两个例子,看看本身是否真正掌握了module.exports
的用法
练习1:导出的变量为值类型
// bar.js
let name = "时光屋小豪";
setTimeout(() => {
name = "123123";
}, 1000);
module.exports = {
name: name,
age: "20",
sayHello: function (name) {
console.log("你好" + name);
},
};
复制代码
// main.js
const bar = require("./bar");
console.log("main.js", bar.name); // main.js 时光屋小豪
setTimeout(() => {
console.log("main.js中2s后", bar.name); // main.js中2s后 时光屋小豪
}, 2000);
复制代码
练习2:导出的变量为引用类型
// bar.js
let info = {
name: "时光屋小豪",
};
setTimeout(() => {
info.name = "123123";
}, 1000);
module.exports = {
info: info,
age: "20",
sayHello: function (name) {
console.log("你好" + name);
},
};
复制代码
// main.js
const bar = require("./bar");
console.log("main.js", bar.info.name); // main.js 时光屋小豪
setTimeout(() => {
console.log("main.js中2s后", bar.info.name); // main.js中2s后 123123
}, 2000);
复制代码
从main.js
输出结果来看,定时器修改的name
变量的结果,并无影响main.js
中导入的结果。
module.exports
的内存里(练1)module.exports
里存放的是info的引用地址,因此由定时器更改的变量,会影响main.js
导入的结果(练2)CommonJS模块加载js文件的过程是运行时加载的,而且是同步的:
const flag = true;
if (flag) {
const foo = require('./foo');
console.log("等require函数执行完毕后,再输出这句代码");
}
复制代码
CommonJS经过module.exports导出的是一个对象:
CommonJS规范的本质就是对象的引用赋值
《JavaScript模块化——ES Module》
在下一篇文章中,
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: