专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

本想搞清楚ESM和CJS模块的转换问题,没想到写完我的问题更

  本来只是好奇打包工具是如何转换ESM和CJS模块的,没想到带着这个问题阅读完编译的代码后,我的问题更多了。
  目前主流的有两种模块语法,一是Node。js专用的CJS,另一种是浏览器和Node。js都支持的ESM,在ESM规范没有出来之前,Node。js的模块编写使用的都是CJS,但是现在ESM已经逐渐在替代CJS成为浏览器和服务器通用的模块解决方案。
  那么问题来了,比如说我早期开发了一个CJS的包,现在想把它转成ESM语法用来支持在浏览器端使用,或者现在使用ESM开发的一个包,想转换成CJS语法用来支持老版的Node。js,转换工具有很多,比如Webpack、esbuild等,那么你有没有仔细看过它们的转换结果都是什么样的,没有没关系,本文就来一探究竟。ESM模块语法
  先来简单过一下常用的ESM模块语法。
  导出:esm。jsexportletname1周杰伦等同于letname2朴树export{name2}重命名export{name1asname3}默认导出一个模块只能有一个默认输出,因此exportdefault命令只能使用一次本质上,exportdefault就是输出一个叫做default的变量或方法,所以可以直接一个值,导入时可以使用任意名称exportdefault华语乐坛经典人物
  导入:具名导入importtitle,{name1,name2,name3,name1asname4}from。esm。js;整体导入importtitle,asnamesfrom。esm。js;CJS模块语法
  CJS模块语法会更简单一点,导出:方式一exports。name2朴树等同于module。exports。name1周杰伦方式二module。exports{name1:周杰伦,name2:朴树}
  导入:整体constnamesrequire(。cjs。js)console。log(names)解构const{name1,name2}require(。cjs。js)console。log(name1,name2)
  从我们肉眼观察的结果,CJS的exports。xxx类似于ESM的exportletxxx,CJS的module。exportsxxx类似于ESM的exportdefaultxxx,但是它们的导入形式是有所不同的,ESM的importxxx的xxx代表的只是exportdefaultxxx的值,如果没有默认导出,这样导入是会报错的,需要使用importasxxx语法,但是CJS其实无论使用的是exports。xxx还是module。exports,实际上导出的都是module。exports这个属性最终的值,所以导入的也只是这个属性的值。
  实际上,CJS和ESM有三个重大的差异:CJS模块输出的是一个值的拷贝,ESM模块输出的是值的引用CJS模块是运行时加载,ESM模块是编译时输出接口CJS模块的require()是同步加载模块,ESM模块的import命令是异步加载,有一个独立的模块依赖的解析阶段
  那么,在它们两者互相转换的过程中,是如何处理这些差异的呢,接下来我们使用esbuild来进行转换,为什么不用webpack呢,无他,唯简单尔,看看它是如何处理的,安装:npminstallesbuild
  增加一个执行转换的文件:build。jsrequire(esbuild)。buildSync({entryPoints:〔〕,待转换的文件outfile:out。js,format:,转换的目标格式,cjs、esm});
  然后我们在命令行输入node。build。js命令即可看到转换结果被输出在out。js文件内。ESM转CJS转换导出
  待转换的内容:exportletname1周杰伦letname2朴树export{name2}export{name1asname3}exportdefault华语乐坛经典人物
  接下来看一下转换结果的代码,核心的导出语句如下:module。exportstoCommonJS(esmexports);
  导出的数据是调用toCommonJS方法返回的结果,先来看看参数esmexports:varesmexports{};export(esmexports,{default:()esmdefault,name1:()name1,name2:()name2,name3:()name1});letname1周杰伦;letname2朴树;varesmdefault华语乐坛经典人物;
  先定义了一个空对象esmexports,然后调用了export方法:vardefPropObject。defineProperty;varexport(target,all){遍历对象for(varnameinall)给对象添加一个属性,并设置属性描述符的取值函数get为all对象上该属性对应的函数,那么该属性的值也就是该函数的返回值defProp(target,name,{get:all〔name〕,enumerable:true});};
  上面所做的事情就是给esmexports对象添加了四个属性,这四个属性很明显就是我们使用ESM的export导出的所有变量,exportdefault默认导出,本质上就是导出了一个叫做default的变量而已,没有什么特别的:exportdefaulta等同于export{aasdefault}
  所以默认导出的变量会定义成名为default的属性添加到这个对象上,这很明显,因为我们知道CJS的导出其实是module。exports属性的值,那么我们使用ESM导出了多个变量,只能都添加到一个对象上来导出,注意看其中两点:
  1。添加属性没有直接使用esmexports。xxx的方式来添加,而是使用Object。defineProperty方法,并且只给属性定义了取值函数get,没有定义赋值函数set,这意味着esmexports的这个属性的值是不能被修改的,这其实是CommonJS和ESM的一个不同点:ESM导出的接口不能修改,而CJS可以。
  所以下面这些ESM做法都是会报错的:importasnamesfrom。esm。js;names。name1许巍;报错importtitle,{name1,name2,name3,name1asname4}from。esm。js;title许巍;报错name1许巍;报错
  而CJS不会:constnamesrequire(。cjs。js);names。name11;成功let{name1,name2}require(。cjs。js);name11;成功
  2。设置属性的描述符时没有直接使用value,比如:varexport(target,all){for(varnameinall)defProp(target,name,{value:all〔name〕,enumerable:true});};export(esmexports,{default:esmdefault,name1:name1,name2:name2,name3:name1,setName1:setName1});
  而是定义了get取值函数,通过函数的形式返回同名变量的值,这其实又是一个不同点了:CJS模块输出的是一个值的拷贝,ESM模块输出的是值的引用。
  比如在ESM模块中:esm。jsexportletname周杰伦exportconstsetName(newName){namenewName}other。jsimport{name,setName}form。esm。jsconsole。log(name)周杰伦setName(许巍)console。log(name)许巍
  可以看到导入地方的值也跟着变了,但是在CJS模块中就不会:cjs。jsletname周杰伦constsetName(newName){namenewName}module。exports{name,setName}other。jslet{name,setName}require(。cjs。js)console。log(name)周杰伦setName(许巍)console。log(name)周杰伦
  正是如此,所以才需要通过设置get函数来实时取值,否则转换成CJS后,变量的值只拷贝了一份,后续变化了都不会再更新。
  回到这行:module。exportstoCommonJS(esmexports);
  看完了esmexports,接下来看看toCommonJS方法:vartoCommonJS(mod)copyProps(defProp({},esModule,{value:true}),mod);
  首先创建了一个空对象,然后使用Object。defineProperty添加了一个esModuletrue的属性,这个属性是用于在导入的时候进行一些判断的,接下来调用了copyProps方法:vargetOwnPropNamesObject。getOwnPropertyNames;返回一个指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组varhasOwnPropObject。prototype。hasOwnProperty;返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键),该方法会忽略掉那些从原型链上继承到的属性vargetOwnPropDescObject。getOwnPropertyDescriptor;返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)varcopyProps(to,from,except,desc){if(fromtypeoffromobjecttypeoffromfunction){for(letkeyofgetOwnPropNames(from))if(!hasOwnProp。call(to,key)key!except)defProp(to,key,{get:()from〔key〕,enumerable:!(descgetOwnPropDesc(from,key))desc。enumerable});}returnto;};
  这个方法做的事情是把from对象的所有属性都在to对象上添加一份,不过如果to对象上存在同名属性则不会覆盖,会发生在如下这种情况:cjs。jsexportletfoo1cjsUse。jsexportfrom。cjs。jsexportletfoo2
  存在同名导出,cjsUse模块会覆盖cjs模块的同名导出,所以最终导出的foo2。
  同时会设置新添加属性的属性描述符,设置取值函数get,返回值为from对象的该属性值,因为没有设置get,所以添加的属性值也是不能被修改的。
  简单来说就是创建了一个新对象,把esmexports的属性都添加到新对象上,但是访问该新对象的属性时实际上最终访问的还是from对象的该属性值,相对于一个代理对象,然后对外导出该新对象。
  百思不得解啊1:为啥要创建一个新对象,而不是直接导出esmexports对象呢?
  另外我们可以发现,ESM的默认导出CJS是不支持的,在ESM中默认导出我们可以这么导入:importdefaultValuefromxxx
  但是转成CJS后不能这样导入:constdefaultValuerequire(xxx)
  而是需要通过。default的形式才能获取到真正的defaultValue:constimportDatarequire(xxx)console。log(importData。default)
  所以可以的话还是尽量少用默认导出吧。转换导入
  接下来看看导入的转换:importtitle,{name1,name2,name3,name1asname4}from。esm。js;console。log(title,name1,name2,name3,name4);
  转换结果:varimportesmtoESM(require(。esm。js));console。log(importesm。default,importesm。name1,importesm。name2,importesm。name3,importesm。name1);
  对导入的数据调用了toESM方法:varcreateObject。create;创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)vardefPropObject。defineProperty;vargetProtoOfObject。getPrototypeOf;返回指定对象的原型(内部〔〔Prototype〕〕属性的值)vartoESM(mod,isNodeMode,target)(导入的模块存在,则使用该模块的原型为原型创建一个新对象作为target(targetmod!null?create(getProtoOf(mod)):{}),将导入模块的属性拷贝到target对象上copyProps(isNodeMode!mod!mod。esModule?defProp(target,default,{value:mod,enumerable:true}):target,mod));
  百思不得解啊2:为啥要以导入模块的原型为原型来创建一个新对象呢?
  百思不得解啊3:为啥导入也要创建一个新对象?
  可以看到也创建了一个新对象,然后把导入模块的属性添加到这个新对象上,前面在转换导出的时候会给导出的对象添加一个esModuletrue的属性,这里就用到了,为true就代表该模块是ESM转换而成的CJS模块,否则就是原始的CJS模块,这样的话会给target对象添加一个default属性,值就是导入的数据,这是为啥呢,其实是为了兼容导入原始的CJS模块,比如:导出exportdefaultclassPerson{}导入importPersonfromxnewPerson()
  转换成CJS以后:导出module。exports{default:()Person}导入constresrequire(x)newres。default()
  但是如果x模块不是由ESM转换而来的,本身就是一个CJS模块:module。exportsPerson
  那么res就是导出的类,再获取它的default属性显然是不对的,所以需要手动创建一个对象,并添加一个default属性来引用。CJS转ESM转换导出
  待转换的内容如下:module。exports。name1周杰伦exports。name2朴树
  转换结果如下:。。。exportdefaultrequirecjs();
  为什么要转换成默认导出而不是具名导出呢,一是因为require本身就很类似importxxx默认导入语法,二是转成具名导出不方便,比如如下导出:constres{name1:周杰伦}module。exportsresif(Math。random()0。5){res。name2许巍}else{res。name3朴树}
  不实际执行代码压根不知道最终导出的是啥,所以具名导出就不可能,只能使用默认导出,这样我只管导出module。exports属性,至于它上面都有啥就不管了。
  看看requirecjs方法:vargetOwnPropNamesObject。getOwnPropertyNames;varcommonJS(cb,mod)functionrequire(){return(mod(0,cb〔getOwnPropNames(cb)〔0〕〕)((mod{exports:{}})。exports,mod),mod。exports);};varrequirecjscommonJS({cjs。js(exports,module){module。exports。name1周杰伦;exports。name2朴树;},});
  百思不得解啊4:为啥要搞成这么奇怪的格式,直接传函数不行吗?
  因为CJS的导出就是使用在module。exports对象上添加属性,或者是重写module。exports属性,所以直接将原模块的代码放到一个函数里,然后通过参数的形式传入module对象和exports属性,这样无需关心代码都做了什么,只要最后导出module。exports属性即可,并且还增加了缓存的机制,这也是CJS的一个特性,即同一个模块,只有第一次导入时会去执行该模块的代码,然后获取到导出的数据后就会把它缓存起来,后续再导入这个模块会直接从缓存里获取导出数据,这也是CJS不同于ESM的特性。转换导入
  待转换的代码:constresrequire(。cjs。js)console。log(res);
  转换结果:
  报错了,提示目前不支持将require转换成esm,这是为啥呢,其实是因为require是同步的,运行时的,所以可以动态导入、条件导入,可以出现在非顶层,把它当做一个普通函数看待即可,但是import导入不行,它是静态编译的,必须出现在顶层,所以是无法转换的,那怎么办呢,很简单,只要把require干掉就行,也就是把所有模块都打包到同一个文件里,假设被引入的文件两个模块如下:cjs。jsmodule。exports{name1:周杰伦,name2:朴树}cjs2。jsmodule。exports{name3:许巍,name4:梁博}
  导入它们的模块内容如下:constresrequire(。cjs。js)console。log(res);constres2require(。cjs2。js)console。log(res2);module。exports{res,res2}
  然后修改一下我们执行转换的build。js文件:build。jsrequire(esbuild)。buildSync({entryPoints:〔〕,outfile:out。js,format:,bundle:true});
  然后再转换就不会报错了,结果如下:。。。cjs。jsvarrequirecjscommonJS({cjs。js(exports,module){module。exports。name1周杰伦;exports。name2朴树;}});cjs2。jsvarrequirecjs2commonJS({cjs2。js(exports,module){module。exports{name3:许巍,name4:梁博};}});cjsUse。jsvarrequirecjsUsecommonJS({cjsUse。js(exports,module){varresrequirecjs();console。log(res);varres2requirecjs2();console。log(res2);module。exports{res,res2};}});exportdefaultrequirecjsUse();
  可以看到其实和转换导出的逻辑是一样的,每个模块的内容都会包裹到一个函数里,然后生成一个函数,执行这个函数时就会执行该模块的代码,然后导出的数据就会挂载到module。exports上,无论是模块内使用还是导出都可以。总结
  温馨提醒,本文的内容纯粹是笔者的个人观点,不一定保证正确另外以上这些问题也可能没有所谓的原因,换一个转换工具,比如babel、rollup等可能又会生成不同的代码,有兴趣的自行尝试吧。
  总结一下:ESM转CJS:所有导出的变量都挂载到一个对象上,然后module。exports该对象。导入的话会判断是经ESM转换的CJS模块,还是原始的CJS模块,都会先创建一个对象,原始CJS模块的话会添加一个default属性来保存导入的数据,非原始CJS模块的话会直接将属性拷贝到新对象上,最后这个新对象作为导入的结果。CJS转CSM:将模块的内容包裹到一个函数内,通过参数的形式传入module对象和module。exports属性,函数的执行结果为module。exports属性的值,并且通过高阶函数的形式来增加缓存导出的功能,转换导出的话直接exportdefault该函数的执行结果,导入的话不能单独转换,需要都打包到同一个文件中,所以也就不存在转换后的import语句。

