跳至主要內容

命名空间

樱桃茶大约 16 分钟

命名空间

命名空间是 TypeScript 提供的一种组织代码的方式。在 JavaScript 中,我们经常会把相关的函数、类和其他资源放在一起。但随着代码库的增长,不同的部分可能需要相同的函数或类的名称,这就引入了全局命名冲突的风险。TypeScript 的命名空间帮助我们解决了这个问题。

  • 命名空间是使用 namespace 关键字声明的。
  • 它们可以将代码包裹在一个具有特定名称的容器内,防止命名冲突。
  • 在一个命名空间内部定义的变量、函数、类等,不会与其他命名空间中的同名项冲突。
  • 使用时,你可以通过 命名空间名.成员名 的形式来访问命名空间内的成员。

例如:

namespace FirstNamespace {
  export class ExampleClass {
    doSomething() {
      console.log("Doing something...");
    }
  }
}

namespace SecondNamespace {
  export class ExampleClass {
    doSomethingElse() {
      console.log("Doing something else...");
    }
  }
}

let firstExample = new FirstNamespace.ExampleClass();
firstExample.doSomething(); // 输出: 'Doing something...'

let secondExample = new SecondNamespace.ExampleClass();
secondExample.doSomethingElse(); // 输出: 'Doing something else...'

在上面的例子中,即使两个命名空间都有一个叫做 ExampleClass 的类,由于它们被包裹在不同的命名空间内,所以没有发生冲突,并且可以通过指定命名空间的前缀来区分它们。

  • 如果要在外部使用命名空间中的项目,必须使用 export 关键字对其进行导出。
  • 命名空间也可以嵌套,内部命名空间需要使用 export 才能从外部命名空间访问。

例如:

namespace OuterNamespace {
  export namespace InnerNamespace {
    export function innerFunction() {
      console.log("Inside the inner function.");
    }
  }
}

OuterNamespace.InnerNamespace.innerFunction(); // 输出: 'Inside the inner function.'
  • TypeScript 还允许使用 /// <reference path="..." /> 指令来告诉编译器文件之间的依赖关系,在编译过程中合并多个文件。然而,在现代模块系统(如 ES6 modules)的背景下,这种做法逐渐不被推荐,建议使用模块导入导出 (import/export) 代替。

命名空间提供了一种避免全局命名冲突的方法,但随着 ES6 模块系统的普及,使用模块化构建应用更加常见。尽管如此,理解命名空间对于维护旧的 TypeScript 代码或者在某些特定情况下可能仍然有用。

第一步

命名空间在 TypeScript 中是一种组织代码的方式,允许你将相关功能和接口分组到一个具有独特名字的容器中。这样做可以帮助防止全局作用域的污染,并且当你的应用程序变得越来越大时,它们能够提供一个很好的结构化方法。

  • 命名空间被定义为一个拥有特定名称的代码块,使用namespace关键字。

    namespace MyNamespace {
      export const someValue = 123;
      export function hello() {
        console.log("Hello from MyNamespace!");
      }
    }
    
  • 在命名空间内部,你可以导出(export)函数、变量、类等,使它们能在命名空间外部被访问。不导出的成员将保持私有,只能在命名空间内部使用。

  • 要在命名空间外部访问其内部的成员,你需要使用namespace.member的形式:

    MyNamespace.hello(); // 输出:Hello from MyNamespace!
    console.log(MyNamespace.someValue); // 输出:123
    
  • 命名空间可以嵌套,即一个命名空间内部可以包含另一个命名空间:

    namespace OuterNamespace {
      export namespace InnerNamespace {
        export function innerFunction() {
          console.log("Hello from InnerNamespace!");
        }
      }
    }
    
    OuterNamespace.InnerNamespace.innerFunction(); // 输出:Hello from InnerNamespace!
    
  • 在不同文件中定义的命名空间可以合并,如果它们具有相同的名字。这通常通过在多个文件中重复声明同一个命名空间来实现:

    // File1.ts
    namespace SharedNamespace {
      export function funcFromFirstFile() {
        console.log("This function is declared in the first file.");
      }
    }
    
    // File2.ts
    namespace SharedNamespace {
      export function funcFromSecondFile() {
        console.log("This function is declared in the second file.");
      }
    }
    
    // 跨文件访问
    SharedNamespace.funcFromFirstFile();
    SharedNamespace.funcFromSecondFile();
    
  • 使用命名空间时需要注意模块化和代码组织。随着 ES6 模块(使用 import/export 语法)的普及,命名空间的使用变得不那么频繁,因为模块本身就提供了防止全局作用域污染和代码组织的机制。在现代 TypeScript 项目中,更推荐使用模块而不是命名空间。

