js模块总结
一、原始写法
模块封装在function中
缺点: 污染了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
模块封装在对象里
缺点: 所有模块成员,内部状态可以被外部改写
立即执行函数写法: 模块封装在立即执行函数内部
可以达到不暴露私有成员的目的
二、主流模块规范
CommonJS规范
背景: 来源于NodeJs的发明: 应用于服务端模块化
暴露模块使用: ++module.exports++ 和 ++exports++
加载模块使用: require()
var math = require('math');
math.add(2,3); // 5
AMD规范
背景: 有了运行在服务端的CommonJS, 开始考虑兼容客户端js模块化。然而CommonJS规范不适用于浏览器环境, 等待require加载模块会阻断应用(服务端因为模块在本地磁盘上,所以可以忽略不计;客户端需要网络加载,所以取决于网速), 所以,客户端需要异步加载组件
模块必须采用特定的define()函数来定义, 使用require()加载
define(id?, dependencies?, factory)
// id:字符串,模块名称(可选)
// dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
//factory: 工厂方法,返回一个模块函数
require([module], callback);
// [module],是一个数组,里面的成员就是要加载的模块
// callback,则是加载成功之后的回调函数
主要有两个Javascript库实现了AMD规范:++require.js++ 和 ++curl.js++
CMD规范
背景: 与AMD类似,是 ++seajs++ 推崇的规范
CMD特点: 依赖就近,用的时候再require
CMD与AMD区别: CMD与AMD皆为异步加载模块,他们最大的区别是对依赖模块的执行时机处理不同
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;
CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
三、现阶段标准: ES6 Modules
背景: ES6标准发布后,module成为标准
es6标准: 使用export导出模块,import引入模块
而一贯的node模块中,仍然使用CommonJS规范: 使用module.exports/exports导出模块, require引入模块
export导出模块
export语法声明用于导出函数、对象、指定文件(或模块)的原始值(注意: 在node中使用的是exports)
export有两种模块导出方式:
命名式导出(名称导出export)和默认导出(定义式导出export default)
命名式导出每个模块可以多个,而默认导出每个模块仅一个。
// 类型1:
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
// 类型2:
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// 类型3:
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
类型1:
name1… nameN为命名式导出的“标识符”。导出后,可以通过这个“标识符”在另一个模块中使用import引用
类型2:
default为模块的默认导出。设置后import不通过“标识符”而直接引用默认导入
类型3:
继承模块并导出继承模块所有的方法和属性
from 表示从已经存在的模块、脚本文件导出
as 可以重命名导出“标识符”
命名式导出
模块可以通过export前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出。
export { myFunction }; // 导出一个已定义的函数
export const foo = Math.sqrt(2); // 导出一个常量
可以使用*和from关键字来实现的模块的继承:
export * from 'math';
错误演示:
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导入时也会很容易引用
export default function() {}; // 可以导出一个函数
export default class(){}; // 也可以出一个类
默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default名称的命名导出。下面两种导出方式是等价的:
const D = 123;
export default D;
export { D as default };
使用示例
export (名称式导出):
// 导出"my-module.js" 模块
export function cube(x) {
return x * x * x;
}
const foo = Math.PI + Math.SQRT2;
export { foo };
// 引用模块
import { cube, foo } from 'my-module';
console.log(cube(3)); // 27
console.log(foo); // 4.555806215962888
export default (默认导出):
// 导出"my-module.js"模块
export default function (x) {
return x * x * x;
}
// 引用 "my-module.js"模块
import cube from 'my-module';
console.log(cube(3)); // 27
import引入模块
import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import模块导入与export模块导出功能相对应,也存在两种模块导入方式:++命名式导入(名称导入)++ 和 ++默认导入(定义式导入)++。
import的语法跟require不同,而且import必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言风格一致。
import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";
name: 从将要导入模块中收到的导出值的名称
member, memberN: 从导出模块,导入指定名称的多个成员
defaultMember: 从导出模块,导入默认导出成员
alias, aliasN: 别名,对指定导入成员进行的重命名
module-name: 要导入的模块。是一个文件名
as: 重命名导入成员名称(“标识符”)
from: 从已经存在的模块、脚本文件等导入
命名式导入
我们可以通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员:
注意: 花括号里面的变量与export后面的变量需要一一对应
import {myMember} from "my-module";
import {foo, bar} from "my-module";
通过*符号,我们可以导入模块中的全部属性和方法。
当导入模块全部导出内容时,就是将导出模块(’my-module.js’)所有的导出绑定内容,插入到当前模块(’myModule’)的作用域中
import * as myModule from "my-module";
导入模块对象时,也可以使用as对导入成员重命名,以方便在当前模块内使用:
import {reallyReallyLongModuleMemberName as shortName} from "my-module";
导入多个成员时,同样可以使用别名:
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";
导入一个模块,但不进行任何绑定:
import "my-module";
默认导入
在模块导出时,可能会存在默认导出。同样的,在导入时可以使用import指令导出这些默认值。
直接导入默认值:
import myDefault from "my-module";
也可以在命名空间导入和名称导入中,同时使用默认导入:
import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用
或
import myDefault, {foo, bar} from "my-module"; // 指定成员导入
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语法。
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就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。
as的用途: 之所以设计这样的功能,是因为两个不同的模块可能暴露出相同的方法名,比如有一个c.js也通过了fun这个接口:
// c.js
export function fun() {};
如果在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。
CommonJS中module.exports 与 exports的区别
Module.exports才是真正的接口,exports只不过是它的一个辅助工具。最终返回给调用的是Module.exports而不是exports。所有的exports收集到的属性和方法,都赋值给了Module.exports。
exports.name = 'abcd'
module.exports.name = 'abcd'
module.exports = { name: 'abcd' }
从require导入方式去理解,关键有两个变量(全局变量module.exports,局部变量exports)、一个返回值(module.exports)
function require(...) {
var module = { exports: {} };
((module, exports) => {
// 你的被引入代码 Start
// var exports = module.exports = {}; (默认都有的)
function some_func() {};
exports = some_func;
// 此时,exports不再挂载到module.exports,
// export将导出{}默认对象
module.exports = some_func;
// 此时,这个模块将导出some_func对象,覆盖exports上的some_func
// 你的被引入代码 End
})(module, module.exports);
// 不管是exports还是module.exports,最后返回的还是module.exports
return module.exports;
}
// demo.js:
console.log(exports); // {}
console.log(module.exports); // {}
console.log(exports === module.exports); // true
console.log(exports == module.exports); // true
console.log(module);
/**
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/larben/Desktop/demo.js',
loaded: false,
children: [],
paths:
[ '/Users/larben/Desktop/node_modules',
'/Users/larben/node_modules',
'/Users/node_modules',
'/node_modules' ] }
*/
每个js文件一创建,都有一个var exports = module.exports = {},使exports和module.exports都指向一个空对象。
module.exports和exports所指向的内存地址相同
由于2,exports = function(){}等类似的导出方式,实际上是修改了exports的引用地址,使得exports没有在module.exports上挂载属性和方法,而require加载模块时返回的是module.exports,所以只能使用
exports.fn = ...这种形式才能成功导出
参考:
https://www.cnblogs.com/libin-1/p/7127481.html
四、NodeJs模块机制(深入浅出nodejs)
在node中引入模块,需要经历3个步骤:
路径分析(模块标识符分析)
辨别模块的属性,是核心模块, 文件模块(相对路径/绝对路径), 还是自定义模块
文件定位
有缓存机制,二次加载会快
文件拓展名分析(.js/.json/.node)
目录分析和包(根据package.json分析,如果没有package.json或路径错误,则依次index.js/.json/.node)
编译执行
.js文件: 通过fs模块同步读文件后编译执行
会对js头尾包装,进行作用域隔离,传入exports,require,module,__filename,__dirname参数
包装后的代码通过vm原生模块的runInThisContext()方法执行
因此node中并没有定义exports, require却可以直接使用
文件模块缓存在Module._cache对象上
.node文件: c/c++编写,调用dlopen()方法加载执行
.json文件: fs读取json文件,并JSON.parse()
(function(exports, require, module, __filename, __dirname) {
var math = require('math')
exports.math = function(radius) {
return Math.PI*radius*radius
}
})
node分为两种模块:核心模块和文件模块
核心模块: 由nodejs本身提供,加载时省略步骤2,3(文件定位和编译执行),因为核心模块在node源代码编译过程中,编译进了二进制执行文件,node进程启动时直接加载进内存中,加载速度最快
核心模块又分为: c/c++编写的和js编写的两部分,编译过程见书21页。js编写的核心模块缓存在NativeModule._cache对象上。
文件模块: 用户编写,加载需要完成的1,2,3过程,比核心模块慢
发表评论 (审核通过后显示评论):