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)
注意:和类与函数不同,枚举可以放在命名空间的后面。
声明文件
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
小结
以上是本篇的全部内容,都是一些比较基础的知识点,后续随着学习的深入在慢慢补充。