所有的验证器都放在一个文件里

命名空间中的“所有的验证器都放在一个文件里”这个概念,是 TypeScript 为了组织代码和避免全局作用域污染提出的一种解决方案。这意味着你可以把相关的类、接口、函数等放在一个封闭的空间内,而不是散落在全局作用域中。这样做有助于避免命名冲突,并且使得代码结构更加清晰。

  • 命名空间实际上是一个"大箱子",在这个箱子里,你可以放任何东西,比如类(class)、接口(interface),甚至是其他命名空间。
  • 使用namespace关键字来定义一个命名空间。

例子:

假设我们正在开发一个应用,需要多种类型的验证器,我们可以将所有这些验证器放在一个叫Validation的命名空间里:

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);
    }
  }
}
  • 在这个例子中,我们定义了一个Validation命名空间,里面包含了一个StringValidator接口和两个实现了该接口的类:LettersOnlyValidatorZipCodeValidator
  • 我们使用export关键字来导出接口和类,使它们在命名空间外部也可访问。
  • lettersRegexpnumberRegexp是正则表达式,用于字符串验证,它们没有被导出,因此它们只在Validation命名空间内部可见。

通过使用命名空间,我们能够将相关功能紧密地组织在一起,同时避免全局作用域的污染。这种方式特别适合于当你的应用程序规模较大,或者当你想要在多个项目之间复用一些代码时。

命名空间

命名空间在 TypeScript 中是一种组织代码的方式,它可以帮助你将相关的接口、类、函数等分组到一个高层次的名字下面。这样做可以防止全局命名冲突,并且有助于模块化和重构。

  • 命名空间是通过namespace关键字定义的。
  • 在命名空间内部,你可以声明任何数量的接口、类、函数等,就像在常规的全局作用域里一样。
  • 命名空间可以嵌套,即一个命名空间内可以包含另一个命名空间。

下面是一些实用例子:

// 定义一个命名空间
namespace MyNamespace {
  // 在命名空间内定义一个接口
  export interface MyInterface {
    doSomething(): void;
  }

  // 在命名空间内定义一个类,实现上面的接口
  export class MyClass implements MyInterface {
    public doSomething() {
      console.log("Doing something...");
    }
  }

  // 命名空间内还可以定义函数
  export function myFunction() {
    console.log("Hello from MyNamespace!");
  }
}

// 使用命名空间中定义的类
const myClassInstance = new MyNamespace.MyClass();
myClassInstance.doSomething();

// 调用命名空间中的函数
MyNamespace.myFunction();

在这个例子中,MyNamespace 是我们创建的命名空间。我们使用了 export 关键字来导出接口、类和函数,使它们可以在命名空间外部访问。在命名空间外部,我们可以通过 MyNamespace.MyClass 来创建类的实例,或者调用 MyNamespace.myFunction() 来执行函数。

  • 命名空间最佳实践建议不要过度使用,因为随着 ES6 模块的推广,模块系统已经能够很好地解决大多数命名空间解决的问题。
  • 使用模块时,每个文件本身就是一个模块,我们可以通过importexport来导入或导出特定的接口、类、函数等。
  • TypeScript 的命名空间和模块不应该混合使用,尽量根据项目需求选择其中一种组织代码的方式。

