在TypeScript中使用namespace封装数据
#
在之前的
typescript module
文章中,我讲解了如何通过typescript的模块系统,将程序的代码逻辑分割成不同的模块放在不同的文件中。但是模块系统有一个前提是,代码运行的环境必须支持模块系统,比如浏览器支持
ES Modules
,所以我们可以使用模块,通过
import
和
export
导入模块。假设我们的代码要在一个不支持任何模块系统的环境中运行,那么我们就无法使用模块系统了,此时我们应该怎么将代码分离呢?
恰好,typescript支持
namespace
,它可以帮助我们将代码逻辑分离,解决问题。
一.namespace——命名空间
#
如果你熟悉C++、Java、C#等语言,namespace对你来说应该并不陌生。namepsace可以用来封装一段代码,在namespace外面的代码,无法直接访问namespace内部的代码。
命名空间通过
namespace
关键字定义。格式如下:
namespace namespace_name {
以下面的例子为例,在
Lib
命名空间外,无法访问
Lib
内部的
_name
和
getName
。
namespace Lib {
const _name = '小明';
function getName() {
return _name;
console.log(_name);
console.log(getName());
如果使用
tsc
编译上面的代码,编译器会直接报错。
因为JavaScript是不支持命名空间语法的,所以typescript是如何实现命名空间的呢?为了了解它的原理,首先注释掉最后两行代码。
namespace Lib {
const _name = '小明';
function getName() {
return _name;
使用
tsc
编译文件(typescript的编译在
这里
有详细介绍)。
tsc index.ts
编译后的js文件内容如下:
var Lib;
(function (Lib) {
var _name = '小明';
function getName() {
return _name;
})(Lib || (Lib = {}));
可以看到,namespace原理是通过
立即执行函数(IIFE)
实现,函数执行完毕,函数内部的变量无法从外界(global scope)获得。
为了获得namespace内部的变量或者函数,
可以通过
export
关键字将namespace中的变量暴露出来,然后通过命名空间名称访问暴露的变量
。
namespace Lib {
const _name = '小明';
export function getName() {
return _name;
console.log(Lib.getName());
使用
tsc
编译,编译通过,编译后的js文件内容如下:
var Lib;
(function (Lib) {
var _name = '小明';
function getName() {
return _name;
Lib.getName = getName;
})(Lib || (Lib = {}));
console.log(Lib.getName());
可以看到编译后的代码,通过将
getName
函数赋值给
Lib.getName
实现
export
的功能,所以在命名空间外部可以访问命名空间内部的变量。
通过编译后的js代码可以看到,
namespace本质上是一个object,我们通过object的属性访问命名空间内部的变量
。
二.导出类型和命名空间
#
和module一样,你可以从命名空间导出类型信息,并通过namespace的名称访问导出的类型。
namespace Home {
export interface Person {
name: string;
age: number;
export const child: Person = {
name: "小明",
age: 6
const man: Home.Person = {
name: "xx",
age: 20
编译后的js代码如下,编译后的js文件不包含任何类型信息。
var Home;
(function (Home) {
Home.child = {
name: "小明",
age: 6
})(Home || (Home = {}));
var man = {
name: "xx",
age: 20
命名空间可以嵌套
,并且子命名空间可以被父命名空间导出,然后通过
命名空间名称链
访问内部命名空间的变量。
namespace Outer {
export namespace Inner {
export const a = 3;
console.log(Outer.Inner.a);
编译后的js文件如下。
var Outer;
(function (Outer) {
var Inner;
(function (Inner) {
Inner.a = 3;
})(Inner = Outer.Inner || (Outer.Inner = {}));
})(Outer || (Outer = {}));
console.log(Outer.Inner.a);
三.别名
#
因为命名空间可以嵌套,当嵌入层级很深的时候,通过
命名空间名称链
访问比较麻烦,例如
Space1.Space2.Space3.Space4.xxx
,可以通过**别名(aliasing)**简化命名空间名称链。
namespace MyLibA {
export namespace Types {
export interface Person {
name: string;
age: number;
export namespace Functions {
export function getPerson(name: string, age: number):
Types.Person {
return {name, age};
var API_FUNCTIONS = MyLibA.Functions;
const ross = API_FUNCTIONS.getPerson('Ross Geller', 30);
上面的代码,通过
var API_FUNCTIONS = MyLibA.Functions;
添加别名的方式,简化了
MyLibA.Functions
的访问。
但是使用同样的方式,给
MyLibA.Types
添加别名会报错,因为
MyLibA.Types
命名空间内部仅包含类型信息,不存在其他字段,所以本质上是不存在的(编译后的JS代码会移除类型信息)。你可以使用
type Person = MyLibA.Types.Person
,简化访问。
TypeScirpt还支持使用
import <alias> =
语句简化内部命名空间的访问,并且给
MyLib.Types
添加别名不会报错,这是typescript给我们提供的一个语法糖,用来为命名空间创建别名。
namespace MyLibA {
export namespace Types {
export interface Person {
name: string;
age: number;
export namespace Functions {
export function getPerson(name: string, age: number):
Types.Person {
return {name, age};
import API_FUNCTIONS = MyLibA.Functions;
import API_Types = MyLibA.Types;
const ross: API_Types.Person = API_FUNCTIONS.getPerson('Ross Geller', 30);
四.导入命名空间
#
因为命名空间本质上就是一个Object,所以可以通过import语句导入命名空间。
export namespace Home {
export interface Person {
name: string;
age: number;
export const child: Person = {
name: "小明",
age: 6
--------------------------------------------------
import {Home} from "./a";
console.log(Home.child.name);
导入命名空间,需要代码的执行环境支持命名空间,上例是ES Modules,如果是NodeJS环境,它支持CommonJS模块系统,那么需要使用
require
、
exports
语句导入导出。
五.模块化
#
Typescript提供了
///
,它仅在ts编译阶段起作用,用于指示ts编译器定位ts文件。
/// <reference path="" />
与c语言中的
#include
类似。它必须出现在文件的最上面,本质上就是一段注释,所以它的作用也仅体现在编译阶段。
reference
指定的
path
属性的值是另一个ts文件的路径,用来告诉编译器当前文件编译的依赖文件,有点类似
import
语句,但是不需要导入指定的变量。
当
reference
指定指定了一个文件,typescript在编译时,会自动将这个文件包含在编译过程,这个文件内所有的全局变量都会在当前文件(
reference
指定存在的文件)被获得。
以下面例子为例,在
index.ts
中,通过
/// <reference path="./math.ts" />
引入
math.ts
文件。
namespace MyMath {
export const add = (a: number, b: number) => {
return a + b;
MyMath.add(3, 4);
通过
tsc index.ts
编译,编译后有
index.js
和
math.js
两个文件,内容如下。
MyMath.add(3, 4);
var MyMath;
(function (MyMath) {
MyMath.add = function (a, b) {
return a + b;
})(MyMath || (MyMath = {}));
当然我们无法在Node环境中执行这些代码,因为这是两个分离的文件,并且没有require语句。我们需要首先将它们打包成一个文件
bundle.js
,然后使用命令
node boundle.js
执行。
在浏览器环境中,我们需要使用
<script>
语句依次加载
math.js
和
index.js
文件。
<script src="./math.js"></script>
<script src="./index.js"></script>
更好的做法,是使用
tsc
的
--outFile
配置选项,将输出文件打包成一个bundle,ts会自动根据
reference
指令,编译文件。
关于
tsc
命令详解和tsconfig.json文件的配置,可以看我的这篇文章:
tsconfig.json详解
。
使用
tsc --outFile bundle.js index.ts
命令编译文件,编译后的bundle.js文件内容如下:
var MyMath;
(function (MyMath) {
MyMath.add = function (a, b) {
return a + b;
})(MyMath || (MyMath = {}));
MyMath.add(3, 4);
六.扩展命名空间
#
使用
reference
指令可以扩展一个早已经定义的命名空间。直接看下面的例子。
const john: MyLibA.Person = MyLibA.defaultPerson;
const ross: MyLibA.Person = MyLibA.getPerson( 'Ross Geller', 30 );
console.log( john );
console.log( ross );
namespace MyLibA {
export const defaultPerson: Person = getPerson( 'John Doe', 21 );
namespace MyLibA {
export interface Person {
name: string;
age: number;