探讨ES6的import export default 和CommonJS的require module.exports

今天来扒一扒在node和ES6中的module,主要是为了区分node和ES6中的不同意义,避免概念上的混淆,同时也分享一下,自己在这个坑里获得的心得。

在ES6之前

模块的概念是在ES6发布之前就出现的,我感觉主要是为了适应大型应用开发的需要而引入了JavaScript世界。模块化编程已经从噱头上升为必备,所以ES6也顺应时代,把这个写进了标准。

CommonJS和AMD都是JavaScript模块化规范,在ES6之前,Node主要遵循CommonJS,而AMD则主要运用在浏览器端,比如requirejs。

而且node发布的时候,就天生具备module,所以从某种意义上讲,是node促进了js世界里面的模块化编程。

// module-file.js for node
module.exports = {
  a : function() {},
  b : 'xxx'
};

把上面这个js文件放在node环境中,我们这样去用它:

var myModule = require('./module-file');
var b = myModule.b;myModule.a();

从模块化思想出发,requirejs和国内前端大牛发布的seajs(遵循CMD)也允许前端猿们通过require去加载另一个模块。不过在模块定义的时候,需要借助一个define函数:

// module-file.js for requirejs
define(function(require, exports, module){
   module.exports = {};
});

requirejs和seajs都支持上面这种形式,在define的回调函数中提供了模拟的require, exports, module,这让习惯了node环境中使用方法的猿类可以顺便在浏览器端写基本相同的代码。

node,requirejs,seajs也同时支持下面这种导出模块的方式:

define(function(){
  return {}; // return的值就是导出的模块
});

这就出现了UMD,即一个兼容多种环境的方案。

Node,requirejs中的exports

node中导出模块接口就是用exports,你可以这样做:

module.exports.a = function() {};
module.exports.b = 'xxx';

也可以写在一个对象中:

module.exports = {
  a : function() {},
  b : 'xxx'
}

在requirejs中,还提供了一个exports变量作为module.exports的别名:

define(function(require, exports, module){
   exports.a = function(){};
   exports.b = 'xxx';
});

注意“别名”的含义:exports是module.exports的地址的引用。它的本质是:

var exrpots = module.exports;

因此,你必须注意两个点,就是导出接口的时候:
1.不能直接用exports={}来导出整个接口;
2.如果使用了module.exports={},那么exports.a等都会被覆盖无效。

ES6中的import和export

在ES6之前,要使用一个模块,必须使用require函数将一个模块引入,但ES6并没有采用这种模块化方案,在ES6中使用import指令引入一个模块或模块中的部分接口,并没有将require写入标准,这也就是说require对于ES6代码而言,只是一个普通函数。

同理,在ES6标准中,导出模块的接口也只能使用export指令,而非exports对象,这也同样意味着module.exports只是node,requirejs等模块化库的自定义变量,而非ES标准接口。

常见export方式

在ES6规定中,这样导出一个模块的接口:

export function fun() {};
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
export * from …;export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;

ES6里面,直接把要导出的变量、函数、对象、类等前面加一个export关键字。比如:

// module-file.js
export function a(){};
export var obj = {};

有一个点:export必须导出具有对应关系的变量,下面的接口输出是错误的:

// 错误演示
export 1; // 这种导出的内容不是变量是绝对错误的,包括导出表达式,也是绝对错误的
var a = 1;
export a;
function b() {}
export b;

上面的这些方法都是错误的,不能这样导出接口。导出接口仅限两种:

1.声明时导出
2.以对象的形式导出(和解构联系起来)

如果要导出某个变量,可以用花括号括起来,像这样:

var a = 1;
export {a}; // 等效于:{a:a}
function b() {}
export {b};

我有点疑惑的是,为何export允许多次export {}这种形式?看上去很奇怪。另外,ES6更厉害之处在于,可以在export变量之后,继续修改变量:

export var obj {};obj.a = 1;

在import之后,obj的值仍然可以在模块内继续改变,这是CommonJS以往不可能做到的。

import基本用法

在另外一个js文件里面这样使用这些接口:

import {a,obj} from './module-file';
a();
alert(obj.b);

和node之前的require不一样,require只能把模块放到一个变量中,而在ES6中,拥有对象解构赋值的能力,所以直接就把引入的模块的接口赋值给变量了。内在机理也有不同,require需要去执行整个模块,将整个模块放到内存中(也就是我们说的运行时),如果只是使用到其中一个方法,性能上就差很多,而import...from则是只加载需要的接口方法,其他方法在程序启动之后根本触及不到,所以这种又被称为“编译时”,性能上好很多。

as关键字

编程的同学对as都容易理解,简单的说就是取一个别名。上面export中可以用,import中其实也可以用:

// a.js
var a = function() {};
export {a as fun};
// b.js
import {fun as a} from './a';a();