行业最新数据!盈利驾校的秘诀在这里现在不一样了!市场很乱,也赚不到钱。周志平(化名)在湖北经营一所驾校已有八年的时间,这是他对如今驾培市场的看法。这话或许有些偏激,但他对驾培行业是有着深厚感情的,以前我做驾培市场的专访叶舟河西走廊是读不完的大书,我愿终身为徒在百万字小说敦煌本纪面世后的四年里,作家叶舟只想做好一件事为父亲写一部以河西走廊为背景,以古凉州为原点的长篇小说。叶舟的父亲是甘肃武威人,酷爱读书。他读张承志,读杨显惠,读阿来经常开放的大市场带来更多新机遇中国经济释放强引力对于在北京召开的全国两会,上海的外企也很关心。近日,上海市外商投资协会举办政府事务和公共关系研修课程,邀请曾在政府媒体等机构工作的资深业内人士给外企人员培训,培训内容与正在召开的全3月养老金调整即将正式敲定,3000以下退休人员,会涨得更多吗?现在已经进入了3月份的时间了,3月份也是两会召开的一个重要日子,按照往年的惯例,在两会的政府工作报告当中,通常都会提起养老金继续调整的待遇,而今年财政部门的新闻发布会已经明确指出,国足创历史!时隔九年重返亚洲杯八强!在3月9日的比赛中,国足U20男足与吉尔吉斯斯坦经过90分钟的激烈竞争,以1比1的比分打成平局。这是国足U20男足在男足亚洲杯中时隔9年再次闯入八强。他们在小组赛中获得一胜一平一负妈妈,我撞死了一名美国外交官!骗子盯上AI技术,克隆声音实施诈骗尽管诈骗有多种形式,但本质上工作方式是一样的骗子冒充值得信任的人孩子爱人或朋友,并说服受害者给他们汇款。但是,AI特别是2023年爆火的生成性人工智能生成的语音使这一诡计更加令人信上位就原形毕露的骗子国君,晋惠公为何将一手好牌打得稀烂?历史开讲作为小宗取代大宗的胜利者,曲沃代翼标志着周朝建立的礼乐秩序遭到破坏,然而晋国的国运并没有像世人想象的那样蒸蒸日上,反而因为晋献公晋惠公晋怀公的辣眼操作导致国家的前路黯淡无光骗子依旧在,切不可轻心!(春天平姐姐摄)今日是周末,就不说证券投资的那些事了。最主要的原因也是因为两会要召开了,投资者都需要知道新的政策动向才好动作,因此,在两会最终的政策或是大会没有结束前,一般投资者都慈母的伟大换取骗子一颗良知好内容我来评病榻上的老人接到诈骗电话,对方谎称是她儿子。老人的儿子其实在两年前因车祸就已经去世,但因为骗子的声音和儿子实在太像,老人竟舍不得挂断,听了好久。最后老人说了实情,并请求从骗子到政协委员,从10亿到5000亿,阿里王坚不一般!刚刚看到一个新闻,阿里王坚当选新一届政协委员,这是一个颠覆中国云计算的男人!入职阿里之前,王坚曾是微软亚洲研究院,比尔盖茨的得力干将,这样一个人才,2008年被马云挖到阿里,做云计刘亦菲李惠仁谷爱凌同框看秀,刘亦菲美得一骑绝尘最近大大小小的时装周都开始了。像米兰时装周,巴黎时装周都已经迎来了好几波明星。往往这种时刻就是见证各大不同领域的艺人同框,或者自家偶像大杀四方。一直被称为神仙姐姐的刘亦菲受路易威登
就目前而言,俄罗斯还有哪些先进装备值得中国买?随着21世纪后,我国军队整体军事实力获得迅猛发展,已经差不多摆脱了依靠进口苏制武器装备度日的处境。说到这里你可能会觉得,我国现在国产武器的研发实力虽然比不上美国,但肯定已经完全超越冬至为什么吃饺子?你知道冬至是为了纪念谁吗?冬至就是24个节气中的一个,古人发现这一天白天最短夜晚最长的一天,也是冬天最冷的开始,于是人们从这一天开始数九,为了抵御严寒补充能量就要吃点肉,所以就留下了冬至不吃肉冻烂脚趾头的说为什么四川有重庆火锅,重庆却没有四川火锅?火锅的发源地在重庆,第一家开在尘都的火锅,是八十年代末九十年代初重庆人去成都开的,所以无论是用料还是味道都是重庆的最为正宗,成都不过是把本来属于重庆的火锅偷梁换柱,摇身一变就成了四圆明园十二兽首目前都身处何方?圆明园古迹海晏堂,建于1759年(乾隆二十四年)。海晏一词取意河清海晏,国泰民安。文苑英华唐郑锡日中有王字赋河清海晏,时和岁丰河,黄河晏,平静。河清海晏也作海晏河清,意指黄河水流澄买了个MBP写c,但是发现MacOS不能用VS2013,而且为什么感觉和win上的不一样呢?谢邀。题主的问题很简单,因为VS2013并没有提供Mac版,当然不能用了。目前发布的VS2019版本已经支持Mac系统了,不过并不支持CC开发。下面来回答下如何在Mac上搭建CC开假如中国有一百个联想,会怎么样?这个问题提的好,我想,假如中国有一百个联想,不用美国打压,中国经济会彻底的沦为西方附属加工厂了。联想的经营理念在改革开放初期无可厚非,但在现在国际形势下,该让它进入历史了。我这样说全国地级市(非省会)高等教育哪里最强?题主已经列举了保定市,保定市共有16所大学,其中本科院校10所,专科院校6所,在此就不一一列举了。地级市高等教育实力最强的除了保定市,当属苏州市。苏州市目前共有高校24所,其中本科孩子不喜欢看书,喜欢听书,听书和看书有多大的区别?孩子不喜欢看书喜欢听书,其实这也是件好事,听书同样是一种阅读形式,就因为它方便快捷所以广受现代人的欢迎。说起听书,我们不能不说说小时候的听广播经历。小时候我家的物质条件极其匮乏,家透析后患者的生命能延续多久?对于这个问题,随着透析和换肾技术的发展,现在尿毒症已经不是绝症。随着医保福利的越来越好,更多的人加入到透析大军中,也不乏有部分已经加入到换肾行列,过上了正常人的生活。那么透析开始后蒲公英根泡水喝对肝脏真的有好处吗?保护肝脏的方法有很多,很多人选择用蒲公英根浸泡水喝,以此来保护自己的肝脏。那么,蒲公英根泡水对肝脏有什么好处呢?蒲公英可以有效清热解毒,因此,蒲公英根泡水喝对肝脏的功效特别好,很有微信大额转账会被监管吗?不往银行卡里提现?会被监管。如果没有可疑的情况银行是不会限制大额转账的。我们银行卡的大额转账都会定时的上传央行,如果你的转账被判定有洗钱或者有违法的嫌疑,你的银行卡账户会被第一时间冻结,调查清楚之后
友情链接:快好找快生活快百科快传网中准网文好找聚热点快软网