六、枚举
1、普通枚举
枚举常使用于我们在程序中需要做权限管理或者做判断时等各种场景。枚举比较简单,下面直接用代码演示:
enum Direction{ Up, Down, Left, Right } console.log(Direction.Up) //0 console.log(Direction.Down) //1 console.log(Direction.Left) //2 console.log(Direction.Right) //3 console.log(Direction[0]) //Up 复制代码
除了以上基本用法外,我们还可以给枚举赋值:
enum Direction{ Up = 10, Down, Left, Right } console.log(Direction.Up) //10 复制代码
2、常量枚举
我们来定义一个常量,与 enum
做判断。
enum Direction{ Up = 'Up', Down = 'Down', Left = 'Left', Right = 'Right' } //定义一个常量,直接与enum做判断 const value = 'Up'; if(value === Direction.Up){ console.log('go Up!') // go Up! } 复制代码
使用常量枚举可以有效地提升性能,常量会内联枚举的任何用法,而不会把枚举变成任意的 Javascript
代码。
这样一说,那是不是所有的 enum
都可以使用常量枚举呢?答案自然是否定的。
枚举的值有两种类型,一种是常量值枚举(constant),一种是计算值枚举(computed)。只有常量值枚举可以进行常量枚举,而计算值枚举不能进行常量枚举。
七、泛型
接下来我们来讲 TypeScript
中最难的一部分,泛型。
1、普通泛型
泛型,即 generics
。指在定义函数、接口或类的时候,我们不预先指定类型,而是在使用的时候再指定类型和其特征。
可以理解为,泛型就相当于一个占位符或者是一个变量,在使用时再动态的填入进去,填进去以后既可以来确定我们的类型值。
接下来我们用代码来演示一下:
function echo<T>(arg: T): T{ return arg } const result = echo(true) console.log(result) // true 复制代码
我们通过 <>
来定义一个未知的泛型,之后当我们给它赋值时,就可以对应值的数据类型。
现在我们再用泛型来定义一个 number
类型的数组。具体代码如下:
// 早期定义一个number类型的数组 let arr: number[] = [1, 2, 3] // 用泛型定义一个number类型的数组 let arrTwo: Array<number> = [1, 2, 3] 复制代码
假如我们现在要调换两个元素的位置,那么用泛型我们可以这么实现。具体代码如下:
function swap<T, U>(tuple: [T, U]): [U, T]{ return [tuple[1], tuple[0]] } const result2 = swap(['abc', 123]) console.log(result2[0]) //123 console.log(result2[1]) //abc 复制代码
通过泛型,我们就顺利调换了两个元素的位置。
2、约束泛型
在泛型中使用 extends
关键字,就可以让传入值满足我们特定的约束条件,而不是毫无理由的随意传入。
比如,我们想要让我们定义的内容是一个数组,我们可以这么处理。具体代码如下:
function echoWithArr<T>(arg: T[]): T[]{ console.log(arg.length) return arg } const arrs = echoWithArr([1, 2, 3]) 复制代码
这样,就把 arrs
定义为一个数组。
假设我们现在想要让我们定义的内容可以访问到 length
方法,那么我们需要加一点佐料。具体代码如下:
interface IWithLength{ length: number } function echoWithLength<T extends IWithLength>(arg: T): T{ console.log(arg.length) return arg } const str = echoWithLength('str') const obj = echoWithLength({ length: 10, width: 20 }) const arr2 = echoWithLength([1, 2, 3]) 复制代码
通过 extends
关键字来继承特定的 interface
,使得我们定义的内容 str
, obj
,arr2
达到可以访问length方法的目的。
通过以上举例,我们可以知道,泛型可以用来灵活的约束参数的类型,参数不需要是个特别死板的类型,而可以通过我们的约束来达到我们想要的目的。
3、泛型在类和接口中的使用
(1)泛型在类中的使用
class Queue<T>{ private data = [] push(item: T){ return this.data.push(item) } pop(): T{ return this.data.shift() } } // 确定这是一个number类型的队列 const queue = new Queue<number>() queue.push(1) console.log(queue.pop().toFixed()) 复制代码
(2)泛型在接口中的使用
interface KeyPair<T, U>{ key: T value: U } let kp1: KeyPair<number, string> = {key: 1, value: 'str'} let kp2: KeyPair<string, number> = {key: 'str', value: 2} 复制代码
通过以上代码演示可以发现,泛型就像是创建了一个拥有特定类型的容器,就仿佛给一个容器贴上标签一样。
4、泛型中keyof语法的使用
interface Person { name: string; age: number; gender: string; } // type T = 'name'; // key: 'name'; // Person['name']; // type T = 'age'; // key: 'age'; // Person['age']; // type T = 'gender'; // key: 'gender'; // Person['gender']; class Teacher { constructor(private info: Person) {} // keyof关键字 getInfo<T extends keyof Person>(key: T): Person[T] { return this.info[key]; } } const teacher = new Teacher({ name: 'Monday', age: 18, gender: 'female' }); const test = teacher.getInfo('age'); console.log(test); 复制代码
八、类型别名
1、类型别名
类型别名,即 type aliase
。类型别名可以看作是一个快捷方式,可以把一个写起来很繁琐的类型创建一个简单的写法。比如:
//用以下这种写法,每次都要写一长串的(x: number, y: number) => number let sum: (x: number, y: number) => number const result = sum(1, 2) //用type给类型进行别名 type PlusType = (x: number, y: number) => number let sum2: PlusType const result2 = sum2(2, 3) //一个类型可以是字符串也可以是数字 type StrOrNumber = string | number let result3: StrOrNumber = '123' result3 = 123 复制代码
2、字符串字面量
字符串字面量,指可以提供一系列非常方便使用的常量写法。比如:
const str: 'name' = 'name' const number: 1 = 1 type Direction = 'Up' | 'Down' | 'Left' | 'Right' let toWhere: Direction = 'Left' 复制代码
3、交叉类型
交叉类型,使用 type
这个扩展对象的一种方式。比如:
interface IName{ name: string } type IPerson = IName & {age: number} let person: IPerson = {name: 'monday', age: 18} 复制代码
九、命名空间
1、namespace是什么
假设我现在有一个命名空间,那么命名空间里面的内容将不会被暴露出去。如下代码所示:
namespace Home { class Header { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Header'; document.body.appendChild(elem); } } class Content { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Content'; document.body.appendChild(elem); } } class Footer { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Footer'; document.body.appendChild(elem); } } class Page { constructor() { new Header(); new Content(); new Footer(); } } } 复制代码
在上面的代码中,有一个命名为 Home
的命名空间,里面一共有 4
个类。那么这个时候,我们想要在外部使用其中的某一个类,是没有办法的。
那如果我们想要将命名空间里面的某个类给暴露出去,该怎么处理呢?
通常情况下,我们会在类的前面加上 export
关键字。类似下面这样:
namespace Home { // 加上 export 关键字 export class Header { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Header'; document.body.appendChild(elem); } } class Content { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Content'; document.body.appendChild(elem); } } class Footer { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Footer'; document.body.appendChild(elem); } } class Page { constructor() { new Header(); new Content(); new Footer(); } } } 复制代码
大家可以看到,加上 export
关键字,就可以将该类在外部进行暴露。而没有加 export
关键字的,在外部就依然是访问不到的。
现在我们来看下它具体如何使用。具体代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> <script src="./dist/page.js"></script> </head> <body> <script> new Home.Header(); </script> </body> </html> 复制代码
大家可以看到,通过 new Home.Header();
的方式,我们可以访问到命名空间 Home
中暴露出来的 Header
类。而其他没有加 export
关键字的,都是无法正常访问到的,它让本来全局的四个变量变得只剩下 Home
一个,这就是命名空间 namespace
给我们带来的好处。
2、namespace的好处
现在,我们来梳理一下命名空间给我们带来的好处。namespace
给我们带来的一个好处就是,用一个类似模块化的开发方式,让我们能够尽可能少的去生成全局变量。或者说,把一组相关的内容封装在一块,最终对外提供统一的暴露接口。
3、依赖命名空间声明
假设我们要在一个命名空间里面去引入另一个命名空间,该怎么使用呢?如下代码所示:
// Home这个命名空间要去依赖其他命名空间的声明 ///<reference path='./components.ts' /> namespace Home { export class Page { user: Components.User = { name: 'monday' } constructor() { new Components.Header(); new Components.Content(); new Components.Footer(); } } } 复制代码
我们可以通过 ///<reference path='./components.ts' />
这种方式,去引入 Components
命名空间,以供我们使用。
十、声明文件
我们在写ts时,难免会有遇到要引入第三方库的时候。这个时候就需要ts来做特殊处理。主要有以下两种做法:
1、 .d.ts 文件引入
假设我们要引入 JQuery
库来使用,那么我们可以在外部新增一个 JQuery.d.ts
文件,文件内代码如下:
// 第一种类型:定义全局变量 declare var JQuery: (selector: string) => any; // 第二种类型:定义全局函数→传递函数 declare function $(readFunc: () => void): void; // 第三种类型:定义全局函数→传递字符串 interface JQueryInstance { html: (html: string) => {}; } declare function $(selector: string): JQueryInstance; 复制代码
之后便可以在我们定义的 ts
文件下引用 JQuery
相关库的内容。比如:
$(function() { $('body').html('<div>123</div>'); }); 复制代码
2、使用 interface 语法实现函数重载
上面的第二和第三种类型,使用了传递函数和传递字符串的两种方式,来实现了函数重载。那么下面,我们来用 interface
语法,来改造一下这种函数重载的方式。 .d.ts
文件代码如下:
interface JQueryInstance { html: (html: string) => JQueryInstance; } // 使用interface的语法,实现函数重载 interface JQuery { (readFunc: () => void): void; (selector: strring): JQueryInstance; } declare var $: JQuery; 复制代码
大家可以看到,我们通过 interface
的方式,将 readFunc
和 selector
给一起并入 JQuery
接口中,最后我们直接将 JQuery
给暴露出去即可。
3、声明对象
上面我们遇到的是 $
只是函数的时候,进行的函数重载。那如果此时的 $
既要当作是函数使用,又要当作是对象使用呢?假设我们现在有这么一段代码:
$(function() { $('body').html('<div>123</div>'); new $.fn.init(); }) 复制代码
现在,我们要在 $
中去访问到 fn
和 init
,那这个时候 $
不仅要当作是函数来使用,还要当作是对象来使用。具体我们可以在 .d.ts
文件中这么处理。具体代码如下:
interface JQueryInstance { html: (html: string) => JQueryInstance; } // 函数重载 declare function $(readyFunc: () => void): void; declare function $(selector: string): JQueryInstance; // 如何对对象进行类型定义,以及对类进行类型定义,以及命名空间的嵌套 declare namespace $ { namespace fn { class init {} } } 复制代码
大家可以看到,我们定义了命名空间,并在命名空间里卖弄继续嵌套命名空间,同时,用 class
类进行了类型定义。最终,我们就可以成功访问到 $.fn.init()
啦!
4、npm模块化引入
我们也可以安装对应的第三方库的 npm
包,这个包是类型定义文件。假如我们现在要引入一个 JQuery
库,那么我们可以这么处理。
npm install --save @type/jquery 复制代码
接下来我们对 .d.ts
文件进行改造,具体代码如下:
// ES6 模块化 declare module 'jquery' { interface JQueryInstance { html: (html: string) => JQueryInstance; } // 混合类型 function $(readyFunc: () => void): void; function $(selector: string): JQueryInstance; namespace $ { namespace fn { class init {} } } export = $; } 复制代码
最后,来到我们想要引入 $
的 ts
文件中。具体代码如下:
import $ from 'jquery'; $(function() { $('body').html('<div>123</123>'); new $.fn.init(); }); 复制代码
十一、内置类型
我们在写 ts
代码时,其实不知不觉已经使用了很多的内置对象。对象呢,是指根据标准(标准指 ECMA
、 DOM
等标准),在全局作用域 global
上面存在的对象。那我们在运行 tsc
时,这些内置的对象就会被当作附加的礼物给程序加载进行。接下来我们来体会一下几种常见的内置对象。
全局对象:
// global object 全局对象 const a: Array<number> = [1, 2, 3] const date = new Date() date.getTime() const reg = /abc/ reg.test('abc') 复制代码
内置对象:
// build-in object 内置对象 Math.pow(2, 2) 复制代码
DOM和BOM对象:
// DOM 和 BOM let body = document.body let allLis = document.querySelectorAll('li') allLis.keys() document.addEventListener('click', e => { e.preventDefault() }) 复制代码
功能性类型:
// Utility Types 功能性类型 interface IPerson{ name: string age: number } let monday: IPerson = {name: 'monday', age: 20} //可选属性 type IPartial = Partial<IPerson> let monday2: Ipartial = {name: 'monday'} //移除某一个属性 type Omit = Omit<IPerson, 'name'> let monday3: Omit = {age: 20} 复制代码
十二、TypeScript中的配置文件
往往我们在刚初始化一个 ts
项目时,都会先运行 tsc --init
,之后呢,会生成一个 tsconfig.json
文件。在这个文件下呢,有很多的配置。那接下来,我们就来分析下其中一些比较值得注意的配置项。
{ "include": ["./demo.ts"], // 只编译 ./demo.ts 文件 "exclude": ["./demo.ts"], // 不编译 ./demo.ts 文件 "files": ["./demo.ts"], // 只编译 ./demo.ts 文件 "removeComments": true, // 表示打包时移除掉ts文件中的注释 "noImplicityAny": true, // 当设置为true时,表示所有的参数都应该设置类型,否则会报错;当设置为false时,则不要求显式地设置any "strictNullChecks": true, // 当设置为true时,表示强制检查null类型;当设置为false时,则表示不强制检查null类型 "rootDir": "./src", // 指定输入的文件的地址 "outFile": "./build/page.js", // 将所有文件最终统一打包到build目录下的page.js文件里 "outDir": "./build", // 指定输出的文件的地址 "incremental": true, // 增量编译,即之前编译过的现在就不用编译了 "allowJs": true, // 允许指定文件夹下的所有文件进行编译 "checkJs": true, // 对编译后的js文件进行语法检测 "sourceMap": true, // 编译后的结果再生成一个 .map 文件 "noUnusedLocals": true, // 对写出的多余的,但是又没有实际作用的代码进行提示 "noUnuesdParameters": true, // 对函数的参数进行校验,如果函数中的参数没有进行使用,则会错误提示 "baseUrl": "./", // TypeScript下的根路径是什么路径 } 复制代码
十三、在ts中对代码进行模块化组织
上面我们看着命名空间的使用方法似乎还有一点麻烦。那事实上,在 ts
中,我们还可以对代码进行模块化组织,通常是通过 import
语句来处理。怎么处理呢?
1、项目结构
2、模块化拆分
第一步,先来定义 src|components.ts
下的代码。具体代码如下:
export class Header { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Header'; document.body.appendChild(elem); } } export class Content { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Content'; document.body.appendChild(elem); } } export class Footer { constructor() { const elem = document.createElement('div'); elem.innerText = 'This is Footer'; document.body.appendChild(elem); } } 复制代码
第二步,使用模块化的方式进行调用。在 src|page.ts
文件下,具体代码如下:
import { Header, Content, Footer } from './components'; export class Page { constructor() { new Header(); new Content(); new Footer(); } } 复制代码
大家可以看到,上面我们使用了 import
语句,来将类进行模块化调用。
第三步,在项目的 index.html
中引用,最终运行。具体代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js"></script> <script src="./build/page.js"></script> </head> <body> <script> require(['page'], function (page) { new page.Page(); }); </script> </body> </html> 复制代码
大家可以看到,如果是在没有使用 webpack
等打包工具的情况下,那么我们需要使用 cdn
的方式去引入一个 require
的库,以便于后续可以使用 require
这种语法。
最终我们来看下浏览器的显示效果。如下图:
可以看到,最终展示除了我们想要的效果。那在上面中,我们就简单了解了在 TypeScript
中,如何通过 import
语句来对模块进行拆分和组合。
十四、结束语
关于 ts
的入门讲到这里就结束啦!希望大家能对 ts
有一个简单的认识!
如果这篇文章对你有用,记得留个脚印jio再走哦~