前端面试系列之Js模块化加载

随着js模块化思想的诞生与变迁,社区逐渐形成了越来越多的模块加载规范,常见的如CommonJS、AMD、CMD、CommonJS等,他们有什么区别?分别是如何使用的呢? 下面用一些例子来分别介绍一下

CommonJS

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志”Javascript模块化编程”正式诞生,因此模块加载规范也随之诞生而来。CommonJS的核心思想是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports module.exports 来导出需要暴露的接口,所有代码都运行在模块作用域,不会污染全局作用域。

//a.js
function test1() {
  console.log('test1');
}
function test2() {
  console.log('test2');
}
exports.test2 = test2;
module.exports = {
	test1
}
//index.js
const f = require('./a.js');
f.test1(); // test1
f.test2(); // error: f.test2 is not a function

值得注意的是,module.exportsexports在模块声明时,最初都指向同一个引用地址,即exports = module.exports = {},但导出的对象为 module.exports。因此,需要注意尽量不要重新声明module.exportsexports,以免断开两者之间的关联,导致意料之外的错误。

CommonJS规范使用require加载模块是同步的,只有加载完成后才能进行后面的操作,不仅加载速度慢,而且还可能导致性能、可用性、调试和跨域访问等问题。nodejs主要应用于服务器编程,加载的模块一般存在于本地硬盘中,加载起来比较快,但是同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的。因此在浏览器端,requirejs和seajs之类的工具包也出现了,他们分别在推广过程中诞生了对应的模块加载规范,即AMDCMD

AMD- 异步模块定义规范

AMD是RequireJS在推广过程中对模块定义的规范化产出,他可以异步加载依赖模块,并且会提前加载。模块必须采用特定的define()函数来定义,主要接口为define(id?, dependencies?, factory),demo如下:

// module1.js  module1 不依赖其他模块 可以直接define(factory) 导出add
define((require, exports, module) => {
  const add = (x, y) => x + y;
  return { add };
});
// 也可以使用exports或者module.exports来导出 不过优先级return > module.exports > exports 
define((require, exports, module) => {
  const add = (x, y) => x + y;
  exports.count = 2
  exports.add = add
  module.exports = { add }
});
// 如果return是唯一代码,还可以简写为object
define({
  count: 2,
  add:(x, y) => x + y,
});
// module2.js  module2引入module中的add函数 导出add3
define(['module1'], function ({ add }) {
  const add3 = n => add(3, n);
  return { add3 };
});
// module3.js 
define(['module1'], function ({ add }) {
  const add6 = n => add(6, n);
  return { add6 };
});
<!--index.html 注意,需要先引入requirejs,定义全局变量require和define-->
<script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"></script>
<script>
// module1为路径,也可写为 define(['./module1'],cb)
// 推荐使用require全局配置来定义每个模块的加载路径,可以是绝对路径,也可以是相对路径
require.config({
  paths: {
    module1: './module1', 
    module2: './module2'
  },
});
</script>	
<script>
  require(['module2', 'module3'], ({ add3 }, { add6 }) => {
    console.log(add3(4)); // 7
    console.log(add6(2)); // 8
  });
</script>

CMD- 通用模块定义规范

CMD是SeaJS在推广过程中对模块定义的规范化产出。他和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。。AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。AMD要求依赖前置,在最开始就提前加载所需要的模块,而CMD推崇依赖就近,可以在任何地方去加载

<!-- index.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/seajs/3.0.3/runtime.js"></script>
<script>
  seajs.config({
    base: './',
    alias: {
      module1: 'module1.js',
      module2: 'module2.js',
      module3: 'module3.js',
    },
  });
  seajs.use('./index.js');
</script>
// index.js
define(function (require, exports, module) {
  const module3 = require('./module3.js');
  module3.add6(5); // 11
  require.async('./module2', function (module2) {
    module2.add3(3); // 6
  });
});

ES6模块

ECMAScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"。普通示例如下:

// a.js
// 使用export命令暴露模块的对变量
export const name2 = 'name2';
export function getName2() {
  console.log('getName2');
}

function getName() {
  console.log('getName');
}
const name = 'name';
// export后跟大括号指定所要输出的一组变量与上面单独输出是等价的
// 但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量
export { name, getName };

// index.js
// 通过import加载这个模块
import { name, name2, getName, getName2,  } from './a';

es6 module中还声明了一个as关键字可以用来对变量进行重命名

// a.js
const name3 = 'name3';
export { name1, name3 as name4 };
// 对输出的变量name3 重命名为 name4 等价于
export const name4 = name3;

// index.js
import { name, getName, name4 as name3 } from './a';
console.log('name3', name3); // name3

es6 module中还可以使用export default命令,为模块指定默认输出。

// a.js
export default function fn1() {
  console.log('fn1');
}

// index.js
import f from './a'
f(); // fn1

本质上,export default就是输出一个叫做default的变量或方法

// a.js
// 输出一个叫做default的变量,所以它后面不能跟变量声明语句
export default const name = 'name2' // error
export default name = 'name'

// index.js
import a from './a'
console.log(a); // name
// 同理,我们可以用as进行重命名
import { default as a } from './a'
console.log(a); // name

当需要导出的内容在不同文件时,我们可以单独做一个汇总:

// util1.js
export default fn1
export const fn11 = fn11

// util2.js
export default fn2

// util3.js
export { fn3, fn4, fn5 }

// index.js
export { default as fn1, fn11 } from './util1';
export { default as fn2 } from './util2';
export { fn3, fn4, fn5 } from './util3';

// 这样 可以直接从index引入fn1,fn2,fn3...
import { fn1, fn2, fn3, fn4} from './index'

举例一些常见的错误使用方式

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

// index.js
import a from './a'
a.fn1() // Uncaught TypeError: Cannot read property 'fn1' of undefined
// `import moduleName from 'module'`moduleName指的是模块module中export default 导出的内容,若未导入则为undefined。正确方式为应该为
import { fn1 } from './a'
fn1();
// a.js
export default {
  fn1: function () {},
};

// index.js
import { fn1 } from './a'
fn1() // import error: 'fn1' is not exported from './a'.
// {}内的内容是export导出的所有变量,而fn1是default对象下的key 
// 正确使用方式为:
import A from './a'  // 引入方式1
import { default as A } from './a'; // 引入方式2 两者等价
A.fn1()
// a.js
export const name = '2';
export default {
  fn1: function () {},
};

// index.js
import * as A from './a';
A.fn1(); // import error: 'fn1' is not exported from './a'
// * 包含export导出的所有内容,包含name、default等
// 正确使用方式为:
A.default.fn1();

总结

下面用表格来分别概括他们

|规范特点api代表
CommonJS同步加载module.exports/exports、requirenodejs
AMD异步加载、依赖前置define、requireRequireJS
CMD异步加载、依赖就近define、require.asyncSeaJS
es6 module静态加载,编译时进行静态分析export、importes6

参考链接:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值