# js模块

function CoolModule() {
  var something = "cool";
  var another = [1, 2, 3];
function doSomething() {
  console.log( something );
}
function doAnother() {
  console.log( another.join( " ! " ) );
}
return {
  doSomething: doSomething,
  doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体。

从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数。

上一个示例代码中有一个叫作 CoolModule() 的独立的模块创建器,可以被调用任意多次,每次调用都会创建一个新的模块实例。当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式:

var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
  console.log( something );
}
function doAnother() {
  console.log( another.join( " ! " ) );
}
return {
  doSomething: doSomething,
  doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

模块模式另一个简单但强大的变化用法是,命名将要作为公共 API 返回的对象。

var foo = (function CoolModule(id) {
function change() {
  // 修改公共 API
  publicAPI.identify = identify2;
}
function identify1() {
  console.log( id );
}
function identify2() {
  console.log( id.toUpperCase() );
}
var publicAPI = {
  change: change,
  identify: identify1
};
  return publicAPI;
})( "foo module" );
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

通过在模块实例的内部保留对公共 API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。

# 现代的模块机制

大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API。这里并不会研究某个具体的库,为了宏观了解我会简单地介绍一些核心概念:

var MyModules = (function Manager() {
  var modules = {};
  function define(name, deps, impl) {
    for (var i=0; i<deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply( impl, deps );
  }
  function get(name) {
    return modules[name];
  }
  return {
    define: define,
    get: get
  };
})();

这段代码的核心是 modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管理的模块列表中。

基于函数的模块并不是一个能被稳定识别的模式(编译器无法识别),它们的 API 语义只有在运行时才会被考虑进来。因此可以在运行时修改一个模块 的 API。

相比之下,ES6 模块 API 更加稳定(API 不会在运行时改变)。由于编辑器知道这一点,因此可以在(的确也这样做了)编译期检查对导入模块的 API 成 员的引用是否真实存在。如果 API 引用并不存在,编译器会在运行时抛出一个或多个“早期”错误,而不会像往常一样在运行期采用动态的解决方案。

# CommonJS模块规范

Node采用CommonJS规范。

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是 私有 ,其他文件不可见。

每个模块内,module 变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX; 

通过module.exports输出变量x和函数addX。

# require方法用于加载模块

var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6 

导出多个内容时

//a.js
function add(a,b){
	return a+b
} 
function result(a,b){
	return a*b
} 
module.exports={add,result}
// ------------------------
//b.js
const {add,result}=require("./a.js");
let s=add(1,3);
let s1=result(2,4)
console.log(s)//4
console.log(s1)//8

# exports 与 module.exports

exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系

exports = function(x) {console.log(x)};
//上面这样的写法是无效的,因为exports不再指向module.exports了。

//可以成功导出
exports.hello = function() {
  return 'hello';
};
-----------------------
exports.hello = function() {
  return 'hello';
};
module.exports = 'Hello world';
//上面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。只能打印出Hello World

//如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能用module.exports输出。
module.exports = function (x){ console.log(x);};

一个JS文件,可以向外exports无数个变量、函数。但是require的时候,仅仅需要require这个JS文件一次。使用的它的变量、函数的时候,用点语法即可。所以,无形之中,增加了一个顶层命名空间。

可以将一个JS文件中,描述一个类。用

module.export = 构造函数名;

的方式向外暴露一个类。

也就是说,js文件和js文件之间有两种合作的模式:

1)某一个js文件中,提供了函数,供别人使用。 只需要暴露函数就行了; exports.msg=msg;

2)某一个js文件,描述了一个类。 module.exports = People;(或者是单一值时)

function People(name,sex,age){
    this.name = name;
    this.sex = sex;
    this.age = age;
}

People.prototype = {
    sayHello : function(){
        console.log(this.name + this.sex + this.age);
    }
}

//此时,People就被视为构造函数,可以用new来实例化了。
module.exports = People;
var People = require("./test/People.js");
var xiaoming = new People("小明","男","12");
xiaoming.sayHello();

# ES6模块规范

一个模块就是一个独立的 JavaScript 文件,如果要读取文件内的变量、函数或类(ES6 新增的概念),那么必须先将它们用 export 关键字导出,因为它们默认都是私有的。

ES6使用 export 和 import 来导出、导入模块。export 和 import 语句都是静态的,无法动态导出和导入。因此只能出现在模块的顶层作用域中,而不能出现在块级或函数作用域中.

由于 ES6 中的模块被设计成了静态的,因此需要在编译阶段就明确模块之间的依赖关系,而不是在运行过程中动态计算,代码如下所示,将模块路径设为变量或表达式都是错误的写法。

let path = "./1.js";
export * from path; //变量
export * from "./" + "1.js"; //表达式
import * as people from path; //变量
import * as people from "./" + "1.js"; //表达式
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year}; 

