名字空间

状态: 初稿

对命名的说明:
有必要说明, 在 TypeScript 1.5 中, 一些术语发生了变化.
“内部模块” 现在叫做 “名字空间”.
“外部模块” 简称 “模块”, 这是出于同 ECMAScript 2015 的命名保持一致的考虑, (module X { 等同于现在提出的 namespace X {).

介绍

本章概述若干在 TypeScript 中用名字空间(以前叫 “内部模块”)组织你程序的方法.
我们”对命名的说明”提到, 现在用”名字空间”指代”内部模块”.
此外, 以往任何用到 module 关键字声明内部模块的地方, 都可以以及应当用 namespace 关键字替代.
我们不想灌输名字一样的术语弄昏新用户.

踏出第一步

首先, 给出将在通篇作为讨论对象的范例程序.
我们编写了一小批极简的字符串验证器, 验证器可以用来检验网页表单用户输入, 或来自外部的数据文件的格式.

单文件存放验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface StringValidator {
isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
}
}

创建名字空间

当越来越多的验证器添加进来, 我们希望它们之间有一定组织形式, 此后, 我们不仅能更高效地管理我们的类型, 也不用担心命名冲突.
所以不要把一大堆名字一股脑儿放进全局名字空间, 而应分散安置在不同名字空间中.

下例, 我们将所有与验证相关的实体移动到 Validation 名字空间.
因为我们想要这里的接口和类对外部可见, 所以它们的定义要以 export 打头.
相反地, lettersRegexpnumberRegexp 变量属于实现细节, 我们没导出它们, 进而对外部不可见.
文件末尾的测试代码表明, 名字空间的类型在外部使用, 需要对它的名字加以限定, 如 Validation.LettersOnlyValidator.

验证器收入名字空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}

跨文件拆分

随着应用程序不断扩大, 我们希望把源程序拆分为一个个小文件, 使其易于维护.

跨文件名字空间

这里, 我们把名字空间 Validation 拆分成多个文件.
虽然每个文件是物理独立的, 但它们实际上在向同一个名字空间贡献内容, 使用起来也像是定义在同一文件.
由于一个文件可能会依赖另一文件, 我们要添加引用标签向编译器揭示文件间的依赖关系.
我们的测试代码无需修改.

Validation.ts
1
2
3
4
5
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
LettersOnlyValidator.ts
1
2
3
4
5
6
7
8
9
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
ZipCodeValidator.ts
1
2
3
4
5
6
7
8
9
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
Test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}

一旦编译了多个文件, 就有必要确保能完整地加载所有编译后的文件.
我们介绍两种方法.

第一种, 合并编译, 指定 --outFile 选项将所有输入文件编译成单一的大 JavaScript 文件:

1
tsc --outFile sample.js Test.ts

编译器自动根据文件中的引用标签调整各输出文件相对顺序. 你也可以分别指定每个文件:

1
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

第二种, 分别编译 (默认), 为每个输入文件生成一个 JavaScript 文件.
得到多个 JS 文件以后, 我们需要在网页中插入 <script> 标签以适当顺序加载每个输出文件, 举例如下:

MyTestPage.html (片段)
1
2
3
4
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

别名

另一种可以减轻名字空间使用负担的方法是用 import q = x.y.z 语句为常用对象起一个更短的名字.
此处不应与模块加载语法 import x = require("name") 混淆, 此语句简单地为特定标识符创建一个别名.
你可以为任何标识符声明这类导入 (普遍作为别名引用), 包括自模块导入创建的对象.

1
2
3
4
5
6
7
8
9
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as 'new Shapes.Polygons.Square()'

注意, 我们没使用 require 关键字; 而是直接把要导入符号的全限定名称作为右值赋值.
类似 var, 但它同时对导入符号的类型或名字空间含义起作用.
特别地, 对于值, import 是来自源标识符的特别引用, 对 var 变量别名的修改不会反映到原始变量.

与其他 JavaScript 库协同工作

要描述非 TypeScript 库的形体, 我们需要声明该库暴露的 API.
由于多数 JavaScript 库只导出少许顶层对象, 名字空间是表示它们较好的一种选择.

我们称没有定义实现的声明为 “外部的”.
按照惯例, 把这些声明放置在 .d.ts 文件中.
如果你熟悉 C/C++, 它们相当于 .h 文件.
一起来看些例子.

外部名字空间

有名的 D3 库在一个叫 d3 的全局对象里定义它所有功能.
由于该库通过 <script> 标签加载 (而不是模块加载器), 它的声明文件采用名字空间描述其形体.
为使 TypeScript 编译器看到它的形体, 要使用外部名字空间声明.
例如, 我们可以这样开始:

D3.d.ts (摘要)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}

export interface Event {
x: number;
y: number;
}

export interface Base extends Selectors {
event: Event;
}
}

declare var d3: D3.Base;
如果这篇文章对您有用,可以考虑打赏:)