命名空间源自 JavaScript 中的模块模式:
var MyModule = {};
(function(exports) {
// 私有变量
var s = "hello";
// 公开函数
function f() {
return s;
}
exports.f = f;
})(MyModule);
MyModule.f();
// 错误 MyModule.s is not a function
MyModule.s();
由两部分组成:
后来在此基础上扩展出模块动态加载,拆分到多文件等支持
TypeScript 结合模块模式和类模式实现了一种模块机制,即命名空间:
namespace MyModule {
var s = "hello";
export function f() {
return s;
}
}
MyModule.f();
// 错误 Property 's' does not exist on type 'typeof MyModule'.
MyModule.s;
编译产物就是经典的模块模式:
var MyModule;
(function (MyModule) {
var s = "hello";
function f() {
return s;
}
MyModule.f = f;
})(MyModule || (MyModule = {}));
MyModule.f();
MyModule.s;
类似于模块,命名空间也是一种组织代码的方式:
namespace Page {
export interface IPage {
render(data: object): string;
}
export class IndexPage implements IPage {
render(data: object): string {
return '<div>Index page content is here.</div>';
}
}
}
// 编译结果为
var Page;
(function(Page) {
var IndexPage = /** @class */ (function() {
function IndexPage() {}
IndexPage.prototype.render = function(data) {
return '<div>Index page content is here.</div>';
};
return IndexPage;
})();
Page.IndexPage = IndexPage;
})(Page || (Page = {}));
同样具有作用域隔离(上例仅暴露出Page
一个全局变量),也支持按文件拆分模块:
// IPage.ts
namespace Page {
export interface IPage {
render(data: object): string;
}
}
// IndexPage.ts
/// <reference path="./IPage.ts" />
namespace Page {
export class IndexPage implements IPage {
render(data: object): string {
return '<div>Index page content is here.</div>';
}
}
}
// App.ts
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
let indexPageContent = new Page.IndexPage().render({value: 'index'})
编译结果为:
// IPage.js
空
// IndexPage.js
/// <reference path="./IPage.ts" />
var Page;
(function (Page) {
var IndexPage = /** @class */ (function () {
function IndexPage() {
}
IndexPage.prototype.render = function (data) {
return '<div>Index page content is here.</div>';
};
return IndexPage;
}());
Page.IndexPage = IndexPage;
})(Page || (Page = {}));
// App.js
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
var indexPageContent = new Page.IndexPage().render({ value: 'index' });
注意到这里通过三斜线指令引入被拆分出去的“namespace 模块”(而不是像 module 一样 import),仍用import
的话,会得到报错:
// 错误 File '/path/to/IndexPage.ts' is not a module.ts(2306)
import IndexPage from "./IndexPage";
P.S.另外,可以通过--outFile
选项生成一个 JS Bundle(默认编译生成对应的同名 JS 散文件)
支持 6 种指令:
/// <reference path="./myFile.ts" />
,引用当前目录下的myFile.ts
/// <reference types="node" />
,引用@types/node/index.d.ts
类型声明,对应--types
选项/// <reference lib="es2015" />
或/// <reference lib="es2017.string" />
,引用内置(类型)库lib.es2015.d.ts
或lib.es2017.string.d.ts
,对应--lib
编译选项/// <reference no-default-lib="true"/>
,编译过程中不加载默认库,对应--noLib
编译选项,同时标记当前文件为默认库(以致于--skipDefaultLibCheck
选项能够跳过检查该文件)///<amd-module name="NamedModule"/>
,指定 AMD 模块名为NamedModule
/// <amd-dependency path="legacy/moduleA" name="moduleA"/>
,依赖legacy/moduleA
,并指定引入模块名为moduleA
(name
属性可选)P.S.更多示例,见Triple-Slash Directives
形式上以 3 条斜线开头,因此称为三斜线指令(triple-slash directives),其中 XML 标签用来表达编译指令。其它注意事项如下:
/// <amd-dependency />
指令已废弃,用import "moduleName";
代替--noResolve
选项时,忽略掉所有/// <reference path="..." />
指令(不引入这些模块)作用上,/// <reference path="..." />
类似于 CSS 中的@import
(在指定--outFile
选项时,模块整合顺序与 path reference 指令顺序一致)
实现上,在预处理阶段会深度优先解析所有三斜线指令,将指定的文件添加到编译过程中
P.S.出现在其它位置的三斜线指令会被当做普通单行注释,不报错,但无效(编译器不认)
命名空间支持嵌套,因此可能会出现深层嵌套的情况:
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
此时可以通过别名来简化模块引用:
import P = Shapes.Polygons;
import Triangle = Shapes.Polygons.Triangle;
let sq = new P.Square();
let triangle = new Triangle();
// 编译后
var P = Shapes.Polygons;
var Triangle = Shapes.Polygons.Triangle;
var sq = new P.Square();
var triangle = new Triangle();
不难发现,这里的import
是var
的语法糖:
This is similar to using var, but also works on the type and namespace meanings of the imported symbol.
因此在给一个值起别名时会创建一个新的引用:
Importantly, for values, import is a distinct reference from the original symbol, so changes to an aliased var will not be reflected in the original variable.
例如:
namespace NS {
export let x = 1;
}
import x = NS.x;
import y = NS.x;
(x as any) = 2;
y === 1; // true
// 编译后
var NS;
(function (NS) {
NS.x = 1;
})(NS || (NS = {}));
var x = NS.x;
var y = NS.x;
x = 2;
y === 1; // true
P.S.import q = x.y.z
别名语法仅适用于命名空间,要求右侧必须是namespace
访问
namespace
与module
TypeScript 1.5 之前只有
module
关键字,不区分内部模块(internal modules)与外部模块(external modules),二者都通过module
关键字来定义。后来清晰起见,新增namespace
关键字表示内部模块
(摘自namespace keyword)
简言之,关键字module
与namespace
在语法上完全等价,例如:
namespace Shape.Rectangle {
export let a;
export let b;
export function getArea() { return a * b; }
}
与
module Shape.Rectangle {
export let a;
export let b;
export function getArea() { return a * b; }
}
的编译结果都是:
var Shape;
(function (Shape) {
var Rectangle;
(function (Rectangle) {
function getArea() { return Rectangle.a * Rectangle.b; }
Rectangle.getArea = getArea;
})(Rectangle = Shape.Rectangle || (Shape.Rectangle = {}));
})(Shape || (Shape = {}));
用namespace
代替旧的module
只是为了避免混淆,或者说是给 ES Module、AMD、UMD 等 Module 概念(所谓的外部模块)让道。因为如果霸占着module
关键字,实际上定义的不是 Module 而是 Namespace 的话,是很让人迷惑的一件事
也就是说:
namespace
或module
关键字声明import
或export
的)文件即模块外部模块可以简单理解为外部文件中的模块,因为可以在同一文件中定义多个不同的namespace
或module
(即内部模块),而无法定义多个ES Module
P.S.毕竟命名空间实质上是IIFE,与模块加载器无关,不存在文件即模块的加载机制约束
概念上,TypeScript遵从ES Module规范(文件即模块),通过编译输出CommonJS、AMD、UMD等模块形式
而命名空间源自JavaScript中的模块模式,算是旧时代的产物,不建议使用(用来声明模块类型除外)
模块引入机制上,命名空间需要通过三斜线指令引入,相当于源码嵌入(类似于CSS中的@import
),会引入额外的变量到当前作用域中
P.S.如果不打包成单文件 Bundle,就需要在运行时引入这些通过三斜线指令引入的依赖(例如通过<script>
标签)
而模块则通过import/require
等方式引入,由调用方决定是否通过变量去引用它,而不会主动影响当前作用域
P.S.import "module-name";
语法就只引入模块(的副作用),不引用并访问模块,具体见import
在模块与命名空间的使用上,有一些实践经验:
export
(数量过多的话,引入模块对象,如import * as largeModule from 'SoLargeModule'
)export as