当前位置: 首页 > 后端技术 > Node.js

CommonJS规范与ES6Module

时间:2023-04-03 12:18:57 Node.js

刚开始接触模块规范的时候,脑子里还没有这个概念。没看懂模块是什么,模块导入导出的原理。更傻的是什么时候require什么时候import,还有CommonJS规范module.exports和exports的区别,ES6模块exportdefault和export的区别。最近研究了一下,记录下自己的理解!如有不妥请及时指出!CommonJS规范的module.exports属性Node内部提供了一个Module构造函数,所有的modules都是Module的实例。CommonJS规范规定,在每个模块内部,模块变量代表当前模块。这个变量是Module的一个实例,它的exports属性(即module.exports)是一个外部接口。加载模块实际上是加载模块的module.exports属性。varx=5;varaddX=function(value){returnvalue+x;};module.exports.x=x;module.exports.addX=addX;上面的代码通过module.exports输出变量x和函数addX。require方法用于加载模块:varexample=require('./example.js');控制台日志(例子.x);//5console.log(example.addX(1));//6module.exports属性表示当前模块的对外输出接口。当其他文件加载模块时,它实际上读取module.exports变量。exports变量为了方便起见,Node为每个模块提供了一个exports变量,指向module.exports。这相当于有一行varexports=module.exports;在每个模块的头部。我的理解是这样的://module.exports默认是一个对象letdefaultVal=Object.create(null);module.exports=defaultVal;//最初exports指向module.exportsletexports=module.exports;//所以我们可以通过下面的方式给exports对象添加方法来扩展导出的值!exports.area=function(r){returnMath.PI*r*r;};exports.circumference=function(r){return2*Math.PI*r;};模块导出的值可以不是对象,可以是任何类型的值!//1、导出无效,导出默认空对象defaultValexports=function(x){console.log(x)};//2、exports.hello无效,导出字符串“Helloworld”exports.hello=function(){return'hello';};module.exports='Helloworld';//3、exports.text无效,导出对象objexports.text="Helloworld";constobj={info:"我还是以前的那个男孩!"}module.exports=obj;//4,exports.text有效,exportobject{text:"Helloworld",info:"IamstilltheboyIusedtobe"}exports.text="Helloworld"module.exports.info="I'mstilltheboyIusedtobe!"//5,有效,导出函数function(x){console.log(x)}module.exports=function(x){console.log(X)};上面前四个通过exports导出的值都是无效的。为什么?因为模块导出的是module.exports而不是exports,前三种情况切断了exports和module.exports的联系!也就是说exports===module.exports为false,不再指向同一个值,我们只会对外导出module.exports,这句话很重要!第四种情况,exports和module.exports还是指向同一个值,都指向默认对象!为了防止出错,最简单的处理方式就是放弃使用exports,只使用module.exports!require命令Node使用CommonJS模块规范,内置的require命令用于加载模块文件。require命令的基本功能是读取并执行一个JavaScript文件,然后返回模块的exports对象。如果没有找到指定的模块,就会报错。加载规则require命令用于加载文件,后缀默认为.js。varfoo=require('foo');//等价于varfoo=require('foo.js');require命令根据参数格式的不同,在不同的路径下查找模块文件。如果参数字符串以“/”开头,则表示加载位于绝对路径下的模块文件。例如,require('/home/marco/foo.js')将加载/home/marco/foo.js。如果参数字符串以“./”开头,则表示加载位于相对路径(相对于当前执行脚本的位置)的模块文件。例如,require('./circle')将在与当前脚本相同的目录中加载circle.js。如果参数字符串不是以“./”或“/”开头,则表示加载默认提供的核心模块(位于Node的系统安装目录),或者安装在所有模块的node_modules目录下的模块级别(全局安装或部分安装)。例如脚本/home/user/projects/foo.js执行require('bar.js')命令,Node会依次搜索以下文件。/usr/local/lib/node/bar.js/home/user/projects/node_modules/bar.js/home/user/node_modules/bar.js/home/node_modules/bar.js/node_modules/bar.js被设计像这样的目的是让不同的模块能够本地化它们所依赖的模块。如果参数字符串不是以“./”或“/”开头,而是一个路径,比如require('example-module/path/to/file'),会先找到example-module的位置,然后它作为参数,找到后续路径。如果没有找到指定的模块文件,Node会尝试在文件名中添加.js,.json,.node后再搜索。.js文件将被解析为文本格式的JavaScript脚本文件,.json文件将被解析为JSON格式的文本文件,.node文件将被解析为编译后的二进制文件。如果您想获得require命令加载的确切文件名,请使用require.resolve()方法。目录加载规则通常,我们会将相关文件放在一个目录中,以便于组织。这时候最好给目录设置一个入口文件,这样require方法就可以通过这个入口文件加载整个目录。在该目录下放置一个package.json文件,将入口文件写入main字段。下面是一个例子。//package.json{"name":"some-library","main":"./lib/some-library.js"}require发现参数字符串指向一个目录,会自动检查包在那个目录。json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本没有package.json文件,则会加载该目录下的index.js文件或index.node文件。模块加载机制CommonJS模块加载机制是输入是输出值的副本。也就是说,一旦输出了一个值,模块内部的变化就不会影响这个值。看看下面的例子。下面是一个模块文件lib.js。//lib.jsvarcounter=3;functionincCounter(){counter++;}module.exports={counter:counter,incCounter:incCounter,};上面的代码输出了内部变量counter和重写这个变量的内部方法incCounter。然后,加载上面的模块。//main.jsvarcounter=require('./lib').counter;varincCounter=require('./lib').incCounter;console.log(计数器);//3incCounter();console.log(计数器);//3上面的代码表明,计数器输出后,lib.js模块内部的变化不会影响计数器。ES6模块ES6模块不是一个对象,一个模块是一个文件,指定的输入代码通过export命令显示,通过import命令输入。exportexport命令指定了对外的接口,必须与模块内部的变量建立一一对应关系。//写法1:exportconstnum=123;exportconstname="xqs";exportfunctionfn(){console.log(obj.text);}exportconstobj={text:"我还是我用的那个男孩成为!”}//Writing2exportconstnum=123;exportconstname="xqs";functionfn(){console.log(obj.text);}constobj={text:"我还是以前的那个男孩!"}export{fn,obj}//写3constnum=123;constname="xqs";functionfn(){console.log(obj.text);}constobj={text:"我还在我曾经是那个男孩!”}export{num,name}export{fn,obj}//写四个constnum=123;constname="xqs";functionfn(){console.log(obj.text);}constobj={text:"I'mstillthatboy!"}export{num,name,fn,obj}上面的写法是等价的,我们一般用最后一种写法,因为输出变量更直观,看得见!变量仅通过导出命令输出。当我们通过import命令接收它们时,它们后面必须有一对花括号{},否则整个加载都通过*。//test.jsconstnum=123;constname="xqs";函数fn(){console.log(obj.text);}constobj={文本:“我还是那个男孩!”}export{num,name,fn,obj}//main.js//Writing1:import{name,fn}from"./test.js"fn();console.log(`我的名字是${name}`);//写2import*asresfrom"./test.js"res.fn();console.log(`我的名字是${res.name}`);通常使用第一种写法,因为ES6模块是“在编译时加载”,上面的例子中我们只需要name和fn,所以我们只需要加载name和fn,而不是全部加载,这样有利于提高加载效率!exportdefault我们还通过exportdefault命令为模块指定默认输出!//test-default.jsexportdefaultfunctionfoo(){console.log("Nochangeatall!")}//main-default.jsimportfooFuntionfrom"./test-default.js"fooFuntion();//test-default-1.jsconstnameObj={name:"MonkeyD.Luffy"};exportdefaultnameObj//main-default-2.jsimportobjfrom"./test-default-1.js"console.log(obj.name);就像上面的例子一样,通过exportdefault输出一个默认值一个函数,但是当我们通过import接收的时候,就不再需要花括号了,我们可以随意指定接收的名字,比如obj!都是模块输出,那么export和exportdefault有什么区别呢?//export.jsexportfunctionfn(){console.log("IamLuffy!")}//export-default.jsexportdefaultfunctionfoo(){console.log("Aceismybrother!")}//main.jsimport{fn}from"./export.js"importfoofrom"./export-default.js"fn();富();一是导入方式略有不同;其次,一个模块只有一个默认输出,那么exportdefault命令只能使用一次,而export可以多次使用!如果和CommonJs规范相比,我个人觉得export更像export,因为export只能输出一种格式,就是对象,而export输出的是一组类似于对象格式的变量!exportdefault就像module.exports,可以输出任何格式。不同的是exportdefault只能使用一次,而module.exports可以在一个模块中使用多次。当然,仅限于不改变默认输出的对象。案子言归正传,exportdefault本质上就是输出一个名为default的变量,系统允许我们给它起任意名字!//test.jsconstname="xqs";export{nameasdefault}//等同于//exportdefaultname;//main.jsimport{defaultastestName}from"./test.js";//相当于importtestNamefrom"./test.js";因为exportdefault命令实际上输出的是一个名为default的变量,所以只能使用一次。export和exportdefault同时使用://test.jsexportfunctionfn(){console.log("我是路飞!")}exportdefaultfunctionfoo(){console.log("Aceismybrother!")}//main.js//编写1:importfoo,{fn}from"./test.js"fn();富();//默认//写法2:import*asresfrom"./test.js"res.fn();res.default();//default//写三个:import{defaultasfoo,fn}from"./test.js"fn();富();//default有两种或三种写法,可以直观的看出exportdefault输出的是一个叫default的变量!import()我们知道ES6模块是静态加载的,也就是说在代码执行之前,已经通过import获取了输入变量!那就不要像require那样实现动态加载!幸运的是,现在使用import()函数来完成动态加载!//test.jsexportfunctionfn(){console.log("IamLuffy!")}exportdefaultfunctionfoo(){console.log("Aceismybrother!")}//main.jsimport("./test.js").then(({default:foo,fn})=>{fn();foo();}).catch(error=>{});通过import()可以像require一样实现动态加载。两者的区别在于import()返回的Promise实例是异步加载的,而require是同步加载的!ES6模块和CommonJS模块之间的差异CommonJS模块输出值的副本,而ES6模块输出对值的引用。CommonJS输出值的副本会在模块第一次加载时缓存模块,当模块加载时,即使输入值发生变化,原模块内部的变量也会受到影响。但是,ES6模块的输出是对该值的引用。当引入模块修改输入变量时,会对原模块内部的变量产生影响!CommonJS模块在运行时加载,而ES6模块是编译时输出接口。CommonJS模块是在运行时加载的,只有在执行require的时候才会加载模块,而ES6模块的导入是在编译阶段执行的,在代码执行之前获取输入变量。参考《JavaScript 标准参考教程(alpha)》,byRuanYifeng,https://javascript.ruanyifeng...《ECMAScript 6 入门》,byRuanYifeng,https://es6.ruanyifeng.com/#d...