原文地址:https://pengfeixc.com/blogs/javascript/typescript-namespace。
在之前的typescript module文章中,我讲解了如何通过typescript的模块系统,将程序的代码逻辑分割成不同的模块放在不同的文件中。但是模块系统有一个前提是,代码运行的环境必须支持模块系统,比如浏览器支持ES Modules,所以我们可以使用模块,通过import
和export
导入模块。假设我们的代码要在一个不支持任何模块系统的环境中运行,那么我们就无法使用模块系统了,此时我们应该怎么将代码分离呢?
恰好,typescript支持namespace
,它可以帮助我们将代码逻辑分离,解决问题。
如果你熟悉C++、Java、C#等语言,namespace对你来说应该并不陌生。namepsace可以用来封装一段代码,在namespace外面的代码,无法直接访问namespace内部的代码。
命名空间通过namespace
关键字定义。格式如下:
namespace namespace_name {
// 命名空间内部代码
}
以下面的例子为例,在Lib
命名空间外,无法访问Lib
内部的_name
和getName
。
// 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
语句简化内部命名空间的访问,并且给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 ='语句,不会报错
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模块系统,那么需要使用require
、exports
语句导入导出。
Typescript提供了///
,它仅在ts编译阶段起作用,用于指示ts编译器定位ts文件。
///
///
与c语言中的#include
类似。它必须出现在文件的最上面,本质上就是一段注释,所以它的作用也仅体现在编译阶段。
reference
指定的path
属性的值是另一个ts文件的路径,用来告诉编译器当前文件编译的依赖文件,有点类似import
语句,但是不需要导入指定的变量。
当reference
指定指定了一个文件,typescript在编译时,会自动将这个文件包含在编译过程,这个文件内所有的全局变量都会在当前文件(reference
指定存在的文件)被获得。
以下面例子为例,在index.ts
中,通过///
引入math.ts
文件。
// ---math.ts
namespace MyMath {
export const add = (a: number, b: number) => {
return a + b;
}
}
// ---index.ts
///
MyMath.add(3, 4);
通过tsc index.ts
编译,编译后有index.js
和math.js
两个文件,内容如下。
// ---index.js
///
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
执行。
在浏览器环境中,我们需要使用语句依次加载
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
指令可以扩展一个早已经定义的命名空间。直接看下面的例子。
// ---a.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
///
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
内部的Person
、getPerson
和defaultPerson
成员。
到这里,本章内容已经说完了。namespace虽然强大,但是如果你问我,什么时候该用命名空间?我会说,尽量避免使用命名空间吧,用Modules系统代替,现在Es Module很方便,在node环境中,也可以使用CommonJS代替命名空间。
namespace出现是早于ES Module的,所以说不定哪一天,namespace就被废弃了呢。
(完)