TypeScript 从入门到放弃(三):模块、命名空间、声明合并、声明文件

本文作为学习笔记,文中内容大多来自官方文档和一些资料,摘抄的部分会在文中标注出原文地址,可以直接参考原文。
前两篇学习了 TS 中基本类型、函数、类、接口、泛型以及高级类型概念和使用方法。这些是基础知识点,虽然简单但是很重要。本文将复习 JS 中的模块对比不同方式模块的区别;如何使用命名空间隔离代码;声明合并的规则等。

模块

在 ES6 之前采用的模块加载方案,主要有 CommonJS
AMD
两种。前者用于服务器,后者用于浏览器。ES6 在此基础上实现了模块的功能,使用简单可以完全取代之前的方案,成为浏览器和服务器通用的模块解决方案。
下面学习 ES6 中模块的导出和导入。

导出

ES6 提供了多种导出方式,例如:单独导出,批量导出、导出接口/函数、导出起别名、默认导出以及复合导出等。

导出使用 export
命令,举个例子:

// a.js
// 单独导出
export const a = 'a'

// 批量导出
const b = 'b'
const c = 'c'
export { b, c }

// 导出接口
export interface P {
  x: number,
  y: number
}

// 导出函数
export function f () { }

// 导出时起别名
function g () { }
export { g as G }

// 默认导出,无需函数名
export default function () {
  console.log("I'm default")
}
// 从 b 导入 str,起别名导出
/*
b.js
export conststr = 'hello'
*/
export { str as Hello } from './b'

导入

对应着 export
命令,导入使用 import
命令。

// 导入
import { a, b, c } from './a'   // 批量导入
import { P } from './a'         // 导入接口
import { f as F } from './a'    // 导入时起别名
import * as All from './a'      // 导出 a 中所用成员,绑定在 all 上
import myFunction from './a'    // 不加 {},导入默认。

关于 ES6 模块可以参考:阮一峰老师 《ES6入门》

浏览器加载

默认情况下,浏览器是同步加载 JS 脚本,遇到
标签会等待执行完脚本才会继续渲染。在文件较大下载和执行时间较长时,会出现浏览器假死无响应。为了解决这个问题,加入异步加载语法。在
中使用 defer
async
属性,指定脚本异步加载。

defer
async

在浏览器中加载模块需要将 type
指定为 module


指定 type="module"

都是异步的。

ES6 模块和 CommonJS 模块的区别

ES6
模块和 CommonJS
是完全不同的。

  • CommonJS
    模块输出的是一个值的拷贝, ES6
    模块输出的是值的引用。
  • CommonJS
    模块是运行时加载的, ES6
    模块时编译时输出接口。

拷贝值和值引用的最大区别是:值拷贝时原始值改变拷贝值不会变,值引用则会随原始值改变。
举个例子:

// b.js
let counter = 3
function incCounter () {
  counter++
}
module.exports = {
  counter,
  incCounter
}

// a.js
const b = require('./b')
console.log(b.counter)
b.incCounter()
console.log(b.counter)

上面的例子输出结果是多少?

b.js
是一个模块导出一个变量 counter
和一个函数 incCounter

首次输出 counter
值为 3。当调用 b.incCounter
函数后再输出 counter
仍为 3。导致 counter
不改变的原因 CommonJS
模块是值拷贝。
同样的例子在 ES6 模块中就不会出现这个问题。

// a.js
export let counter = 3
export function incCounter () {
  counter++
}
// b.js
import { counter, incCounter } from './a'
console.log(counter) // 3
incCounter()
console.log(counter) // 4

以上是 ES6 模块和 CommonJS 模块的区别。具体可以参考阮一峰老师 《ES6入门-模块加载》

TS 中模块

为了支持 CommonJS
exports
, TS 提供了 export=
语法。 export=
语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。若使用 export=
导出一个模块,则必须使用 TS 的特定语法 import module = require("module")
来导入此模块。

命名空间

命名空间用来解决重名问题,定义命名空间使用 namespace
关键字。

namespace Shape {
  export function square (x: number) {
    return x * x
  }
}

定义了一个 Shape
命名空间,向外部提供了一个 Square
函数,使用 export
关键字导出。调用的方法是 Shape.square(1)
直接使用命名空间调用。
引入命名空间的方法比较特殊,格式如下:

/// 

使用 ///
引用命名空间。

声明合并

声明合并是指编译器将同名的独立声明合并为单个声明。合并后的声明拥有原来多个声明的特性。

接口的声明合并

interface A {
  x: number;
  // y: string;
}
interface A {
  y: number,
  foo (bar: number): number
}
// a 必须实现所有的属性和方法。
let a: A = {
  x: 1,
  y: 1,
  foo (bar: number) {
    return bar
  }
}

两个地方声明同样的接口时,编译器会自动合并到一起。当相同属性类型不同是会提示错误。

命名空间的声明合并

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

命名空间合并的是导出成员,非导出成员是无法被合并访问的。

函数与命名空间合并

// 命名空间和函数的合并
function Lib () { }
// 相当于给函数增加了一个静态属性
namespace Lib {
  // 需要 export 
  export let version = '1.0'
}

console.log(Lib.version)

可以使用函数和命名空间合并的方式,为函数添加属性。
注意:函数的声明必须在命名空间前。

类与命名空间合并

// 命名空间和类合并
class C { }
// 为类添加静态属性
namespace C {
  export let state = 1
}

使用合并为类添加一些静态的属性。
注意:类的定义必须在命名空间前。

枚举与命名空间合并

// 命名空间和枚举合并
enum Color {
  Red,
  Yellow,
  Blue
}
// 增加一个方法
namespace Color {
  export function mix () { }
}
console.log(Color)

注意:和类与函数不同,枚举可以放在命名空间的后面。

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

参考 《TypeScript入门教程 – 声明文件》

小结

以上是本篇的全部内容,都是一些比较基础的知识点,后续随着学习的深入在慢慢补充。