模块化编程是一种软件设计方法,它强调将程序按照功能划分为独立可交互的模块。一个模块是一段可重用的代码,它将功能的实现细节封装在模块内部。模块也是一种组织代码的方式。一个模块可以声明对其他模块的依赖,且模块之间只能通过模块的公共API进行交互。在新的工程或代码中,应该优先使用模块来组织代码,因为模块提供了更好的封装性和可重用性。
自1996年JavaScript诞生到2015年ECMAScript 2015发布,在将近20年的时间里JavaScript语言始终缺少原生的模块功能。在这20年间,社区的开发者们设计了多种模块系统来帮助进行JavaScript模块化编程。其中较为知名的模块系统有以下几种:-
CommonJS是一个主要用于服务器端JavaScript程序的模块系统。CommonJS使用require语句来声明对其他模块的依赖,同时使用exports语句来导出当前模块内的声明。CommonJS的典型应用场景是在Node.js程序中。在Node.js中,每一个文件都会被视为一个模块。
CommonJS模块系统在服务器端JavaScript程序中取得了成功,但无法给浏览器端JavaScript程序带来帮助。主要原因有以下两点:
基于以上原因,CommonJS的设计者又进一步设计了适用于浏览器环境的AMD模块系统。AMD是“Asynchronous Module Definition”的缩写,表示异步模块定义。AMD模块系统不是将一个文件作为一个模块,而是使用特殊的define函数来注册一个模块。因此,在一个文件中允许同时定义多个模块。AMD模块系统中也提供了require函数用来声明对其他模块的依赖,同时还提供了exports语句用来导出当前模块内的声明。
虽然CommonJS模块和AMD模块有着紧密的内在联系和相似的定义方式,但是两者不能互换使用。CommonJS模块不能在浏览器中使用,AMD模块也不能在Node.js中使用。如果一个功能模块既要在浏览器中使用也要在Node.js环境中使用,就需要分别使用CommonJS模块和AMD模块的格式编写两次。UMD模块的出现解决了这个问题。UMD是“Universal Module Definition”的缩写,表示通用模块定义。一个UMD模块既可以在浏览器中使用,也可以在Node.js中使用。UMD模块是基于AMD模块的定义,并且针对CommonJS模块定义进行了适配。因此,编写UMD模块会稍显复杂。
(function(factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
} else if (typeof define === 'function' && define.amd) {
define(['require', 'exports'], factory);
}
})(function(require, exports) {
function add(x, y) {
return x + y;
}
exports.add = add;
});
在经过了将近10年的标准化设计后,JavaScript语言的官方模块标准终于确定并随着ECMAScript 2015一同发布。它就是ECMAScript模块,简称为ES模块或ESM。
ECMA-Script模块是正式的语言内置模块标准,而前面介绍的CommonJS、AMD等都属于非官方模块标准。在未来,标准的ECMAScript模块将能够在任何JavaScript运行环境中使用,例如浏览器环境和服务器端环境等。
实际上,在最新版本的Chrome、Firefox等浏览器上以及Node.js环境中已经能够支持ECMAScript模块。ECMAScript模块使用import和export等关键字来定义。
ECMAScript模块是JavaScript语言的标准模块,因此TypeScript也支持ECMAScript模块。在后面的介绍中,我们将ECMAScript模块简称为模块。
每个模块都拥有独立的模块作用域,模块中的代码在其独立的作用域内运行,而不会影响模块外的作用域(有副作用的模块除外,后文将详细介绍)。模块通过import语句来声明对其他模块的依赖;同时,通过export语句将模块内的声明公开给其他模块使用。
模块不是使用类似于module的某个关键字来定义,而是以文件为单位。一个模块对应一个文件,同时一个文件也只能表示一个模块,两者是一对一的关系。若一个TypeScript文件中带有顶层的import或export语句,那么该文件就是一个模块,术语为“Module”。
若一个TypeScript文件中既不包含import语句,也不包含export语句,那么该文件称作脚本,术语为“Script”。脚本中的代码全部是全局代码,它直接存在于全局作用域中。因此,模块中的代码能够访问脚本中的代码,因为在模块作用域中能够访问外层的全局作用域。
//导出变量
let a=1;
export a
//导出函数
export function (){}
//导出对象
export {
a
}
export default {}
//批量导入
import {a,b} from "./a.ts"
//给导入的变量起一个别名
import {f as F} from "./a.ts"
import defaultModule from 'xxx';
导出
module.exports.a=4
module.exports={
a
}
导入
require("./a.ts")
commonjs只能引入js文件,如果要引入ts文件,需要将ts文件编译成js文件。
在ECMAScript 2015之前,JavaScript语言没有内置的模块支持。在JavaScript程序中,通常使用“命名空间”来组织并隔离代码以免产生命名冲突等问题。
最为流行的实现命名空间的方法是使用立即执行的函数表达式。这是因为立即执行的函数表达式能够创建出一个新的作用域并且不会对外层作用域产生影响。
命名空间通过namespace关键字来声明,它相当于一种语法糖。
//使用namespace声明命名空间
namespace Share{
function square(){
return "hello";
}
}
在定义命名空间的名字时允许使用以点符号“.”分隔的名字,这与其他编程语言中的命名空间声明类似。
namespace System.Utils {
function isString(value: any) {
return typeof value === 'string';
}
}
此例中定义的命名空间相当于两个嵌套的命名空间声明,它等同于如下的代码:
namespace System {
export namespace Utils {
function isString(value: any) {
return typeof value === 'string';
}
}
}
命名空间和模块不能混用,不能在模块中使用命名空间,命名空间最好在全局环境中使用。
默认情况下,在命名空间内部的声明只允许在该命名空间内部使用,在命名空间之外访问命名空间内部的声明会产生错误。
namespace Utils {
function isString(value: any) {
return typeof value === 'string';
}
// 正确
isString('yes');
}
Utils.isString('no');
// ~~~~~~~~
// 编译错误!Utils中不存在isString属性
如果我们查看由此例中的TypeScript代码生成的JavaScript代码,那么就能够明白为什么这段代码会产生错误,示例如下:
// output.js
var Utils;
(function (Utils) {
function isString(value) {
return typeof value === 'string';
}
isString('yes');
})(Utils || (Utils = {}));
Utils.isString('no'); // 运行错误
通过分析生成的JavaScript代码能够发现isString仅存在于立即执行的函数表达式的内部作用域,在外部作用域不允许访问内部作用域中的声明。
如果想要让命名空间内部的某个声明在命名空间外部也能够使用,则需要使用导出声明语句明确地导出该声明。导出命名空间内的声明需要使用export关键字,示例如下:
namespace Utils {
export function isString(value: any) {
return typeof value === 'string';
}
// 正确
isString('yes');
}
// 正确
Utils.isString('yes');
命名空间中导出的成员不可以重复定义,同一个命名空间中不能出现两次同名的变量。