需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

// 写法一
export var m = 1;

// 写法二
var m = 1;
export {m};

// 写法三
var n = 1;
export {n as m}; 

# export default 命令

使用export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}

提示

通过export方式导出,在导入的时候需要加{},export default不需要在导入的时候加{}

// 第一组
export default function car() { // 输出
  // ...
}

import car from 'car'; // 输入

// 第二组
export function car2() { // 输出
  // ...
};

import {car2} from 'car2'; // 输入

完整的四种方案导出

let name = "strick";
export default name; //写法一
export default function getName() { //写法二
 return "strick";
}
export default function() { //写法三
 return "strick";
}
export { name as default }; //写法四
import name from "./1.js"; //写法一
import name, { age } from "./1.js"; //写法二
import name, * as people from "./1.js"; //写法三
import { default as myName } from "./1.js"; //写法四

# import加载

  1. 多次重复导入也只执行一次,import 语句在内部实现了单例模式
import 'lodash';
import 'lodash';
  1. 可动态加载,有明显的性能优势
import('/modules/myModule.mjs')
 .then((module) => {
 // Do something with the module.
 });
  1. 复合写法 ,在同一个模块中,先引入再导出同一个模块可简写
export { foo, bar } from 'my_module';
// 《====》
import { foo, bar } from 'my_module';
export { foo, bar };

  1. 只读变量:用 import 导入的变量都是只读的,相当于为它添加了 const 限制,如果在模块中为其重新赋值,那么必会引起类型错误。想要更新导入的变量的值,有一种间接的实现办法,代码如下所示,先在要导出的 1.js 模块内定义 name 变量和 setName()函数。
export let name = "strick";
export function setName(str) {
 name = str;
}

然后在另一个模块中导入刚刚的两个成员,接着将新的 name 值通过 setName()函数传入到 1.js 模块内部进行更新,代码如下所示,name 变量最终输出的结果正是那个新值。

import { name, setName } from "./1.js";
console.log(name); //"strick"
setName("freedom");
console.log(name); //"freedom" 
  1. 成员提升:从模块中导入的成员默认都会被提升至当前模块作用域的顶部,类似于声明提升。因此,如下所示的代码写法都是正确的。
console.log(age);
getAge();
import { age, getAge } from "./1.js";
  1. 简洁导入:import 语句中的导入标识符和 from 关键字都是可选的,但要注意,只有当两者一起省略时,语句才能被正确执行,如下所示。
import "./jquery.js";

import懒加载

# 用script标签加载模块

在浏览器中,无论是以外部还是内联的方式嵌入模块文件,都需要将它的 type 属性设为“module”,代码如下所示。并且在加载模块时为了避免脚本阻塞,会自动应用布尔属性 defer,即 HTML 文档的解析和模块文件的下载是同时进行的,待到解析完后才会执行模块。

<script src="1.js" type="module"></script>
<script type="module">
 import { name } from "./1.js";
 console.log(name);
</script>
<script src="2.js" type="module"></script>

上面代码会被依次执行,先执行第一个外部模块,再执行内联模块,最后执行第二个外部模块。注意,在每个模块中用 import 导入的其他模块也会被解析和下载,并且同一个模块每次只能被加载一次。

# 为什么模块化

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

原文地址 (opens new window)

最后更新: 11/21/2024, 2:37:03 PM