js模块总结

一、原始写法

  1. 模块封装在function中
    • 缺点: 污染了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
  2. 模块封装在对象里
    • 缺点: 所有模块成员,内部状态可以被外部改写
  3. 立即执行函数写法: 模块封装在立即执行函数内部
    • 可以达到不暴露私有成员的目的

二、主流模块规范

  1. CommonJS规范
    • 背景: 来源于NodeJs的发明: 应用于服务端模块化
    • 暴露模块使用: ++module.exports++++exports++
    • 加载模块使用: require()
var math = require('math');
math.add(2,3); // 5
  1. 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++
  1. CMD规范
    • 背景: 与AMD类似,是 ++seajs++ 推崇的规范
    • CMD特点: 依赖就近,用的时候再require

CMD与AMD区别: CMD与AMD皆为异步加载模块,他们最大的区别是对依赖模块的执行时机处理不同

  1. AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;
  2. CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

三、现阶段标准: ES6 Modules

背景: ES6标准发布后,module成为标准

  1. es6标准: 使用export导出模块,import引入模块
  2. 而一贯的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: 从已经存在的模块、脚本文件等导入

命名式导入

  1. 我们可以通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员:

注意: 花括号里面的变量与export后面的变量需要一一对应

import {myMember} from "my-module";
import {foo, bar} from "my-module";
  1. 通过*符号,我们可以导入模块中的全部属性和方法。

当导入模块全部导出内容时,就是将导出模块(’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' ] }
*/
  1. 每个js文件一创建,都有一个var exports = module.exports = {},使exports和module.exports都指向一个空对象。
  2. module.exports和exports所指向的内存地址相同
  3. 由于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个步骤:

  1. 路径分析(模块标识符分析)

辨别模块的属性,是核心模块, 文件模块(相对路径/绝对路径), 还是自定义模块

  1. 文件定位

有缓存机制,二次加载会快

  • 文件拓展名分析(.js/.json/.node)
  • 目录分析和包(根据package.json分析,如果没有package.json或路径错误,则依次index.js/.json/.node)
  1. 编译执行
  • .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过程,比核心模块慢

你可能感兴趣的:(js模块总结)