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过程,比核心模块慢

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):