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

前言

本文作为学习笔记,文中内容大多来自官方文档和一些资料,摘抄的部分会在文中标注出原文地址,可以直接参考原文。

前两篇学习了 TS 中基本类型、函数、类、接口、泛型以及高级类型概念和使用方法。这些是基础知识点,虽然简单但是很重要。本文将复习 JS 中的模块对比不同方式模块的区别;如何使用命名空间隔离代码;声明合并的规则等。

模块

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

下面学习 ES6 中模块的导出和导入。

导出

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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 命令。

1
2
3
4
5
6
// 导入
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 脚本,遇到 <script> 标签会等待执行完脚本才会继续渲染。在文件较大下载和执行时间较长时,会出现浏览器假死无响应。为了解决这个问题,加入异步加载语法。在 <script>中使用deferasync属性,指定脚本异步加载。

  • defer: 渲染完在执行。要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行。
  • async: 下载完就执行。一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

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

1
<script type="module" src="a.js"/>

指定 type="module"<script> 都是异步的。

ES6 模块和 CommonJS 模块的区别

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

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

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

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 模块中就不会出现这个问题。

1
2
3
4
5
6
7
8
9
10
11
// 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 中模块

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

命名空间

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

1
2
3
4
5
namespace Shape {
export function square (x: number) {
return x * x
}
}

定义了一个 Shape 命名空间,向外部提供了一个 Square 函数,使用 export 关键字导出。调用的方法是 Shape.square(1) 直接使用命名空间调用。

引入命名空间的方法比较特殊,格式如下:

1
/// <reference path="shape.ts"/>

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

声明合并

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

接口的声明合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}
}

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

命名空间的声明合并

1
2
3
4
5
6
7
8
namespace Animals {
export class Zebra { }
}

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

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

函数与命名空间合并

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

console.log(Lib.version)

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

类与命名空间合并

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

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

枚举与命名空间合并

1
2
3
4
5
6
7
8
9
10
11
// 命名空间和枚举合并
enum Color {
Red,
Yellow,
Blue
}
// 增加一个方法
namespace Color {
export function mix () { }
}
console.log(Color)

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

声明文件

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

声明文件通过 declare 关键字告诉 TS,正在表述其他地方已经存在的代码。

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

小结

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

参考