Typescript 实战 --- (10)命名空间和模块
1、命名空间
ts 中的 “命名空间” 就是之前的 “内部模块”,任何使用 module 关键字来声明一个内部模块的地方都应该使用 namespace 关键字来替换
// ts 中的“内部模块” (废弃) module X { } // ts 中的“命名空间” (推荐) namespace X { }
(1)、export 关键字
使用 export 关键字修饰需要在命名空间之外访问的成员如:接口和类
// a.ts namespace Shape { let pi = Math.PI; export function circle(r: number): number { return pi *r * r; } } Shape.circle(2); // 变量 pi 是实现的细节,不需要导出,因此在命名空间之外是不能访问的 Shape.pi; // Error: 类型“typeof Shape”上不存在属性“pi”
(2)、分离到多文件
多个文件可以共享同一个命名空间以便于维护,尽管是不同的文件,但仍是同一个命名空间。在使用的时候就如同这些文件是在一个文件中定义的一样
// b.ts namespace Shape { export function square(width: number) { return width ** 2; } }
(3)、三斜线指令
/// <reference path="..." />
用于声明文件之间的依赖关系,仅可放在包含它的文件最顶端,三斜线指令的前面只能出现单行或多行注释;如果出现在一个语句或声明之后,就会被当作普通的单行注释,并且不具有特殊的含义。
上例中,a.ts 和 b.ts 共享同一个命名空间 Shape,在 b.ts 中可以访问到 a.ts 的 circle方法,但是执行的时候会报错
这个时候就需要使用三斜线指令在 b.ts 中引用 a.ts
// b.ts /// <reference path="./a.ts" /> namespace Shape { export function square(width: number) { return width ** 2; } } console.log(Shape.circle(2));
编译结果如下:
(4)、别名
可以使用 import q = x.y.z 给常用的对象起一个短的名字
namespace Shape { let pi = Math.PI; export function circle(r: number): number { return pi *r * r; } } import circle = Shape.circle; console.log(circle(1)); // 3.141592653589793
2、声明合并
所谓 “声明合并” 就是编译器会把程序中多个地方具有相同名称的声明合并为一个声明,好处是可以把程序中散落各处的重名声明合并到一起
2-1、合并接口
// a.ts interface A { name: string; } interface A { age: number; } let a: A = { name: 'Kobe', age: 41 } console.log(a); // { name: 'Kobe', age: 41 }
如果是全局模块,接口A的声明合并甚至可以在不同的文件中,例如新建一个 b.ts 也声明一个接口A,原来a.ts中的变量a就不能正确输出了
// b.ts interface A { gender: string; }
2-1-1、接口的非函数成员
接口中的非函数成员必须保持唯一性,如果不唯一的话,它们的类型必须相同
// a.ts interface A { name: string; age: number; } interface A { name: number; age: number; } // 两个接口中,成员age的类型相同,成员name的类型不相同,编译器提示报错
2-1-2、接口的函数成员
接口中的每一个函数成员都会被声明成 函数重载
// a.ts interface A { name: string; foo(x: number): number; } interface A { age: number; foo(x: string): string; foo(x: number[]): number[]; } // 实现接口A时,函数foo需要返回一个 any类型 let a: A = { name: 'Kobe', age: 41, foo(x: any) { return x } }
函数重载的时候,需要注意函数声明的顺序,因为编译器会按顺序进行匹配。那么,在接口合并时,这些顺序是如何确定的呢?
(1)、在接口的内部,按照书写的顺序来确定;
// a.ts interface A { age: number; foo(x: string): string; // 1 foo(x: number[]): number[]; // 2 }
(2)、接口之间,后面的接口排在前面
// a.ts interface A { name: string; foo(x: number): number; // 3 } interface A { age: number; foo(x: string): string; // 1 foo(x: number[]): number[]; // 2 }
(3)、如果一个函数的参数是一个字符串字面量,这个声明将会提升到整个函数声明的最顶端
// a.ts interface A { name: string; foo(x: number): number; // 5 foo(x: 'a'): string; // 2 } interface A { age: number; foo(x: string): string; // 3 foo(x: number[]): number[]; // 4 foo(x: 'b'): string; // 1 }
2-2、合并命名空间
(1)、命名空间中导出的成员是不可以重复定义的,这一点与合并接口不同
// a.ts namespace Shape { const pi = Math.PI; export function square(r: number) { return pi * r ** 2; } } // b.ts namespace Shape { export function square(width: number) { return width * width; } }
(2)、合并命名空间和函数 --- 相当于给函数添加了属性
// a.ts function Lib() {} namespace Lib { export let str = 'hello world'; } console.log(Lib.str); // hello world
(3)、合并命名空间和类 --- 相当于给类添加了静态属性
// a.ts class C {} namespace C { export let str = 'hello world'; } console.log(C.str); // hello world
注意:命名空间与函数或者类合并时,必须要放在函数或者类的后面,否则会报错
2-3、合并枚举
合并命名空间与枚举,相对于给枚举添加了属性。同时需要注意,命名空间与枚举的位置不受限制
// a.ts // 注意命名空间与枚举的位置 namespace Color { export function mix() {} } enum Color { Red, Green, Blue } console.log(Color);