上面这段代码,export的时候,对外提供的接口是fun,它是a.js内部a这个函数的别名,但是在模块外面,认不到a,只能认到fun。

import中的as就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。之所以是这样,是因为有的时候不同的两个模块可能通过相同的接口,比如有一个c.js也通过了fun这个接口:

// c.js
export function fun() {};

如果在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。

default关键字

其他人写教程什么的,都把default放到export那个部分,我觉得不利于理解。在export的时候,可能会用到default,说白了,它其实是别名的语法糖:

// d.js
export default function() {}
// 等效于:function a() {}; export {a as default};

在import的时候,可以这样用:

import a from './d';
// 等效于,或者说就是下面这种写法的简写,是同一个意思import {default as a} from './d';

这个语法糖的好处就是import的时候,可以省去花括号{}。简单的说,如果import的时候,你发现某个变量没有花括号括起来,那么你在脑海中应该把它还原成有花括号的as语法。

所以,下面这种写法你也应该理解了吧:

import _,{each,map} from '_';

*符号

*就是代表所有,只用在import中,我们看下两个例子:

import * as underscore from '_';

在意义上和import _ from '';是不同的,虽然实际上后面的使用方法是一样的。它表示的是把''模块中的所有接口挂载到underscore这个对象上,所以可以用underscore.each调用某个接口。

export * from '_';
// 等效于:import * as all from '_';export all;

该用require还是import?

接下来的问题,就是我们在实操中,还有必要用require吗?我感觉ES6标准已经将之前的所有模块化规范都给碾压了,这在以前的标准发布中极少见,ES6用更加简单的方式,实现了更加有效的module,感觉require可以回家养老了。

标准与非标准

既然是标准,那么就是所有引擎应该去实现的,node和浏览器未来都会直接支持这种模块加载方式,require完成历史使命回家自己玩儿。而且作为node或浏览器,同时可以利用import提供自己的API,比如手机端提供基于网络的定位API,这都不用SDK了,直接内置在客户端内部,import一下就可以了。

不过现在import导入模块还并不是全部环境都支持,使用babel可以让node支持ES6,但在浏览器端,则毫无办法,可能还得暂时依赖require。但是非常不好的消息是,require不是ES6标准,这也就是说如果将来浏览器支持import后,你想用它,就必须升级代码,而不能直接被兼容。

import只能在文件开头使用,在import之前,你不能有其他的代码,这和其他语言是一样的。但是require则不同,它相当于node的一个定义在全局的函数,你可以在任意地方使用它,甚至使用变量表达式作为它的参数,这样有一个好处,就是可以在循环中加载模块。

有没有兼容import和require的模块?

但是很坑的是,node的模块导出和ES6标准也不符,因为node的模块体系遵循的是CommonJS规范,这就导致你写的模块文件,不可能同时支持require和import。

要强调的就是,不要把require和import两种模块加载方案混用,比如:

// module-file.js
module.exports = {};
// a.js
import a from './moule-file';

这种混搭感觉不是很好(但可以用,下面有解释)。所以,其实我没有任何建议,我只是觉得,躺在坑里,挺自在的……毕竟node中require的使用更加灵活一点,它没有必须放在哪里的限制,所以可以在任意位置使用,而且它的结果也非常形象,甚至可以把require当做一个引用类型别名,可以这样使用:

require('./a')(); // a模块是一个函数,立即执行a模块函数
var data = require('./a').data; // a模块导出的是一个对象
var a = require('./a')[0]; // a模块导出的是一个数组

这样的写法感觉像给模块取了一个别名,使用的时候非常灵活。但是需要注意的是,如果你打算使用require来导入这个模块,那么请使用module.exports导出这个模块。

(临时)兼容方案

有没有一种兼容方案呢?

function a() {}
class b {}
module.exports = {a,b}; // {a,b}是ES6的写法

在实践中发现,module.exports可以兼容require和import,而且这个案例需要你的node环境配置好支持ES6语法。module.exports导出的模块,如果使用import,那么完全就是一个对象赋值、解构的过程:

import mod,{a,b} from './a';

之所以这是成立的,是因为我们使用babel对ES6代码进行转码后执行,而实际上,目前为止,没有任何一个环境是支持ES6 module方案的,即使babel,也仅仅是将ES6的import,export转码为require, module.exports后交给node去执行。

导出的模块接口被赋值给mod,所以mod是一个对象,含有a,b两个方法。这里的mod并没有通过default导出,所以和ES6有非常大的意义上的区别,这种非标准的写法,墙裂建议永远不要用。而且,由于require和module.exports是非标准的东西,仅在Node环境中有效,所以当未来浏览器支持模块导入时,并不会主动提供require,而是采用import,如果要使用require,还是不得不使用requirejs等库,借助define来用。

所以,最终,如果你打算用CommonJS,就不要掺和进ES6.

转载链接:https://www.tangshuang.net/2882.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容