这是我参与更文挑战的第十七天,活动详情查看:更文挑战
在 JavaScript 中,命名空间能够有效地避免全局污染,只不过在 ES6 引入了模块系统后,命名空间就很少被提及了。但 TS 同样实现了此特性,尽管在 ES6 模块中,完全不需要考虑全局污染的问题,但如果要使用一些全局的类库,命名空间仍然是一个比较好的解决方案。
命名空间代码示例
a.ts
namespace Shape {
const pi = Math.PI;
export function cricle(r: number) {
return pi * r ** 2;
}
}
复制代码
这里声明一个 Shape
的命名空间, 在命名空间之内可以定义任意多个变量
这些变量只能在 Shape
的命名空间 下可见
如果想要成员在全局可见的话,就需要使用 export
将它们导出
随着程序的改不断扩张,命名空间可能变得越来越大,那么命名空间也是可以拆分的
命名空间不能和模块混用,命名空间最好在一个全局的环境中使用,
使用方式:编译成 js 文件,在 html
中通过 script
引入
b.ts
/// <reference path="a.ts" />
namespace Shape {
export function square(x: number) {
return x * x;
}
}
Shape.cricle(1);
Shape.square(1);
复制代码
被编译成 js 文件后
执行 tsc ./src/b.ts
命令查看被编译后的代码如下
var Shape;
(function (Shape) {
var pi = Math.PI;
function cricle(r) {
return pi * Math.pow(r, 2);
}
Shape.cricle = cricle;
})(Shape || (Shape = {}));
复制代码
命名空间的原理:
命名空间被编译成了一个立即执行函数
这个函数创建一个闭包,闭包之内有一些私有成员.被导出的成员会被挂载到全局变量下
编译器会把程序多个地方具有相同名称的声明合并为一个声明,
优点
把程序中散落各处的声明合并到一起,比如在程序中多个地方定义了同样名字的接口,那么在使用接口的时候就会对这个多处的定义同时具有感知能力,那么通过声明合并,就可以避免遗漏
接口声明合并
// 定义一个接口A
interface A {
x: number;
}
// 在定义一个同名接口A
interface A {
y: string;
}
/*
这个时候两个接口就会合并成一个接口
如果是两个全局的接口,并且接口不在一个文件中,也可以发生接口合并
*/
// 定义一个变量a,类型为A, 则变量a中就需要具备接口A中所有的成员
let a: A = {
x: 1,
y: "2",
};
复制代码
接口的成员
非函数成员
对于接口中非函数的成员,要求我们保证它的唯一性。如果不唯一的话,它的类型必须相同。
namespace Test2 {
interface A {
x: number;
y: number;
}
interface A {
/*
后续属性声明必须属于同一类型。属性“y”的类型必须为“number”,但此处却为类型“string”。ts(2717)
*/
y: string;
}
let a: A = {
x: 1,
/*
不能将类型“string”分配给类型“number”。ts(2322)
index.ts(29, 9): 所需类型来自属性 "y",在此处的 "A" 类型上声明该属性
*/
y: "2",
};
}
复制代码
解决
namespace Test2 {
interface A {
x: number;
y: string;
}
interface A {
y: string;
}
let a: A = {
x: 1,
y: "2",
};
}
复制代码
函数成员
每一个函数成员都会被声明成一个函数重载
namespace Test2 {
interface A {
x: number;
y: string;
foo(bar: number): number;
}
interface A {
y: string;
foo(bar: string): string;
foo(bar: number[]): number[];
}
let a: A = {
x: 1,
y: "2",
/*
接口A中实现了一个 重载列表,在实现的时候就需要指定一个更宽泛的类型
*/
foo(a: any) {
return a;
},
};
}
复制代码
函数重载需要注意函数声明顺序, 因为编译器会根据顺序进行匹配. 那么在声明合并的时候,这些顺序是如何确定的呢?
– 在接口的内部,按照书写的顺序执行
– 在接口之间,后面的接口会排在前面
namespace Test2 {
interface A {
x: number;
y: string;
foo(bar: number): number; // 执行顺序-3
}
interface A {
y: string;
foo(bar: string): string; // 执行顺序-1
foo(bar: number[]): number[]; // 执行顺序-2
}
let a: A = {
x: 1,
y: "2",
foo(a: any) {
return a;
},
};
}
复制代码
– 例外: 如果函数的参数是一个字符串字面量,那么这个声明就会被提升到整个函数声明的最顶部
namespace Test2 {
interface A {
x: number;
y: string;
foo(bar: number): number; // 执行顺序-5
foo(bar: "a"): number; // 执行顺序-2
}
interface A {
y: string;
foo(bar: string): string; // 执行顺序-3
foo(bar: number[]): number[]; // 执行顺序-4
foo(bar: "a"): string; // 执行顺序-1
}
let a: A = {
x: 1,
y: "2",
foo(a: any) {
return a;
},
};
}
复制代码
命名空间合并
a.ts
namespace Shape {
const pi = Math.PI;
export function cricle(r: number) {
return pi * r ** 2;
}
}
复制代码
b.ts
namespace Shape {
export function square(r: number) {
return r * r;
}
}
复制代码
在两个文件中定义了 Shape 命名空间, 这两个命名空间就会发生合并,
在命名空间中导出的成员是不能重复定义的( 与接口的实现相反,因为接口是可以重复定义的 )
命名空间与函数的合并
function Lib() {}
// 相当于给函数增加了一个属性
namespace Lib {
export let version = "1.0";
}
console.log(Lib.version);
复制代码
命名空间与类的合并
class C {}
// 相当于给类添加静态属性
namespace C {
export let state = 1;
}
console.log(C.state);
复制代码
命名空间与枚举的合并
enum Color {
Red,
Yellow,
Blue,
}
// 相当于给枚举增加了一个方法
namespace Color {
export function mix() {}
}
console.log(Color);
复制代码
命名空间在与函数或 类进行声明合并的时候,一定要放在函数定义 或者类定义的后面,
在枚举合并中确没有此问题
原因:
错误示例:
// 命名空间声明不能位于与之合并的类或函数前ts(2434)
namespace C {
export let state = 1;
}
class C {}
console.log(C.state);
复制代码