Node中没搞明白require和import,你会被坑的很惨


原文链接: Node中没搞明白require和import,你会被坑的很惨

Node中没搞明白require和import,你会被坑的很惨 - 腾讯Web前端 IMWeb 团队社区
ES6 export && export default 差异总结 - 掘金

ES6 标准发布后,module 成为标准,标准的使用是以 export 指令导出接口,以 import 引入模块,但是在我们一贯的 node 模块中,我们采用的是 CommonJS 规范,使用 require 引入模块,使用 module.exports 导出接口。

不把 require 和 import 整清楚,会在未来的标准编程中死的很难看。

require 时代的模块

node 编程中最重要的思想之一就是模块,而正是这个思想,让 JavaScript 的大规模工程成为可能。模块化编程在 js 界流行,也是基于此,随后在浏览器端,requirejs 和 seajs 之类的工具包也出现了,可以说在对应规范下,require 统治了 ES6 之前的所有模块化编程,即使现在,在 ES6 module 被完全实现之前,还是这样。

node 的 module 遵循 CommonJS 规范,requirejs 遵循 AMD,seajs 遵循 CMD,虽各有不同,但总之还是希望保持较为统一的代码风格。

// a.js

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

// ----------- AMD or CMD ----------------
define(function(require, exports, module){
  module.exports = {
    a : function() {},
    b : 'xxx'
  };
});

可以看出,为了保持风格的高度统一,除了在浏览器端的模块中要使用一个 define 函数来提供模块的闭包以外,其他代码可以完全一致。

// b.js

// ------------ node ---------
var m = require('./a');
m.a();

// ------------ AMD or CMD -------------
define(function(require, exports, module){
   var m = require('./a');
   m.a();
});

在使用上,也非常相似。虽然 AMD or CMD 提供了更加丰富的风格,但是我们本文主要是讨论 node 环境下,所以不做扩展。

ES6 中的 module

ES6 发布的 module 并没有直接采用 CommonJS,甚至连 require 都没有采用,也就是说 require 仍然只是 node 的一个私有的全局方法,module.exports 也只是 node 私有的一个全局变量属性,跟标准半毛钱关系都没有。

export 导出模块接口

export 的用法挺复杂的,具体有哪些可以看这里。这里举几个例子:

// a.js
export default function() {}
export function a () {}

var b = 'xxx';
export {b}; // 这是ES6的写法,实际上就是{b:b}
setTimeout(() => b = 'ooo', 1000);
export var c = 100;

在要导出的接口前面,加入 export 指令。

在 export 之后,b 还可以被修改,这和 CommonJS 有着巨大不同,关于内部机理的东西,本文就无耻的省略了。

注意,下面的语法有严重错误:

// 错误演示
export 1; // 绝对不可以
var a = 100;
export a;

export 在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出 1 没有任何意义,也不可能在 import 的时候有一个变量与之对应。export a虽然看上去成立,但是a的值是一个数字,根本无法完成解构,因此必须写成export {a}的形式。即使 a 被赋值为一个 function,也是不允许的。而且,大部分风格都建议,模块中最好在末尾用一个 export 导出所有的接口,例如:

export {fun as default,a,b,c};

import 导入模块

import 的语法跟 require 不同,而且 import 必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言风格一致。

import 的使用和 export 一样,也挺复杂,可以在这里大致了解。举几个例子:

import $ from 'jquery';
import * as _ from '_';
import {a,b,c} from './a';
import {default as alias, a as a_a, b, c} from './a';

这里有一些坑,暂时不透露,下面会讲到。

import 后面跟上花括号的形式是最基本的用法,花括号里面的变量与 export 后面的变量一一对应。这里,你必须了解对象的解构赋值的知识,没这知识,你根本没法在这里装逼。了解了解构赋值,这里的 “一一对应” 的关系就能具体理解了。

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 'jquery';

import 后面第一个${defalut as $}的替代写法。

* 符号

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

import * as _ from '_';

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

另外还可以通过 * 号直接继承某一个模块的接口:

export * from '_';

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

  • 符号尽可能少用,它实际上是使用所有 export 的接口,但是很有可能你的当前模块并不会用到所有接口,可能仅仅是一个,所以最好的建议是使用花括号,用一个加一个。

该用 require 还是 import?

require 的使用非常简单,它相当于 module.exports 的传送门,module.exports 后面的内容是什么,require 的结果就是什么,对象、数字、字符串、函数…… 再把 require 的结果赋值给某个变量,相当于把 require 和 module.exports 进行平行空间的位置重叠。

而且 require 理论上可以运用在代码的任何地方,甚至不需要赋值给某个变量之后再使用,比如:

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

你在使用时,完全可以忽略模块化这个概念来使用 require,仅仅把它当做一个 node 内置的全局函数,它的参数甚至可以是表达式:

require(process.cwd() + '/a');

但是 import 则不同,它是编译时的(require 是运行时的),它必须放在文件开头,而且使用格式也是确定的,不容置疑。它不会将整个模块运行后赋值给某个变量,而是只选择 import 的接口进行编译,这样在性能上比 require 好很多。

从理解上,require 是赋值过程,import 是解构过程,当然,require 也可以将结果解构赋值给一组变量,但是 import 在遇到 default 时,和 require 则完全不同:var $ = require('jquery');import $ from 'jquery'是完全不同的两种概念。

上面完全没有回答 “改用 require 还是 import?” 这个问题,因为这个问题就目前而言,根本没法回答,因为目前所有的引擎都还没有实现 import,我们在 node 中使用 babel 支持 ES6,也仅仅是将 ES6 转码为 ES5 再执行,import 语法会被转码为 require。这也是为什么在模块导出时使用 module.exports,在引入模块时使用 import 仍然起效,因为本质上,import 会被转码为 require 去执行。

但是,我们要知道这样一个道理,ES7 很快也会发布,js 引擎们会尽快实现 ES6 标准的规定,如果一个引擎连标准都实现不了,就会被淘汰,ES6 是迟早的事。如果你现在仍然在代码中部署 require,那么等到 ES6 被引擎支持时,你必须升级你的代码,而如果现在开始部署 import,那么未来可能只需要做很少的改动。(完)

`