构建 Typescript 知识体系(十一)-理解命名空间及声明合并

这是我参与更文挑战的第十七天,活动详情查看:更文挑战

在 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);
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享