命名空间
在 ECMAScript 2015 之前,JavaScript 语言没有内置的模块支持。在 JavaScript 程序中,通常使用 命名空间 来组织并隔离代码以免产生命名冲突等问题。最为流行的实现命名空间的方法是使用立即执行的函数表达式。这是因为立即执行的函数表达式能够创建出一个新的作用域并且不会对外层作用域产生影响。
下例中,使用立即执行的函数表达式定义了两个命名空间,在这两个命名空间中定义的变量 x
不会相互冲突。示例如下:
(function() {
const x = 0;
})();
(function() {
const x = { message: 'hello world' };
})();
TypeScript 利用了这个经典的命名空间实现方式并提供了声明命名空间的简便语法。
命名空间声明
命名空间通过 namespace
关键字来声明,它相当于一种语法糖。示例如下:
namespace Utils {
function isString(value: any) {
return typeof value === 'string';
}
}
此例中,我们声明了一个名为 Utils
的命名空间。这段 TypeScript 代码在编译后将生成如下 JavaScript 代码:
// output.js
"use strict";
var Utils;
(function (Utils) {
function isString(value) {
return typeof value === 'string';
}
})(Utils || (Utils = {}));
我们能够看到命名空间被转换成了立即执行的函数表达式。
在定义命名空间的名字时允许使用以点符号 . 分隔的名字,这与其他编程语言中的命名空间声明类似。示例如下:
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 Outer {
namespace Inner {
const a = 0;
type Nullable<T> = T | undefined | null;
interface Point {
x: number;
y: number;
}
class Cat {
name: string;
}
function f(p: Point) {
console.log(p.x);
}
}
}
导出命名空间内的声明
默认情况下,在命名空间内部的声明只允许在该命名空间内部使用,在命名空间之外访问命名空间内部的声明会产生错误。示例如下:
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');
此例中,我们使用 export
关键字导出了 isString
函数声明。因此,在 Utils
外部也可以使用 isString
函数。此例中的代码生成的 JavaScript 代码如下所示:
// output.js
var Utils;
(function (Utils) {
function isString(value) {
return typeof value === 'string';
}
Utils.isString = isString;
isString('yes');
})(Utils || (Utils = {}));
Utils.isString('yes');
在访问导出的命名空间声明时,需要使用命名空间名和导出声明名并用点符号连接,这类似于对象属性访问的语法。
别名导入声明
我们可以使用 import
语句为命名空间的导出声明起一个别名。当命名空间名字比较长时,使用别名能够有效地简化代码。示例如下:
namespace Utils {
export function isString(value: any) {
return typeof value === 'string';
}
}
namespace App {
import isString = Utils.isString;
isString('yes');
Utils.isString('yes');
}
此例中,在 App
命名空间中为从 Utils
命名空间中导出的 isString
函数声明设置了一个别名 isString
,这样就可以像第 10 行一样使用 isString
来引用 Utils.isString
函数,而不必像第 12 行那样写出完整的访问路径。
别名导入本质上 相当于 新声明了一个变量并将导出声明赋值给该变量。例如,上例中的代码编译后生成的 JavaScript 代码如下所示:
"use strict";
var Utils;
(function (Utils) {
function isString(value) {
return typeof value === 'string';
}
Utils.isString = isString;
})(Utils || (Utils = {}));
var App;
(function (App) {
var isString = Utils.isString; // 别名导入声明
isString('yes');
Utils.isString('yes');
})(App || (App = {}));
需要注意的是,别名导入只是相当于新声明了一个变量而已,实际上不完全是这样的,因为别名导入对类型也有效。示例如下:
namespace Utils {
export interface Point {
x: number;
y: number;
}
}
namespace App {
import Point = Utils.Point;
const p: Point = { x: 0, y: 0 };
}
此例中的代码编译后生成的 JavaScript 代码如下所示:
// output.js
"use strict";
var App;
(function (App) {
const p = { x: 0, y: 0 };
})(App || (App = {}));
在多文件中使用命名空间
在实际工程中,代码不可能都放在同一个文件中,一定会拆分到不同的源代码文件。我们也可以将同一个命名空间声明拆分到不同的文件中,TypeScript 最终会将同名的命名空间声明合并在一起。例如,在如下两个文件中声明了同名的命名空间。
a.ts
文件的内容如下:
namespace Utils {
export function isString(value: any) {
return typeof value === 'string';
}
export interface Point {
x: number;
y: number;
}
}
b.ts
文件的内容如下:
namespace Utils {
export function isNumber(value: any) {
return typeof value === 'number';
}
}
最终,合并后的 Utils
命名空间中存在三个导出声明 isString
、isNumber
和 Point
。
文件间的依赖
当我们将命名空间拆分到不同的文件后,需要注意文件的加载顺序,因为文件之间可能存在依赖关系。例如,有两个拆分后的文件 a.ts
和 b.ts
。
a.ts
文件的内容如下:
namespace App {
export function isString(value: any) {
return typeof value === 'string';
}
}
b.ts
文件的内容如下:
namespace App {
const a = isString('foo');
}
这两个文件中,b.ts
依赖于 a.ts
。因为 b.ts
中调用了 a.ts
中定义的方法。我们需要保证 a.ts
先于 b.ts
被加载,否则在执行 b.ts
中的代码时将产生 isString
未定义的错误。
定义文件间的依赖关系有多种方式,本节将介绍以下两种:
-
使用
tsconfig.json
文件。 -
使用三斜线指令。
tsconfig.json
通过 tsconfig.json
配置文件能够定义文件间的加载顺序。例如,通过如下的配置文件能够定义 a.ts
先于 b.ts
被加载,这里我们主要配置了 outFile
和 files
两个选项。示例如下:
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"outFile": "main.js"
},
"files": ["a.ts", "b.ts"]
}
首先,outFile
选项指定了编译后输出的文件名。在指定了该选项后,编译后的 a.ts
和 b.ts
文件将被合并成一个 main.js
文件。其次,files
选项指定了工程中包含的所有源文件。files
文件列表是有序列表,我们正是通过它来保证 a.ts
先于 b.ts
被加载。最终编译后输出的 main.js
内容如下:
"use strict";
// a.ts
var App;
(function (App) {
function isString(value) {
return typeof value === 'string';
}
App.isString = isString;
})(App || (App = {}));
// b.ts
var App;
(function (App) {
const a = App.isString('foo');
})(App || (App = {}));
由该输出文件能够看到 a.ts
位于 b.ts
之前,因此不会产生错误。
关于 tsconfig.json
的详细介绍请参考8.3节。
三斜线指令
三斜线指令是 TypeScript 早期版本中就支持的一个特性,我们可以通过它来定义文件间的依赖。三斜线指令的形式如下所示:
/// <reference path="a.ts" />
此例中的三斜线指令声明了对 a.ts
文件的依赖。
我们可以在 b.ts
中使用三斜线指令来声明对 a.ts
文件的依赖。
a.ts
文件的内容如下:
namespace App {
export function isString(value: any) {
return typeof value === 'string';
}
}
b.ts
文件的内容如下:
/// <reference path="a.ts" />
namespace App {
const a = isString('foo');
}
在使用了三斜线指令后,编译器能够识别出 b.ts
依赖于 a.ts
。在编译 b.ts
之前,编译器会确保先编译 a.ts
。就算在 tsconfig.json
配置文件的 files
选项中将 b.ts
放在了 a.ts
之前,编译器也能够识别出正确的依赖顺序。示例如下:
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"outFile": "main.js"
},
"files": ["b.ts", "a.ts"]
}
我们甚至都不需要在 files
选项中包含 a.ts
文件,只需要包含 b.ts
即可。因为在编译 b.ts
时,编译器将保证依赖的文件会一同被编译。示例如下:
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"outFile": "main.js"
},
"files": ["b.ts"]
}
使用以上两个例子中的 tsconfig.json
配置文件都能够得到正确且相同的输出文件 main.js
。示例如下:
"use strict";
// a.ts
var App;
(function (App) {
function isString(value) {
return typeof value === 'string';
}
App.isString = isString;
})(App || (App = {}));
// b.ts
/// <reference path="a.ts" />
var App;
(function (App) {
const a = App.isString('foo');
})(App || (App = {}));