命名空间

在 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() { }
    }
}

使用命名空间的成员

  1. 导出并使用命名空间的成员

    在命名空间内部,你可以直接使用命名空间内的成员,但在命名空间外部无法直接使用命名空间的非导出成员。

    例如,以下代码声明了命名空间 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"
  2. 使用别名

在调用其他命名空间导出的成员前,使用 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 位于全局作用域中,这不仅可能导致命名污染,而且不利于识别出各个空间之间的依赖关系,尤其是在大型项目中。

无论是性能还是代码管理,模块都优于命名空间。因此,对于任何规模的项目,都建议全部使用模块,不要使用命名空间。