使用命名空间的验证器

在 TypeScript 中,使用命名空间的验证器是指利用命名空间(Namespaces)来组织代码,特别是对于验证功能或模块。命名空间可以帮助你将相似的接口、类、函数等分组在一起,这样做不仅可以避免全局作用域污染,还能使代码结构更清晰、更易于管理和使用。以下是关于如何使用命名空间进行验证器开发的一些实践示例:

  • 创建命名空间: 使用namespace关键字定义一个新的命名空间。
namespace Validation {
  // 在这里定义相关的接口、类等
}
  • 定义验证接口和类: 在命名空间内部定义验证逻辑相关的接口或类。通常,你可能需要一个接口来描述验证器应该具备的方法,然后实现具体的验证类。
namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && /^[0-9]+$/.test(s);
    }
  }

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return /^[a-zA-Z]+$/.test(s);
    }
  }
}
  • 使用export关键字导出: 要在命名空间外部使用其内部定义的类型(接口、类等),需要使用export关键字。这样,外部代码就可以通过命名空间访问到这些类型了。

  • 引用命名空间中的类: 当需要使用命名空间中的验证器时,直接通过命名空间加类名的方式引用即可。

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

// 测试字符串
let strings = ["Hello", "98052", "101"];

// 使用验证器进行验证
for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${
        validators[name].isAcceptable(s) ? "matches" : "does not match"
      } ${name}`
    );
  }
}
  • 组织代码和避免冲突: 命名空间最大的好处之一是帮助你组织代码,并避免名称冲突。特别是在大型项目或多人协作项目中,通过给不同的功能或模块设置不同的命名空间,可以有效地减少命名冲突的可能性。

通过以上步骤,你可以看到如何在 TypeScript 中使用命名空间来创建、组织和使用验证器。这种方法有助于保持代码的清晰和有序,同时提高了代码的可重用性和可维护性。

分离到多文件

命名空间是 TypeScript 中用来组织代码和避免命名冲突的一个特性。当你的代码库变得很大,或者当你在多个文件中使用相同的变量名时,命名空间就变得非常有用。但是,当你想要将一个命名空间分散到多个文件中时,就需要了解如何将它们分离并正确地引入到你的项目中。

  • 在 TypeScript 中,可以使用/// <reference path="..." />指令来告诉编译器文件之间的关系。
  • 当你将一个命名空间分割成多个文件时,每个文件将包含该命名空间的一部分。这些文件都被视为同一个命名空间的不同段。
  • 使用export声明来导出命名空间中要在其他文件中访问的部分,而不是整个命名空间。
  • 在一个入口文件中,使用/// <reference path="..." />指令来引用所有其他文件,然后只需要在 HTML 页面中或在构建脚本中引用这个入口文件。

举几个实例:

假设你有一个工具库,你希望把相关的函数按照功能分散在不同的文件中。

utils/validation.ts

namespace Utils {
  export function validateText(input: string): boolean {
    // ...验证文本
    return true; // 假设验证总是成功
  }
}

utils/numbers.ts

namespace Utils {
  export function calculateSum(numbers: number[]): number {
    // ...计算总和
    return numbers.reduce((a, b) => a + b, 0);
  }
}

main.ts

/// <reference path="utils/validation.ts" />
/// <reference path="utils/numbers.ts" />

let isValid = Utils.validateText("Some text");
let sum = Utils.calculateSum([1, 2, 3, 4]);

console.log(isValid); // true
console.log(sum); // 10

在上面的例子中,我们首先定义了两个分别位于utils/validation.tsutils/numbers.ts的文件,其中Utils命名空间的不同部分被实现。最后,在main.ts中通过reference指令把这两个文件链接起来,并使用Utils命名空间中的函数。

多文件中的命名空间

命名空间在 TypeScript 中用来组织代码,避免全局命名冲突。当命名空间跨越多个文件时,需要正确地管理它们以确保代码可以正常工作。

  • 多文件中的命名空间:在大型项目中,你可能希望将一个大的命名空间分散到多个文件中,以提高代码的可管理性。在 TypeScript 中,可以通过/// <reference path="..." />指令将多个文件联结起来,形成一个统一的命名空间。

  • 例子:假设有一个命名空间MyNamespace,分布在两个文件中:file1.tsfile2.ts

    • file1.ts:

      namespace MyNamespace {
        export class MyClass1 {
          doSomething() {
            console.log("Doing something...");
          }
        }
      }
      
    • file2.ts:

      /// <reference path="file1.ts" />
      namespace MyNamespace {
        export class MyClass2 {
          callAnotherFunction() {
            let myClass1 = new MyClass1();
            myClass1.doSomething();
          }
        }
      }
      

      file2.ts中,我们使用了/// <reference path="file1.ts" />指令来引用file1.ts中的内容。这使得file2.ts能够访问file1.ts中定义的MyNamespace命名空间及其成员。

  • 编译多文件命名空间:当使用命名空间跨多个文件时,编译这些文件需要额外的注意。你可以手动编译每个文件,但更方便的做法是使用一个 tsconfig.json 文件,列出所有相关的文件,并使用tsc(TypeScript 编译器)一次性编译整个项目。

  • 注意事项

    • 确保在 HTML 页面或最终的 JavaScript 文件中按照正确的顺序引入编译后的 JavaScript 文件。
    • 当在多个文件中使用相同的命名空间时,TypeScript 默认会把它们视为同一个命名空间的不同部分,允许跨文件访问类、接口等。

通过将命名空间分布在多个文件中,可以使得代码结构更清晰、更易于管理,特别是在大型项目中。但同时也要小心处理好文件之间的依赖关系和编译配置。

别名

命名空间别名在 TypeScript 中,是用来简化较长的命名空间或模块路径的方法。当你有一个在嵌套的命名空间或者模块中定义的类型或者值,并且你需要在另一个地方频繁地引用它时,使用别名可以让代码更加清晰易读。下面通过几个实例来解释如何使用命名空间别名:

  • 使用 import 关键词为命名空间或模块定义别名:

    namespace Shapes {
      export namespace Polygons {
        export class Triangle {}
        export class Square {}
      }
    }
    
    import polygons = Shapes.Polygons;
    let square = new polygons.Square();
    

    在上面的例子中,Shapes.Polygons 路径被赋予了 polygons 这个别名,之后就可以通过 polygons.Square 来创建一个 Square 实例,而不需要完整的 Shapes.Polygons.Square 路径。

  • 使用别名简化模块导入路径:

    如果你正在使用模块系统(比如 CommonJS 或 ES6 模块),也可以给导入的模块创建别名来简化访问路径。

    import * as LongModuleName from "./some/long/path/module.ts";
    
    let instance = new LongModuleName.SomeClass();
    

    在这个例子中,"./some/long/path/module.ts" 导出的所有内容都被赋予了 LongModuleName 这个别名,使得创建 SomeClass 实例时不需要重复冗长的模块路径。

通过使用命名空间别名,你可以在不牺牲可读性和维护性的前提下,更有效地组织和引用代码中的类型或值。这对于大型项目尤其有用,因为它们往往涉及多层嵌套的模块或命名空间。

使用其它的 JavaScript 库

命名空间中的“使用其他 JavaScript 库”部分涉及的是在 TypeScript 中使用那些原本为纯 JavaScript 编写的库。由于 TypeScript 是 JavaScript 的一个超集,它允许你使用所有的 JavaScript 代码和库。但是,为了能够充分利用 TypeScript 的类型检查等特性,最好是有相应的类型声明。

  • 类型声明文件

    • 当你在 TypeScript 项目中引入外部 JavaScript 库时,你需要告诉 TypeScript 这个库的结构,这样 TS 才能提供类型检查和代码提示。
    • 类型声明文件通常具有.d.ts扩展名。
    • 可以为已存在的 JavaScript 库编写自定义的类型声明文件,或者使用社区已经创建好的。
  • DefinitelyTyped

    • 是一个巨大的类型声明文件仓库,社区维护着许多流行 JavaScript 库的类型声明。
    • 可以通过npm安装这些声明文件,一般形式是@types/library_name(例如:@types/lodash)。
  • 示例

    • 假设你正在使用一个名为awesome-library.js的 JavaScript 库,在没有官方或 DefinitelyTyped 上的类型声明,你可以创建一个awesome-library.d.ts文件来声明库导出的函数或对象等。
    • // awesome-library.d.ts
      declare module "awesome-library" {
        function doSomethingAwesome(): void;
        interface AwesomeLibraryOptions {
          coolSetting: boolean;
        }
      }
      
    • 在你的 TypeScript 文件中就可以这样使用:
    • /// <reference path="awesome-library.d.ts" />
      import { doSomethingAwesome } from "awesome-library";
      
      doSomethingAwesome();
      
  • 注意事项

    • 当使用非 TypeScript 编写的库时,如果没有找到相应的类型声明文件,将无法享受到 TypeScript 的类型检查优势。
    • 如果确定不需要类型检查,可以将库导入为 any 类型,但这样会失去很多 TypeScript 的好处。
    • 创建类型声明文件可能需要深入理解所使用的 JavaScript 库的内部结构。

外部命名空间

外部命名空间(External Namespaces)

在 TypeScript 中,当你想要使用 JavaScript 库时,你可能会遇到的一个问题是如何在 TypeScript 项目中合理地组织和引用这些已有库的代码。尤其是对于那些并没有为 TypeScript 提供类型定义文件(.d.ts 文件)的旧库。这时,外部命名空间就成为了一个解决方案。

  • 为什么需要外部命名空间?

    • 许多 JavaScript 库是通过全局变量的形式被引入和使用的。在 TypeScript 中,我们希望能够以某种方式声明这些全局变量的类型,以便能够享受到类型检查和智能提示等特性。
    • 外部命名空间允许开发者为这些全局变量提供类型定义,而不需要修改原始库的代码。
  • 如何使用外部命名空间?

    假设有一个全局库 MyGlobalLib ,它有一个 doSomething() 方法和一个 version 属性。

// 声明一个外部命名空间 MyGlobalLib
declare namespace MyGlobalLib {
  function doSomething(): void;
  let version: string;
}

// 现在可以安全地使用 MyGlobalLib 的方法和属性,同时享受到类型检查
MyGlobalLib.doSomething();
console.log(MyGlobalLib.version);
  • 使用 declare namespace 关键字来声明一个外部命名空间。这告诉 TypeScript,“信任我,这个全局变量或库存在,并具有我所声明的结构和类型”。

  • declare namespace 内部,你可以像编写普通 TypeScript 代码一样添加函数、变量等成员的声明,但实际上并不会产生真正的 JavaScript 代码。这些仅仅是类型声明,用于编译时的检查。

  • 注意事项

    • 当你有权访问库的源代码时,或者该库已经提供了官方的类型定义文件,推荐使用模块导入(import ... from ...) 而不是外部命名空间。
    • 外部命名空间主要适用于处理旧的 JavaScript 库或那些没有提供相应 .d.ts 类型声明文件的库。
    • 对于流行的 JavaScript 库,你通常可以在 DefinitelyTyped 项目上找到现成的类型定义文件(.d.ts),这可以让你避免手动声明外部命名空间。

使用外部命名空间是处理老旧 JavaScript 代码库和项目在 TypeScript 中集成时的一个实用技巧。理解并掌握它,将帮助你更好地利用 TypeScript 的强大功能,同时保持与现有项目的兼容。