Symbol与模块
Symbol 就像一个箱子,哪怕里面装的东西是一样的,也是没法相等的,这是因为每一个箱子都是唯一的。
Symbol 产生主要跟for of
循环有关,而for of
与for in
的区别就是,in
遍历的是对象的key
, 而 of
则是遍历 value
。
只要实现了 Symbol.iterator
这个接口,就可以通过for of
遍历。
最简单的例子就是
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // "4", "5", "6"
}
模块导入与导出
我们知道,基本上任何语言的都有相应管理代码导包的机制,比如 java 的 package,php 的 include 和 require 等。
而我们的 js 当可以运行在后端的时候,所以必须要有一种模块加载机制,于是就有了 cjs
规范。
而对于前端浏览器环境来说,我们引入 script 是通过标签进行引入的,而我们加载好的库,通常会在全局变量上面挂载某一个变量,比如我们熟知的$
,然而就是应该这样的方式容易导致模块的命名冲突,假如 a.js
和 b.js
都想往 window
上面挂载一个 $
对象,这样我们就没法确定这个$
从哪来的。
所以说我们不得不重视模块的重要性。
在 ts
中,我们想要让别人使用的方法就export
导出。想要使用别人的方法就用import
导入。
创建我们的 a.ts
文件。
export function whatsYourName(name: string) {
console.log(name);
}
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
其实 ts 跟 es6 的导出基本是一致的。
假如我们像上面这样导出,我们新建一个 b.ts
来导入 a 导出的东西。
这里就提示了我们所有导出的方法。我们可以看到我用了一个{}
,其实这就是解构。
我们可以把导出了看做为一个对象{}
,export function whatsYourName
,其实就是在导出的对象上面挂载一个whatsYourName
引用。
当然我们的export
可以直接写在声明的前面,或者使用先声明后导出的模式。
export { ZipCodeValidator };
这种先声明后导出,都需要用 {}
包裹起来。
ZipCodeValidator as mainValidator
这里的 as 并不是类型转换,而且给导出的取一个别名。
export default {
name: 'a'
}
当我们使用这样的 default
关键字的好处就是,我并不需要具体知道你类里面有哪些方法,我直接拿,就是拿到的一个默认导出, 也就是default
后面的东西,导入的时候不用加{}
。
export
可以多次导出,不过取的时候要用{}
和它具体的名称,因为只有对应了才能解构。
而export default
则不需要{}
,而且你可以给它取任何名称,并且一个模块只能有一个默认导出。
import a from "./a";
比如这里拿到的 a
就是{name:'a'}
当然我们也可以使用* as
这样的语法,把 a
模块里面所有 export 导出的都挂载到 all 这个变量上面去。
此时我们再修改一下我们b.ts
export * from './a';
export const name = 'b.ts';
export * from './a';
就是把 a 文件里面所有 export 的,再导出一次。
再次新建一个c.ts
文件
同样可以得到提示。
这样是不包含默认导出的。
假如想导出 a 的默认导出。
import a from './a';
export { a };
我们只能这样导出,没人任何其他的简写形式。
我们通过命令
tsc --module commonjs a.ts
编译得到commonjs
规范的 js 文件
exports.mainValidator = ZipCodeValidator;
exports.__esModule = true;
exports["default"] = {
name: 'a'
};
多有的 export
导出的变成了exports
上面的属性。而默认导出的挂载到了default
属性上面。
tsc --module amd a.ts
而 amd
define(["require", "exports"], function (require, exports) {
"use strict";
function whatsYourName(name) {
console.log(name);
}
exports.whatsYourName = whatsYourName;
exports.numberRegexp = /^[0-9]+$/;
var ZipCodeValidator = (function () {
function ZipCodeValidator() {
}
ZipCodeValidator.prototype.isAcceptable = function (s) {
return s.length === 5 && exports.numberRegexp.test(s);
};
return ZipCodeValidator;
}());
exports.ZipCodeValidator = ZipCodeValidator;
exports.mainValidator = ZipCodeValidator;
exports.__esModule = true;
exports["default"] = {
name: 'a'
};
});
就是在外面加了一个大的 define
函数而已,这个函数来自 require.js
,前端的一个异步加载 js 解决方案。
假如你通过node
运行会报错,因为此时没有 define
函数
以及umd
,这是一种兼容 amd
与 commonjs
的做法。通过node
依然可以运行。
不信你可以自己打印点东西看下。
tsc --module amd a.ts
(function (dependencies, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === 'function' && define.amd) {
define(dependencies, factory);
}
})(["require", "exports"], function (require, exports) {
"use strict";
function whatsYourName(name) {
console.log(name);
}
exports.whatsYourName = whatsYourName;
exports.numberRegexp = /^[0-9]+$/;
var ZipCodeValidator = (function () {
function ZipCodeValidator() {
}
ZipCodeValidator.prototype.isAcceptable = function (s) {
return s.length === 5 && exports.numberRegexp.test(s);
};
return ZipCodeValidator;
}());
exports.ZipCodeValidator = ZipCodeValidator;
exports.mainValidator = ZipCodeValidator;
exports.__esModule = true;
exports["default"] = {
name: 'a'
};
});
还有 System
tsc --module system a.ts
System.register([], function (exports_1, context_1) {
"use strict";
var __moduleName = context_1 && context_1.id;
function whatsYourName(name) {
console.log(name);
}
exports_1("whatsYourName", whatsYourName);
var numberRegexp, ZipCodeValidator;
return {
setters: [],
execute: function () {
exports_1("numberRegexp", numberRegexp = /^[0-9]+$/);
ZipCodeValidator = (function () {
function ZipCodeValidator() {
}
ZipCodeValidator.prototype.isAcceptable = function (s) {
return s.length === 5 && numberRegexp.test(s);
};
return ZipCodeValidator;
}());
exports_1("ZipCodeValidator", ZipCodeValidator);
exports_1("mainValidator", ZipCodeValidator);
exports_1("default", {
name: 'a'
});
}
};
});
而这个 System 的导出是通过exports_1("ZipCodeValidator", ZipCodeValidator);
导出的。
第一个参数是导出名称,第二个参数是导出的引用。
现在,你应该都清楚,每一种模式转换之后的 js都是如何导出的了。
其实这些导出,都是挂载到某一个变量下面,集中管理,或许是对象,或许是数组,等需要用的时候,再去根据名字拿就是了。
外部模块与内部模块
外部模块,顾名思义,外,表示不属于内部的,对于 ts 语言来说,内部就是 ts 文件,此时的外代表着 js 文件。
我们知道引用 JS 文件,需要为它写 d.ts
文件,此时拥有d.ts
的文件,我们可以把它看做js + d.ts = .ts
。
而引用 js 或者 ts 文件需要 import
,我们把所有需要import
的都叫做引用外部模块。因为模块是基于文件的导入导出的,需要导入的就是来自外部的。
而内部模块就代表着 ts 内部的,同时它有一个别名叫做命名空间。命名空间的作用就是把一份代码分割到多个文件。
模块的寻找
其实跟 node 寻找模块一样,这里简单的说一下。
它会根据你写的相对路径去找文件。ts 因为需要代码提示,所以它通常会找d.ts
和.ts
文件。
假如你写的是绝对路径,比如import $ from 'jquery'
这种,我们知道 jquery 并没有.ts
版本的,但是有人提供了jquery
的 d.ts
文件。
在 ts2.0 版本之后,我们直接直接通过npm install @types/xxx
安装d.ts
文件。
当我们通过 npm install @types/jquery
安装之后,会在我们的node_modules
里面有个@types
文件夹,里面存放着我们的d.ts
文件。
当 ts
找不到的时候会来这个目录找,再找不到就报错了,当然你可以定一些它需要寻找的特定目录,比如说你创建一个专门存放d.ts
的文件夹,然后在tsconfig.json
配置一下。
tsconfig.json
就是我们编译ts
文件的编译选项。
小实验
首先创建一个文件夹ts-modules
通过tsc --init
生成 tsconfig.json
通过npm init -y
生成 package.json
再安装jquery
mkdir ts-modules
cd ts-modules
tsc --init
npm init -y
npm install jquery @types/jquery -S
创建 src
目录,进入再创建我们的main.ts
mkdir src
cd src
touch main.ts
你一定要清楚 @types/jquery
是 d.ts
而jquery
是 js
,只有这俩样合起来才能被正常使用。
还有一点你必须要明白,tsc
并不会打包代码。
在你的 main.ts
里面输入
import * as $ from "jquery";
$(document).html("Hello World!");
来到tsconfig.json
,添加一行。
"outDir": "./dist"
在你的终端里面
tsc
我们可以看到dist/main.js
"use strict";
var $ = require("jquery");
$(document).html("Hello World!");
此时的 jquery 并没有打包到该文件里面。
此时新建我们的modules.ts
和 namespace.ts
// modules.ts
export const modules_a = 123;
// namespace.ts
namespace OwnSpace{
export let var_a = 'own_space';
}
当我们在main.ts
里面输入 OwnSpace
的时候,我们发现出现了代码提示。
而modules.ts
里面导出的modules_a
则不会,这是模块与命名空间的一个小区别。
当我们给namespace.ts
添加一个导出的时候,立刻就报错了。
所有我们知道,包含 namespace
的外层不应该有 export
,要不然就变成模块了。
当然namespace
里面的变量只有导出才能被访问。
tsc
编译一下
来看看我们编译出来的命名空间
var OwnSpace;
(function (OwnSpace) {
OwnSpace.var_a = 'own_space';
})(OwnSpace || (OwnSpace = {}));
其实他就是自执行函数,然后给他挂载一些变量而已。
其实就跟这里演示的一样,通过var
其实就是挂载到了window
变量上面。
而多次通过var
声明并不会报错。
修改一下namespace.ts
namespace OwnSpace{
export let var_a = 'own_space_b';
}
namespace OwnSpace{
export let var_b = 'own_space_b';
let inner_b = '123';
}
编译出来的会像这样。
var OwnSpace;
(function (OwnSpace) {
OwnSpace.var_a = 'own_space_b';
})(OwnSpace || (OwnSpace = {}));
(function (OwnSpace) {
OwnSpace.var_b = 'own_space_b';
var inner_b = '123';
})(OwnSpace || (OwnSpace = {}));
只有 export
出来的变量才会挂载到OwnSpace
变量上面。
此时我们再新建一个name2.ts
namespace OwnSpace{
export let var_c = 'own_space_c';
}
编译之后,在 dist
目录新建一个 index.html
<meta charset="utf-8">
<script src='namespace.js'></script>
<script src='name2.js'></script>
<script>
console.log(OwnSpace);
</script>
用浏览器打开它,并打开控制台
namespace.js
挂载了var_a
和 var_b
name2.js
挂载了 var_c
我们看到所有的命名空间下面的东西都是通过.
来访问的,我们可以认为命名空间就是一个对象。
所以 namespace 有什么作用呢?
其实非常明显他就是往 window
上面挂载某些变量,就像 jquery
。
当你在 html 中引入<script src='xxxxxxx/jquery.js'></script>
它就会往window 上面挂载一个$
其实这样非常鸡肋,容易造成全局命名空间污染,而且现在都有打包工具了,没必要这么干,之所以会有 namespace
可能是为了方便为已有的js
库写d.ts
。
所以大多数时候你是用不到 namespace
的。
配置 d.ts
typescript 的代码核心就是d.ts
,所以说只要d.ts
写的好,走遍天下都不怕。
继续小实验
在之前的小实验里面,再新建俩个文件,some.d.ts
、some.js
// some.d.ts
declare const name : string;
export default name;
// some.js
export defalt const name = 'hello world';
在main.ts
文件里面
import name from './some';
这里我们使用的是相对路径。这样是正确无误的。
创建test.d.ts
,定义一个外部模块。外部模块是不是需要 import 的啊?
我们发现这里有一个双引号
declare module "lodash"{
export let version : string;
let _ : any;
export default _;
}
在 main.ts
中
import { version } from "lodash";
这里的双引号就和之前的对应,你会发现这里没有相对路径,而是直接 "lodash"
。
这里之所以能找到是因为它们在同一个目录。
而这寻找,他会遍历你项目里面的文件,你不信可以把这个文件移动到项目根目录下。
其实编译器会在 main.ts
文件的头部会自动添加一个这样的编译指令,尽管现在你是看不到它的。
这个指令会告诉编译器去哪寻找d.ts
文件,表示这个文件使用了d.ts
里面声明的名字; 并且,这个包要在编译阶段与声明文件一起被包含进来
/// <reference path="../test.d.ts" />
在你的 tscofig.json
里面配置这一项。
"listFiles": true
而且还可以配置去哪找,分别是 typeRoots
和 types
选项。
所有的选项都在这
https://www.tslang.cn/docs/handbook/compiler-options.html
而关于编译器怎么遍历.d.ts
目录。
在你的 tscofig.json
里面配置这一项。
"traceResolution": true
再次编译你就可以看到打印出了寻找路径。
里面还有非常多的配置选项,自己去尝试一下,算是留给大家的课后作业。
学习的最好办法就是尝试。