====== 第十二章:命名空间 ====== ===== 本章概述 ===== 命名空间(Namespace)是 TypeScript 早期版本中用于组织代码和组织类型命名冲突的重要特性。虽然在 ES6 模块普及后,模块已成为首选的代码组织方式,但命名空间在某些场景下仍然非常有用,特别是在声明文件和全局扩展中。本章将详细介绍命名空间的概念、用法以及与模块的区别。 ===== 12.1 什么是命名空间 ===== ==== 12.1.1 命名空间的概念 ==== 命名空间是一种将相关代码组织在一起的方式,它可以包含类、接口、函数、变量等。命名空间通过创建一个新的作用域来避免命名冲突。 // 定义命名空间 namespace MyNamespace { export const version = "1.0.0"; export class User { constructor(public name: string) {} } export interface Config { timeout: number; } export function greet(name: string): string { return `Hello, ${name}!`; } } // 使用命名空间 console.log(MyNamespace.version); const user = new MyNamespace.User("Alice"); console.log(MyNamespace.greet("World")); ==== 12.1.2 为什么需要命名空间 ==== 在没有模块系统(ES6 Modules)之前,JavaScript 代码通常使用全局变量,这容易导致命名冲突: // 不使用命名空间 - 全局命名冲突风险 // library-a.ts function processData(data: any) { /* ... */ } // library-b.ts function processData(data: any) { /* ... */ } // 冲突! // 使用命名空间解决冲突 namespace LibraryA { export function processData(data: any) { /* ... */ } } namespace LibraryB { export function processData(data: any) { /* ... */ } } // 使用 LibraryA.processData({}); LibraryB.processData({}); ===== 12.2 命名空间的基本用法 ===== ==== 12.2.1 定义命名空间 ==== namespace Validation { // 内部变量 - 不导出则外部无法访问 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 导出接口 export interface StringValidator { isValid(s: string): boolean; } // 导出类 export class EmailValidator implements StringValidator { isValid(s: string): boolean { return emailRegex.test(s); } } export class PhoneValidator implements StringValidator { isValid(s: string): boolean { return /^\d{11}$/.test(s); } } // 导出函数 export function validateAll( validators: StringValidator[], value: string ): boolean { return validators.every(v => v.isValid(value)); } } // 使用 const emailValidator = new Validation.EmailValidator(); console.log(emailValidator.isValid("test@example.com")); // true ==== 12.2.2 嵌套命名空间 ==== 命名空间可以嵌套定义,形成层级结构: namespace App { export namespace Utils { export function formatDate(date: Date): string { return date.toISOString().split('T')[0]; } export function generateId(): string { return Math.random().toString(36).substr(2, 9); } } export namespace Models { export interface User { id: string; name: string; } export class UserRepository { private users: User[] = []; add(user: User): void { this.users.push(user); } findById(id: string): User | undefined { return this.users.find(u => u.id === id); } } } } // 使用嵌套命名空间 const formattedDate = App.Utils.formatDate(new Date()); const userRepo = new App.Models.UserRepository(); userRepo.add({ id: App.Utils.generateId(), name: "Alice" }); ==== 12.2.3 别名简化访问 ==== 对于深层嵌套的命名空间,可以使用 import 别名简化访问: namespace Company { export namespace Department { export namespace Team { export class Member { constructor(public name: string) {} } export function createTeam(name: string): Member[] { return []; } } } } // 使用别名 import Team = Company.Department.Team; const member = new Team.Member("Alice"); const newTeam = Team.createTeam("Engineering"); // 等价于 const member2 = new Company.Department.Team.Member("Bob"); ===== 12.3 命名空间与文件 ===== ==== 12.3.1 单文件中的多个命名空间 ==== 一个文件可以包含多个命名空间: // types.ts namespace Types { export interface Point { x: number; y: number; } export type ID = string | number; } namespace Helpers { export function distance(p1: Types.Point, p2: Types.Point): number { const dx = p1.x - p2.x; const dy = p1.y - p2.y; return Math.sqrt(dx * dx + dy * dy); } } // 使用 const p1: Types.Point = { x: 0, y: 0 }; const p2: Types.Point = { x: 3, y: 4 }; console.log(Helpers.distance(p1, p2)); // 5 ==== 12.3.2 跨文件的命名空间 ==== 命名空间可以分散在多个文件中,通过 " " 指令连接: // validation/StringValidator.ts namespace Validation { export interface StringValidator { isValid(s: string): boolean; } } // validation/EmailValidator.ts /// namespace Validation { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export class EmailValidator implements StringValidator { isValid(s: string): boolean { return emailRegex.test(s); } } } // validation/ZipCodeValidator.ts /// namespace Validation { export class ZipCodeValidator implements StringValidator { isValid(s: string): boolean { return /^\d{6}$/.test(s); } } } ==== 12.3.3 编译配置 ===== 编译多文件命名空间需要特殊配置: { "compilerOptions": { "outFile": "./dist/bundle.js", // 将所有文件合并输出 "module": "AMD", // 或使用 System "declaration": true // 生成 .d.ts 声明文件 } } ===== 12.4 命名空间与模块的区别 ===== ==== 12.4.1 关键区别 ==== | 特性 | 命名空间 (Namespace) | 模块 (Module) | |------|---------------------|---------------| | 语法 | namespace XXX {} | export / import | | 编译输出 | 合并为单个文件 (outFile) | 单独文件 | | 加载方式 | 全局加载 | 按需加载 | | 依赖声明 | /// | import | | 现代推荐度 | 较少使用 | 推荐使用 | | 适用场景 | 声明文件、全局扩展 | 应用程序代码 | ==== 12.4.2 何时使用命名空间 ==== **推荐使用命名空间的场景:** 1. **声明文件(.d.ts)中组织类型** 2. **扩展现有库的全局类型** 3. **需要编译成单个文件的传统项目** // 声明文件中使用命名空间 // lodash.d.ts declare namespace _ { interface LoDashStatic { chunk(array: T[], size?: number): T[][]; compact(array: (T | null | undefined)[]): T[]; } } declare const _: _.LoDashStatic; ==== 12.4.3 何时使用模块 ==== **推荐使用模块的场景:** 1. **现代应用程序开发** 2. **需要代码分割和懒加载** 3. **使用打包工具(Webpack、Vite、Rollup)** 4. **Node.js 服务端开发** // 推荐使用模块方式 // utils/validation.ts export interface Validator { isValid(value: unknown): boolean; } export class EmailValidator implements Validator { isValid(value: unknown): boolean { return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); } } // main.ts import { EmailValidator, Validator } from './utils/validation'; const validator: Validator = new EmailValidator(); ===== 12.5 命名空间的高级用法 ===== ==== 12.5.1 命名空间与类合并 ===== 命名空间可以与类合并,用于添加静态成员: class Album { label!: Album.AlbumLabel; } namespace Album { export class AlbumLabel { constructor(public label: string) {} } // 静态属性 export let defaultLabel = new AlbumLabel("Unknown"); // 静态方法 export function createWithLabel(title: string, labelName: string): Album { const album = new Album(); album.label = new AlbumLabel(labelName); return album; } } // 使用 const album = new Album(); album.label = new Album.AlbumLabel("Sony Music"); const album2 = Album.createWithLabel("My Album", "Warner"); ==== 12.5.2 命名空间与函数合并 ===== 命名空间可以与函数合并,添加属性和方法: function greet(name: string): string { return greet.prefix + name + greet.suffix; } namespace greet { export let prefix = "Hello, "; export let suffix = "!"; export function formal(name: string): string { return `Dear ${name},`; } export function casual(name: string): string { return `Hey ${name}!`; } } // 使用 console.log(greet("World")); // Hello, World! greet.prefix = "Hi, "; console.log(greet("World")); // Hi, World! console.log(greet.formal("Mr. Smith")); // Dear Mr. Smith, ==== 12.5.3 命名空间与枚举合并 ===== enum Color { Red, Green, Blue } namespace Color { export function toHex(color: Color): string { switch (color) { case Color.Red: return "#FF0000"; case Color.Green: return "#00FF00"; case Color.Blue: return "#0000FF"; default: return "#000000"; } } export function fromHex(hex: string): Color | undefined { switch (hex.toUpperCase()) { case "#FF0000": return Color.Red; case "#00FF00": return Color.Green; case "#0000FF": return Color.Blue; default: return undefined; } } } // 使用 console.log(Color.toHex(Color.Red)); // #FF0000 const color = Color.fromHex("#00FF00"); console.log(color); // 1 (Color.Green) ===== 12.6 全局扩展 ===== ==== 12.6.1 扩展全局对象 ===== 命名空间常用于扩展全局对象(如 Window): // global-extensions.d.ts declare global { interface Window { myApp: { version: string; config: AppConfig; }; } interface AppConfig { apiUrl: string; debug: boolean; } } // 使用时需要导出,使其成为模块 declare global { // 扩展声明 } export {}; // 使文件成为模块 ==== 12.6.2 扩展内置类型 ===== // 扩展 Array 接口 declare global { interface Array { first(): T | undefined; last(): T | undefined; } } // 实现 if (!Array.prototype.first) { Array.prototype.first = function(this: T[]): T | undefined { return this[0]; }; } if (!Array.prototype.last) { Array.prototype.last = function(this: T[]): T | undefined { return this[this.length - 1]; }; } export {}; // 使用 const arr = [1, 2, 3]; console.log(arr.first()); // 1 console.log(arr.last()); // 3 ===== 12.7 本章小结 ===== 本章我们学习了: 1. **命名空间基础** - 概念、定义、export 关键字 2. **嵌套命名空间** - 层级结构、import 别名 3. **多文件命名空间** - 跨文件组织、reference 指令 4. **与模块的区别** - 适用场景对比 5. **声明合并** - 与类、函数、枚举的合并 6. **全局扩展** - 扩展 Window、Array 等内置类型 ===== 12.8 练习题 ===== ==== 练习 1:命名空间基础 ==== 创建一个 Geometry 命名空间,包含: - 计算圆面积的函数(接收半径参数) - 计算矩形面积的函数(接收宽高参数) - 计算三角形面积的函数(接收底高参数) - 导出一个常量 PI 参考答案 namespace Geometry { export const PI = Math.PI; export function circleArea(radius: number): number { return PI * radius * radius; } export function rectangleArea(width: number, height: number): number { return width * height; } export function triangleArea(base: number, height: number): number { return 0.5 * base * height; } } // 使用 console.log(Geometry.PI); // 3.14159... console.log(Geometry.circleArea(5)); // 78.54... console.log(Geometry.rectangleArea(4, 6)); // 24 console.log(Geometry.triangleArea(4, 6)); // 12 ==== 练习 2:嵌套命名空间 ==== 创建一个 Company 命名空间,包含: - HR 子命名空间:Employee 类、Department 枚举 - Finance 子命名空间:Invoice 类、calculateTax 函数 - 使用 import 别名简化访问 参考答案 namespace Company { export namespace HR { export enum Department { Engineering = "Engineering", Sales = "Sales", Marketing = "Marketing" } export class Employee { constructor( public name: string, public department: Department, public salary: number ) {} getInfo(): string { return `${this.name} - ${this.department}`; } } } export namespace Finance { export class Invoice { constructor( public id: string, public amount: number, public taxRate: number = 0.1 ) {} getTotal(): number { return this.amount * (1 + this.taxRate); } } export function calculateTax(amount: number, rate: number): number { return amount * rate; } } } // 使用别名 import HR = Company.HR; import Finance = Company.Finance; const emp = new HR.Employee("Alice", HR.Department.Engineering, 5000); console.log(emp.getInfo()); const invoice = new Finance.Invoice("INV001", 1000, 0.13); console.log(invoice.getTotal()); console.log(Finance.calculateTax(1000, 0.13)); ==== 练习 3:声明合并 ==== 创建一个 Calculator 类与命名空间合并: - Calculator 类具有基本运算方法 - 命名空间部分添加历史记录功能 - 命名空间部分添加常量(如 PI、E) 参考答案 class Calculator { add(a: number, b: number): number { Calculator.addToHistory(`${a} + ${b} = ${a + b}`); return a + b; } subtract(a: number, b: number): number { Calculator.addToHistory(`${a} - ${b} = ${a - b}`); return a - b; } multiply(a: number, b: number): number { Calculator.addToHistory(`${a} * ${b} = ${a * b}`); return a * b; } divide(a: number, b: number): number { if (b === 0) throw new Error("Cannot divide by zero"); Calculator.addToHistory(`${a} / ${b} = ${a / b}`); return a / b; } } namespace Calculator { // 常量 export const PI = Math.PI; export const E = Math.E; // 历史记录 const history: string[] = []; export function addToHistory(entry: string): void { history.push(entry); } export function getHistory(): string[] { return [...history]; } export function clearHistory(): void { history.length = 0; } // 工具函数 export function factorial(n: number): number { if (n <= 1) return 1; return n * factorial(n - 1); } } // 使用 const calc = new Calculator(); console.log(calc.add(5, 3)); // 8 console.log(calc.multiply(4, 7)); // 28 console.log(Calculator.PI); // 3.14159... console.log(Calculator.factorial(5)); // 120 console.log(Calculator.getHistory()); // ["5 + 3 = 8", "4 * 7 = 28"] ==== 练习 4:全局扩展 ==== 扩展 String 接口,添加以下方法: - reverse(): 反转字符串 - isPalindrome(): 判断是否为回文 - toTitleCase(): 转换为标题格式 参考答案 // string-extensions.d.ts declare global { interface String { reverse(): string; isPalindrome(): boolean; toTitleCase(): string; } } // string-extensions.ts if (!String.prototype.reverse) { String.prototype.reverse = function(this: string): string { return this.split('').reverse().join(''); }; } if (!String.prototype.isPalindrome) { String.prototype.isPalindrome = function(this: string): boolean { const cleaned = this.toLowerCase().replace(/[^a-z0-9]/g, ''); return cleaned === cleaned.reverse(); }; } if (!String.prototype.toTitleCase) { String.prototype.toTitleCase = function(this: string): string { return this.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() ); }; } export {}; // 使用 const str = "hello world"; console.log(str.reverse()); // dlrow olleh console.log("A man a plan a canal Panama".isPalindrome()); // true console.log("hello world".toTitleCase()); // Hello World ===== 扩展阅读 ===== - [[TypeScript:第十三章_类型推断|下一章:类型推断]] - [[https://www.typescriptlang.org/docs/handbook/namespaces.html|TypeScript 官方文档 - 命名空间]] - [[https://www.typescriptlang.org/docs/handbook/declaration-merging.html|TypeScript 官方文档 - 声明合并]]