日期:2021年9月24日标签:JavaScript

在TypeScript中使用namespace封装数据 #

在之前的typescript module文章中,我讲解了如何通过typescript的模块系统,将程序的代码逻辑分割成不同的模块放在不同的文件中。但是模块系统有一个前提是,代码运行的环境必须支持模块系统,比如浏览器支持ES Modules,所以我们可以使用模块,通过importexport导入模块。假设我们的代码要在一个不支持任何模块系统的环境中运行,那么我们就无法使用模块系统了,此时我们应该怎么将代码分离呢?

恰好,typescript支持namespace,它可以帮助我们将代码逻辑分离,解决问题。

一.namespace——命名空间 #

如果你熟悉C++、Java、C#等语言,namespace对你来说应该并不陌生。namepsace可以用来封装一段代码,在namespace外面的代码,无法直接访问namespace内部的代码。

命名空间通过namespace关键字定义。格式如下:

namespace namespace_name {
    // 命名空间内部代码
}

以下面的例子为例,在Lib命名空间外,无法访问Lib内部的_namegetName

// index.ts
namespace Lib {
    const _name = '小明';
    
    function getName() {
        return _name;
    }
}

console.log(_name); // Error: Cannot find name '_name'
console.log(getName()); // Error: Cannot find name 'getName'

如果使用tsc编译上面的代码,编译器会直接报错。

因为JavaScript是不支持命名空间语法的,所以typescript是如何实现命名空间的呢?为了了解它的原理,首先注释掉最后两行代码。

// index.ts
namespace Lib {
    const _name = '小明';
    
    function getName() {
        return _name;
    }
}

// console.log(_name); 
// console.log(getName());

使用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关键字导出getName
    export function getName() {
        return _name;
    }
}

// 通过命名空间名称访问内部的变量(函数)
console.log(Lib.getName());

使用tsc编译,编译通过,编译后的js文件内容如下:

var Lib;
(function (Lib) {
    var _name = '小明';
    // 使用export关键字导出getName
    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);

// Error: Property 'Types' does not exist on type 'typeof MyLibA'
// 因为Types命名空间仅包含类型信息,编译后的js代码,类型信息会被移除
// var API_TYPES = MyLibA.Types;

上面的代码,通过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; // 使用'import <alias> ='语句,不会报错

const ross: API_Types.Person = API_FUNCTIONS.getPerson('Ross Geller', 30);

四.导入命名空间 #

因为命名空间本质上就是一个Object,所以可以通过import语句导入命名空间。

// ---a.ts

// 导出命名空间Home
export namespace Home {
    export interface Person {
        name: string;
        age: number;
    }

    export const child: Person = {
        name: "小明",
        age: 6
    };
}

--------------------------------------------------
// ---b.ts

// 导入命名空间Home
import {Home} from "./a";

console.log(Home.child.name);

导入命名空间,需要代码的执行环境支持命名空间,上例是ES Modules,如果是NodeJS环境,它支持CommonJS模块系统,那么需要使用requireexports语句导入导出。

五.模块化 #

Typescript提供了///,它仅在ts编译阶段起作用,用于指示ts编译器定位ts文件。

/// <reference path="./b.ts" />

/// <reference path="" />与c语言中的#include类似。它必须出现在文件的最上面,本质上就是一段注释,所以它的作用也仅体现在编译阶段。

reference指定的path属性的值是另一个ts文件的路径,用来告诉编译器当前文件编译的依赖文件,有点类似import语句,但是不需要导入指定的变量。

reference指定指定了一个文件,typescript在编译时,会自动将这个文件包含在编译过程,这个文件内所有的全局变量都会在当前文件(reference指定存在的文件)被获得。

以下面例子为例,在index.ts中,通过/// <reference path="./math.ts" />引入math.ts文件。

// ---math.ts
namespace MyMath {
    export const add = (a: number, b: number) => {
        return a + b;
    }
}
// ---index.ts
/// <reference path="./math.ts" />

MyMath.add(3, 4);

通过tsc index.ts编译,编译后有index.jsmath.js两个文件,内容如下。

// ---index.js
/// <reference path="./math.ts" />
MyMath.add(3, 4);
// ---math.js
var MyMath;
(function (MyMath) {
    MyMath.add = function (a, b) {
        return a + b;
    };
})(MyMath || (MyMath = {}));

当然我们无法在Node环境中执行这些代码,因为这是两个分离的文件,并且没有require语句。我们需要首先将它们打包成一个文件bundle.js,然后使用命令node boundle.js执行。

在浏览器环境中,我们需要使用<script>语句依次加载math.jsindex.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 = {}));
/// <reference path="./math.ts" />
MyMath.add(3, 4);

六.扩展命名空间 #

使用reference指令可以扩展一个早已经定义的命名空间。直接看下面的例子。

// ---a.ts
/// <reference path="./b.ts"/>
const john: MyLibA.Person = MyLibA.defaultPerson;
const ross: MyLibA.Person = MyLibA.getPerson( 'Ross Geller', 30 );
console.log( john ); // {name: 'John Doe', age: 21}
console.log( ross ); // {name: 'Ross Geller', age: 30}
// ---b.ts
/// <reference path="./c.ts" />
namespace MyLibA {
    export const defaultPerson: Person = getPerson( 'John Doe', 21 );
}
// c.ts
namespace MyLibA {
    export interface Person {
        name: string;
        age: number;
    }
    export function getPerson( name: string, age: number ): Person {
        return { name, age };
    }
}

b.ts文件中,通过reference指令,引入了c.ts,扩展了MyLibA,添加defaultPerson变量,而且在b.ts文件中可以访问MyLibA中的所有变量,例如getPerson( 'John Doe', 21 );

a.ts文件中,通过reference指令,引入了b.ts,此时在a.ts文件中可以访问命名空间MyLibA内部的PersongetPersondefaultPerson成员。

七.建议 #

到这里,本章内容已经说完了。namespace虽然强大,但是如果你问我,什么时候该用命名空间?我会说,尽量避免使用命名空间吧,用Modules系统代替,现在Es Module很方便,在node环境中,也可以使用CommonJS代替命名空间。

namespace出现是早于ES Module的,所以说不定哪一天,namespace就被废弃了呢。

附:参考资料 #

(完)

目录