前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >命名空间_TypeScript笔记15

命名空间_TypeScript笔记15

作者头像
ayqy贾杰
发布2019-06-12 15:16:41
6950
发布2019-06-12 15:16:41
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.由来

命名空间源自 JavaScript 中的模块模式:

代码语言:javascript
复制
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 结合模块模式和类模式实现了一种模块机制,即命名空间:

代码语言:javascript
复制
namespace MyModule {
  var s = "hello";
  export function f() {
    return s;
  }
}

MyModule.f();
// 错误 Property 's' does not exist on type 'typeof MyModule'.
MyModule.s;

编译产物就是经典的模块模式:

代码语言:javascript
复制
var MyModule;
(function (MyModule) {
  var s = "hello";
  function f() {
    return s;
  }
  MyModule.f = f;
})(MyModule || (MyModule = {}));
MyModule.f();
MyModule.s;

二.作用

类似于模块,命名空间也是一种组织代码的方式:

代码语言:javascript
复制
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一个全局变量),也支持按文件拆分模块:

代码语言:javascript
复制
// 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'})

编译结果为:

代码语言:javascript
复制
// 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的话,会得到报错:

代码语言:javascript
复制
// 错误 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.tslib.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,并指定引入模块名为moduleAname属性可选)

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.出现在其它位置的三斜线指令会被当做普通单行注释,不报错,但无效(编译器不认)

四.别名

命名空间支持嵌套,因此可能会出现深层嵌套的情况:

代码语言:javascript
复制
namespace Shapes {
  export namespace Polygons {
    export class Triangle { }
    export class Square { }
  }
}

此时可以通过别名来简化模块引用:

代码语言:javascript
复制
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();

不难发现,这里的importvar的语法糖:

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.

例如:

代码语言:javascript
复制
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访问

五.namespacemodule

TypeScript 1.5 之前只有module关键字,不区分内部模块(internal modules)与外部模块(external modules),二者都通过module关键字来定义。后来清晰起见,新增namespace关键字表示内部模块

(摘自namespace keyword)

简言之,关键字modulenamespace在语法上完全等价,例如:

代码语言:javascript
复制
namespace Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

代码语言:javascript
复制
module Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

的编译结果都是:

代码语言:javascript
复制
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 的话,是很让人迷惑的一件事

六.模块与命名空间

内部模块与外部模块

也就是说:

  • 内部模块:即命名空间,通过namespacemodule关键字声明
  • 外部模块:即模块(如ES Module、CommonJS、AMD、UMD 等),不用特别声明,(含有importexport的)文件即模块

外部模块可以简单理解为外部文件中的模块,因为可以在同一文件中定义多个不同的namespacemodule(即内部模块),而无法定义多个ES Module

P.S.毕竟命名空间实质上是IIFE,与模块加载器无关,不存在文件即模块的加载机制约束

概念差异

概念上,TypeScript遵从ES Module规范(文件即模块),通过编译输出CommonJS、AMD、UMD等模块形式

而命名空间源自JavaScript中的模块模式,算是旧时代的产物,不建议使用(用来声明模块类型除外)

加载机制差异

模块引入机制上,命名空间需要通过三斜线指令引入,相当于源码嵌入(类似于CSS中的@import),会引入额外的变量到当前作用域中

P.S.如果不打包成单文件 Bundle,就需要在运行时引入这些通过三斜线指令引入的依赖(例如通过<script>标签)

而模块则通过import/require等方式引入,由调用方决定是否通过变量去引用它,而不会主动影响当前作用域

P.S.import "module-name";语法就只引入模块(的副作用),不引用并访问模块,具体见import

最佳实践

在模块与命名空间的使用上,有一些实践经验:

  • 减少了命名空间嵌套层级,比如只含有静态方法的class通常是不必要的,模块名足够表达语义
  • 模块仅暴露一个API时,用export default 更合适,引入更方便,而且调用方不必关注API名
  • 要暴露出多个API的话,都直接export(数量过多的话,引入模块对象,如import * as largeModule from 'SoLargeModule'
  • 通过re-export扩展现有模块,例如export as
  • 不要在模块里使用命名空间,因为模块本就具有逻辑结构(文件目录结构)和模块作用域,命名空间提供不了更多好处了

参考资料

  • Namespaces
  • Namespaces and Modules
  • Triple-Slash Directives
  • 1.10 Namespaces
  • What’s the difference between internal and external modules in TypeScript?
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-04-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.由来
  • 二.作用
  • 三.三斜线指令
  • 四.别名
  • 五.namespace与module
  • 六.模块与命名空间
    • 内部模块与外部模块
      • 概念差异
        • 加载机制差异
          • 最佳实践
            • 参考资料
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档