一、由來
命名空間源自 JavaScript 中的 [模塊模式](/articles/模塊模式-javascript 設計模式 2/):
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();
由兩部分組成:
- 模塊閉包(module closure):封裝模塊實現,隔離作用域
- 模塊對象(module object):該模塊暴露出去的變量和函數
後來在此基礎上擴展出模塊動態加載,拆分到多文件等支持
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 模塊名:
///<amd-module name="NamedModule"/>,指定 AMD 模塊名為NamedModule - 指定 AMD 模塊依賴(已廢棄):
/// <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關鍵字聲明 - 外部模塊:即模塊(如 ES Module、CommonJS、AMD、UMD 等),不用特別聲明,(含有
import或export的)文件即模塊
外部模塊可以簡單理解為外部文件中的模塊,因為可以在同一文件中定義多個不同的 namespace 或 module(即內部模塊),而無法定義多個 ES Module
P.S. 畢竟命名空間實質上是 IIFE,與模塊加載器無關,不存在文件即模塊的加載機制約束
概念差異
概念上,TypeScript 遵從 ES Module 規範(文件即模塊),通過編譯輸出 CommonJS、AMD、UMD 等模塊形式
而命名空間源自 JavaScript 中的 [模塊模式](/articles/模塊模式-javascript 設計模式 2/),算是舊時代的產物,不建議使用(用來 [聲明模塊類型](/articles/模塊-typescript 筆記 13/#articleHeader7) 除外)
加載機制差異
模塊引入機制上,命名空間需要通過三斜線指令引入,相當於源碼嵌入(類似於 CSS 中的 @import),會引入額外的變量到當前作用域中
P.S. 如果不打包成單文件 Bundle,就需要在運行時引入這些通過三斜線指令引入的依賴(例如通過 <script> 標籤)
而模塊則通過 import/require 等方式引入,由調用方決定是否通過變量去引用它,而不會主動影響當前作用域
P.S. import "module-name"; 語法就只引入模塊(的副作用),不引用並訪問模塊,具體見 import
最佳實踐
在模塊與命名空間的使用上,有一些實踐經驗:
- 減少了命名空間嵌套層級,比如只含有靜態方法的 class 通常是不必要的,模塊名足夠表達語義
- 模塊僅暴露一個 API 時,用 export default 更合適,引入更方便,而且調用方不必關注 API 名
- 要暴露出多個 API 的話,都直接
export(數量過多的話,[引入模塊對象](/articles/module-es6 筆記 13/#articleHeader5),如import * as largeModule from 'SoLargeModule') - 通過 re-export 擴展現有模塊,例如
export as - 不要在模塊裡使用命名空間,因為模塊本就具有邏輯結構(文件目錄結構)和模塊作用域,命名空間提供不了更多好處了
暫無評論,快來發表你的看法吧