命名空间
在 ECMAScript 6 发布前,早期的 TypeScript 版本就提供了内部模块的功能,它主要用于组织代码,解决各个声明出现重名的问题。随着 ECMAScript 6 的发布,模块功能得到了 ECMAScript 官方的支持,TypeScript 的内部模块变得毫无必要,TypeScript 1.5 将内部模块更名为命名空间,并使用不同的关键字。命名空间是一个可能废弃的功能。
无论从性能还是从代码管理来说,模块都优于命名空间。无论在多大规模的项目中,都建议使用模块,不要使用命名空间。
但在实际项目中,可能会遇到遗留的命名空间代码,所以仍需要了解命名空间的用法。本节将对命名空间做简要介绍。
声明命名空间
命名空间使用 namespace 关键字声明,语法如下。
namespace 命名空间名称 {
//任意代码
}
例如,以下代码声明了一个名为 Space1 的命名空间。在该命名空间的作用域中,你可以编写任意代码,在本例中声明了一个数值变量 x。
namespace Space1 {
let x: number = 1;
}
各个命名空间具有独立的作用域,因此它们具有同名的声明也互不影响。例如,以下代码声明了两个命名空间,在命名空间及顶层代码中都声明了变量 a 和函数 test。
namespace SpaceA {
let a: number = 1;
function test() { };
}
namespace SpaceB {
let a: string = "";
function test(text: string) {
console.log(text);
}
}
let a: boolean = true;
function test(num1: number) { }
命名空间内支持嵌套声明命名空间。例如,以下代码声明命名空间 SpaceA,在该命名空间的内部声明了命名空间 SpaceB。
namespace SpaceA {
let a: number = 1;
namespace SpaceB {
function test() { }
}
}
使用命名空间的成员
-
导出并使用命名空间的成员
在命名空间内部,你可以直接使用命名空间内的成员,但在命名空间外部无法直接使用命名空间的非导出成员。
例如,以下代码声明了命名空间 Space1,它拥有一个变量 a 和一个函数 test(),其中 test() 函数在输出时使用了变量 a。由于在命名空间内部使用其成员,因此这段代码能正确编译、执行。在命名空间外的顶层代码中也使用了变量 a 和 test() 函数,由于在命名空间外部无法使用命名空间的非导出成员,因此会引起编译错误。
namespace Space1 { let a: string = "hello"; function test(text: string) { console.log(a + text); } } //编译错误:找不到名称"a"。ts(2304) console.log(a); //编译错误:找不到名称 "test"(2582) test("world");
在命名空间中,使用 export 关键字导出内部成员;在命名空间外部,通过 “命名空间.成员名称” 的方式使用这些导出成员。
例如,以下代码声明了命名空间 Space1,它拥有分别导出的变量 a 函数 test()。接下来,使用 Space1.a 和 Space1.test() 在其他地方访问这些成员,在顶层代码及命名空间 Space2 中访问这两个成员,并产生相应的输出。
namespace Space1 { export let a: string = "hello"; export function test(text: string) { console.log(a + text); } } namespace Space2{ console.log(Space1.a); //输出"hello" Space1.test("world"); //输出"helloworld" } console.log(Space1.a); //输出"hello" Space1.test("world"); //输出"helloworld"
在声明时,用 “命名空间1.命名空间2….命名空间n” 的方式可以一次性声明多级命名空间。
例如,以下代码中一次性声明了两个命名空间——命名空间 Space1 和子命名空间 Space2。
namespace Space1.Space2 { export let a: string = "hello"; } console.log(Space1.Space2.a); //输出"hello"
以上代码实际上相当于以下代码的简写形式。注意,Space2 命名空间的声明指定了 export 关键字,表示可以对外访问。
namespace Space1 { export namespace Space2 { export let a: string = "hello"; } } console.log(Space1.Space2.a); //输出"hello"
-
使用别名
在调用其他命名空间导出的成员前,使用 import 关键字将其重命名,后续直接使用该别名来调用它们,以简化嵌套层次和代码长度。
例如,以下代码定义了 3 个命名空间,这 3 个命名空间存在嵌套关系。在最里层的命名空间 Space3 中,分别声明一个变量 greeting 和一个接口 Person。
namespace Space1 {
export namespace Space2 {
export namespace Space3 {
export let greeting: string = "hello";
export interface Person { name: string };
}
}
}
如果不使用命名空间别名,则调用变量 greeting 和接口 Person 时,代码会显得冗长,示例代码如下。
console.log(Space1.Space2.Space3.greeting);
let person1: Space1.Space2.Space3.Person = { name: "Sam" };
使用命名空间别名后,情况将得到改善。例如,以下代码分别为变量 greeting 和接口 Person 指定别名,之后在使用它们时,代码将大幅精简。
import Person = Space1.Space2.Space3.Person;
import greeting = Space1.Space2.Space3.greeting;
console.log(greeting);
let person2: Person = { name: "Lily" };
在多文件中使用命名空间
在实际项目中,不同命名空间的代码通常会存放到不同的文件中,这些命名空间可能被其他文件引用。
在命名空间的功能最初推出时,并不存在 ECMAScript 6 规范,因此当时对于多个文件通常的处理办法是用多个 .ts
文件存放不同命名空间的代码,然后在使用这些命名空间的代码中加入编译指令,如三斜线指令。使用方式为 “/// <reference path="其他TypeScript文件路径" />”,这表示将当前文件编译成 JavaScript 时,将加入其他 TypeScript 文件一同编译,这样在编译时就会合并为一个 .js
文件。
例如,现在有 a.ts、b.ts 和 c.ts 三个文件。
a.ts 的文件内容如下。
namespace Space1 {
export let a: number = 1;
}
代码中声明了 Space1 命名空间,它拥有一个变量 a。
b.ts 的文件内容如下。
namespace Space2 {
export function test(text: string) { console.log(text); };
}
代码中声明了 Space2 命名空间,它拥有一个函数 test()。
如果 c.ts 文件需要使用 a.ts 和 b.ts 文件的命名空间,那么 c.ts 文件中需要增加对应的三斜线指令来引用它们。c.ts 的文件内容如下。
/// <reference path="a.ts" />
/// <reference path="b.ts"/>
console.log(Space1.a);
Space2.test("hello");
前两行代码使用了三斜线指令,表示在编译 c.ts 文件时,会同时将 a.ts 和 b.ts 文件的内容一起编译为 c.js。后两行代码分别使用 Space1 命名空间中的变量 a 和 Space2 命名空间中的函数 test()。
接下来,就可以编译 c.ts 文件了。在编译 c.ts 文件时,需要使用 --out 参数,这表示将当前文件及它引用的文件合并编译到指定位置。以下命令将 c.ts 文件及其引用的 a.ts 和 b.ts 文件合并编译到 bundle.js 文件中。
$ tsc c.ts --out bundle.js
打开 bundle.js 文件,其内容如下。
var Space1;
(function (Space1) {
Space1.a = 1;
})(Space1 || (Space1 = {}));
var Space2;
(function (Space2) {
function test(text) { console.log(text); }
Space2.test = test;
;
})(Space2 || (Space2 = {}));
/// <reference path="a.ts" />
/// <reference path="b.ts"/>
console.log(Space1.a);
Space2.test("hello");
可以看到代码中同时包含了 a.ts、b.ts 和 c.ts 文件的编译结果。
输出结果如下。
> 1
> hello
命名空间的本质与局限
下面介绍命名空间的本质与局限。以下 TypeScript 代码声明了一个名为 Space1 的命名空间,在它的内部不仅声明并导出了变量 a 和函数 test(),还声明了变量 b,但没有导出它。
namespace Space1 {
export let a: number = 1;
export function test() { }
let b: string = "hello";
}
将 TypeScript 代码编译为 JavaScript 代码后,结果如下。
var Space1;
(function (Space1) {
Space1.a = 1;
function test() { }
Space1.test = test;
var b = "hello";
})(Space1 || (Space1 = {}));
可以看到,命名空间本质上是一个普通的 JavaScript 对象,它通过立即执行函数为这个对象的各个属性赋值。例如,前面在命名空间 Space1 中声明并导出的变量 a 和函数 test() 实际上只是该对象的属性与方法,而未导出的变量 b 的值并未赋给 Space1 对象。由于变量 b 在立即执行函数中,因此它的作用域仅限于函数内。而命名空间 Space1 位于全局作用域中,这不仅可能导致命名污染,而且不利于识别出各个空间之间的依赖关系,尤其是在大型项目中。
无论是性能还是代码管理,模块都优于命名空间。因此,对于任何规模的项目,都建议全部使用模块,不要使用命名空间。