TypeScript学习笔记

257 阅读14分钟

概述

js开发中存在的问题

  1. 使用了不存在的变量、函数或成员;
  2. 把一个不确定的类型当做一个确定的类型处理;
  3. 在使用null或undefined的成员。(TypeError: Cannot- read property 'name' of undefined,在统计的前端开发排名前10的错误中,这个错误排第一);

js和ts代码对比

  1. 看下面简单的一段程序,将姓名的首字母大写后反转返回;
    下面的程序中有几处错误,如果在js中,我们只有在代码运行时才能发现错误;
function getUserName() {
    if (Math.random() < 0.5) {
        return 'zhang yin'
    };

    return 404;
}

let myname = getUsername();
mynema = myname.split(' ').map(el => el[0].touppercase + el.subStr(1)).join(' ');
  1. 现在什么都不用做,只把js后缀换成ts,错误立刻就显示出来了,ts不会等到执行时才报错。

ts代码

  • 点击快速修复

ts代码

  • 还有错误,提示myname可能是string或者number,number上没有split方法。加判断

ts代码

  • 还有错误,一直点快速修复

ts代码

ts代码

  • 完美,错误全部修复

ts代码

js的原罪

  1. js语言本身的特点,决定了该语言无法适应大型的复杂的项目;
  2. 弱类型:某个变量,可以随时更换类型;
  3. 解释性:错误发生的时间,是在运行时;

前端开发中,大部分时间都是在排错;

TypeScript (ts = js + 类型系统)

  1. TypeScript是js的超集,是一个可选的、静态的类型系统;
  2. 超集:包含的关系,整数是正整数的超集;
  3. 类型系统:对代码中所用的标识符(变量、函数、参数、返回值)进行类型检查;
  4. 可选的,学习曲线相对平滑;
  5. ts不参与任何运行时的类型检查;
  6. ts在编写时报错,js在执行时报错;

ts的常识

  1. 2012年微软发布;
  2. Anders Hejlsberg 负责开发ts项目;
  3. 开源的,拥抱ES标准;

额外的惊喜

  1. 有了类型检查,无形中增强了面向对象的开发;
  2. js中也有类和对象,js支持面向对象开发,没有类型检查,很多面向对象的场景实现起来会有诸多问题;
  3. 使用ts后,可以编写出完善的面向对象代码;

参考文献

在node中搭建ts开发环境

安装TypeScript

npm i typescript -g

将ts编译成js

tsc index.ts

默认情况下,TS会做出下面几种假设:

  1. 假设当前的执行环境是浏览器环境(Dom);
  2. 如果代码中没有使用模块化语句(import、export),便认为该代码是时全局执行;
  3. 默认编译的目标代码是ES3;

解决方法

  1. 使用tsc命令行的时候,加上选项参数;
  2. 使用ts配置文件,更改编译选项;

TypeScript的配置文件

生成tsconfig.json

tsc --init

使用了配置文件后,使用tsc进行编译时,不能跟上文件名,如果跟上文件名,会忽略配置文件

@types/node

npm i -D @types/node

@types/node是一个ts官方的类型库,其中包含了很多对js代码的类型描述;

例:
JQuery:用js写的,没有类型检查;
安装@types/jquery,为jquery库添加类型定义;

使用第三方库简化流程

ts-node:将ts代码在内存中完成编译,同时完成运行

npm i ts-node -g ts-node /src/index.ts

nodemon:用于检测文件的变化

npm i nodemon -g
nodemon --exec ts-node sec/index.ts

写成script脚本:

"dev": "nodemon --watch src -e ts --exec ts-node src/index.ts" // 只监控src下的ts文件

基本类型检查

类型约束和编译结果对比

编译结果中没有类型约束信息;

如何进行类型约束

  1. ts 是一个可选的静态类型系统;
  2. ts 在很多场景中可以完成类型推导;
  3. any 表示任意类型,ts 不进行类型约束;
  4. 如何区分数字字符串和数字,关键看怎么读?如果按照数字的方式朗读,则为数字,否则为字符串

基本类型检查

  1. number
  2. string
  3. boolean
  4. array
  5. object
  6. null 和 undefined 是所有其它类型的子类型,它们可以复制给其它类型;
    "strictNullChecks": true, 可以获得更严格的空类型检查,null 和 undefined 只能赋值给自身

其它常用类型

  1. 联合类型:多种类型任选其一 ;
  • 配合类型保护进行判断;
  • 类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof 可以触发类型保护;
let name: string | undefined = undefined;
if (typeof name === "string") {
    name.toUpperCase();
};
  1. void 类型:通常用于约束函数的返回值,表示该函数没有任何返回;
  2. never 类型:通常用于约束函数的返回值,表示该函数永远不会结束;
function demo () {
    throw new Error("错误");
    console.log("永远不会执行");
};
function demo1 () {
    while(true){
        cconsole.log('never')
    }
};
  1. 字面量类型: 使用一个值进行约束
let arr: []; // arr永远只能取值为一个空数组
let gender: '男' | '女';
let user: {
    name: string
    age: number
} // 一个对象,必须要name和age
  1. 元祖类型(Tuple):一个固定类型的数组,并且数组中每一项的类型确定
let arr: [string, number];  
arr = ['zy', 21];
  1. any类型:可以绕过类型检查,any类型可以赋值给任意类型
let name: any = 'zhangyin';
let age: number = any; 

类型别名

类型别名:对已经类型定义名称;

type Gender = 'man' | 'womam';
type User = {
  name: string
  age: number
  gender: Gender
}
let person: User;
person = {
  name: 'zhangyin',
  gender: 'man',
  age: 21,
}; 
function getUsers (g: Gender): User[] {
  return[];
}

函数的相关约束

  1. 函数重载:在函数实现之前,对函数调用的多种方式进行声明
function combine (a: number, b: number): number;
function combine (a: string, b: string): string;
function combine (a: number | string, b: number | string): number | string {
  if (typeof a === 'number' && typeof b === 'number') {
    return a * b;
  }
  else if (typeof a === 'string' && typeof b === 'string') {
    return a + b;
  }
  throw new Error('a和b必须是相同的类型');
}
const result = combine(3, 4);
  1. 可选参数和默认值:可以在某些参数后加上问号,表示该参数可以不用传递
function sum (a: number, b: number, c?: number = 0) {
  if (c) {
   return a + b + c;
  } else {
    return a + b;
  }
};

demo:创建并打印扑克牌

目标:创建一副扑克牌(不包括大小王),打印该扑克牌;

type Deck = Card[];
type Color = "♠" | "♥" | "♣" | "♦";
type Card = {
  color: Color;
  mark: number;
};

function createDeck(): Deck {
  const deck: Deck = [];
  for (let i = 1; i <= 13; i++) {
    deck.push({
      mark: i,
      color: "♠"
    });

    deck.push({
      mark: i,
      color: "♣"
    });

    deck.push({
      mark: i,
      color: "♥"
    });

    deck.push({
      mark: i,
      color: "♦"
    });
  }
  return deck;
}

function printDeck(deck: Deck) {
  let deckStr = "";
  deck.forEach((el, i) => {
    if ((i + 1) % 6 === 0) {
      deckStr += el.color + el.mark + "\n";
    } else {
      deckStr += el.color + el.mark + "\t";
    }
  });

  console.log(deckStr);
}

printDeck(createDeck());

打印结果

结果

扩展类型-枚举

枚举

定义:枚举是扩展类型中的一种,枚举通常用于约束某个变量的取值范围。

字面量和联合类型配合使用,也可以达到相同的目的。

扩展类型:类型别名、枚举、接口、类

字面量类型的问题

  1. 在类型约束位置,会产生重复代码;
let gender: "男" | "女";
gender = "男";
gender = "女";

function user (g: "男" | "女) {};

可以使用类型别名解决问题

type Gender = "男" | "女" ; // 将 “男” | “女” 抽离成类型别名,避免代码重复
let gender: Gender;
gender = "男";
gender = "女";

function user (g: Gender) {};
  1. 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。(无法用类型别名解决);
  2. 字面量类型不会进入到编译结果;

枚举可以完美的解决以上的问题。

如何定义一个枚举

enum 枚举名{
  枚举字段1 = 值1,
  枚举字段2 = 值2,
  ...
}

enum Gender{
  male = "男", // male是逻辑名称,“男”是真实的值,也可能是先生,帅哥
  female = "女", // female是逻辑名称,“女”是真实的值,也可能是女士、美女
}

let gender: Gender;
gender = Gender.male; // 赋值时赋的是逻辑名称
gender = Gender.female;

枚举会参与编译,会在编译结果中出现,在编译结果中表现为对象

var Gender;
(function (Gender) {
    Gender["male"] = "\u7537";
    Gender["female"] = "\u5973";
})(Gender || (Gender = {}));
let gender;
gender = Gender.male;
gender = Gender.female;

枚举的规则

  1. 枚举的字段值可能是字符串或数字
  2. 数字枚举的值会自动递增
  3. 被数字枚举约束的变量,可以直接赋值为数字

企业开发经验

  1. 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  2. 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值

demo:使用枚举优化扑克牌

 
// 枚举改写扑克牌
type Deck = Card[];
type Card = {
  color: Color,
  mark: Mark,
}
  
enum Color {
  heart = "♥",
  spade = "♠",
  club = "♣",
  diamond = "♦",
}
enum Mark {
  A = "A",
  two = "2",
  three = "3",
  four = "4",
  five = "5",
  six = "6",
  seven = "7",
  eight = "8",
  nine = "9",
  ten = "10",
  eleven = "J",
  twelve = "Q",
  thirteen = "K",
}

function createDeck (): Deck {
  const deck : Deck = [];

  const marks = Object.values(Mark);
  const colors = Object.values(Color);

  for (const m of marks) {
      for (const c of colors ) {
        deck.push({
          color: c,
          mark: m
        })
      }
  }

  return deck;
}

function printDeck(deck: Deck) {
  let result = '\r';
  deck.forEach((card, i) => {
    let str = card.color + card.mark;
    result += str + '\t';
    if ((i+1)%6 === 0) {
      result += '\n';
    }
  })
  console.log(result);
}

const deck = createDeck();
printDeck(deck);

打印出来的结果

结果

扩展知识-枚举的位运算

一个文件有4种权限,读、写、删、创建

写成枚举

enum Permission {
  Read = 1, // 0001
  Write = 2, // 0010
  Create = 4, // 0100
  Delete = 8 // 1000
}
  1. 如何组合权限 或运算:参加运算的两个数据,按二进制位进行“与”运算,有一位是1就得1;
let p: Permission = Permission.Read | Permission.Write;
  1. 如何判断是否拥有某个权限 与运算:参加运算的两个数据,按二进制位进行“与”运算,全部为1才得1;
function hasPermission(target: Permission, per: Permission): boolean {
  return (target & per) === per;
}
// 例:判断p是否有可读的权限
hasPermission(p, Permission.Read);
  1. 如何删除某个权限 // 异或运算:参加运算的两个数据,按二进制位进行“与”运算,相同取零,不同取一;
p = p ^ Permission.Write;

模块化

关于模块化的相关配置:

配置名称含义
module设置编译结果中使用的模块化标准
moduleResolution设置解析模块的模式
noImplicitUseStrict编译结果中不包含"use strict"
removeComments编译结果移除注释
noEmitOnError错误时不生成编译结果
esModuleInterop启用es模块化交互非es模块导出

TS中如何书写模块化语句

最佳实践:TS中,导入和导出模块,统一使用ES6的模块化标准


不使用ES6模块化标准(不推荐)


# 编译结果中的模块化

可以在tsconfig.json文件配置

TS中的模块化在编译结果中:

- 如果编译结果的模块化标准是ES6: 没有区别
- 如果编译结果的模块化标准是commonjs:导出的声明会变成exports的属性,默认的导出会变成exportsdefault属性;

# 如何在TS中书写commonjs模块化代码

导出:export = xxx

导入:import xxx = require("xxx")

# 模块解析

模块解析:应该从什么位置寻找模块

TS中,有两种模块解析策略

- classic:经典
- node:node解析策略(唯一的变化,是将js替换为ts)
  - 相对路径```require("./xxx")```
  - 非相对模块```require("xxx")```

接口

接口:inteface

扩展类型:类型别名、枚举、接口、类

TypeScript的接口:用于约束类、对象、函数的契约(标准)

契约(标准)的形式:

  • API文档,弱标准
  • 代码约束,强标准

和类型别名一样,接口,不出现在编译结果中

  1. 接口约束对象

  2. 接口约束函数

类型兼容性

B->A,如果能完成赋值,则B和A类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配

  • 对象类型:鸭子辨型法

类型断言

当直接使用对象字面量赋值的时候,会进行更加严格的判断

  • 函数类型

参数:传递给目标函数的参数可以少,但不可以多

返回值:要求返回必须返回;不要求返回,你随意;

TS中的类

面向对象思想

基础部分,学习类的时候,仅讨论新增的语法部分。

属性

使用属性列表来描述类中的属性

属性的初始化检查

strictPropertyInitialization:true

属性的初始化位置:

  1. 构造函数中
  2. 属性默认值

属性可以修饰为可选的

属性可以修饰为只读的

使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:暂时不讲

Symble

属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写

访问器

作用:用于控制属性的读取和赋值

泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值

在函数中使用泛型

在函数名之后写上<泛型名称>

如何在类型别名、接口、类中使用泛型

直接在名称后写上<泛型名称>

泛型约束

泛型约束,用于现实泛型的取值

多泛型

深入理解类和接口

面向对象概述

为什么要讲面向对象

  1. TS为前端面向对象开发带来了契机

JS语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误,尽管可以使用注释或文档或记忆力,但是它们没有强约束力。

TS带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是据有强约束力的。

  1. 面向对象中有许多非常成熟的模式,能处理复杂问题

在过去的很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验。

什么是面向对象

面向对象:Oriented(基于) Object(事物),简称OO。

是一种编程思想,它提出一切以类对切入点思考问题。

其他编程思想:面向过程、函数式编程

学开发最重要最难的是什么?思维

面向过程:以功能流程为思考切入点,不太适合大型应用

函数式编程:以数学运算为思考切入点

面向对象:以划分类为思考切入点。类是最小的功能单元

类:可以产生对象的模板。

如何学习

  1. TS中的OOP (面向对象编程,Oriented Object Programing)
  2. 小游戏练习

理解 -> 想法 -> 实践 -> 理解 -> ....

类的继承

继承的作用

继承可以描述类与类之间的关系

坦克、玩家坦克、敌方坦克 玩家坦克是坦克,敌方坦克是坦克

如果A和B都是类,并且可以描述为A是B,则A和B形成继承关系:

  • B是父类,A是子类
  • B派生A,A继承自B
  • B是A的基类,A是B的派生类

如果A继承自B,则A中自动拥有B中的所有成员

@startuml

Tank <|-- PlayerTank
Tank <|-- EnemyTank
EnemyTank <|-- BossTank

@enduml

成员的重写

重写(override):子类中覆盖父类的成员

子类成员不能改变父类成员的类型

无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。

注意this关键字:在继承关系中,this的指向是动态——调用方法时,根据具体的调用者确定this指向

super关键字:在子类的方法中,可以使用super关键字读取父类成员

类型匹配

鸭子辨型法

子类的对象,始终可以赋值给父类

面向对象中,这种现象,叫做里氏替换原则

如果需要判断一个数据的具体子类类型,可以使用instanceof

protected修饰符

readonly:只读修饰符

访问权限修饰符:private public protected

protected: 受保护的成员,只能在自身和子类中访问

单根性和传递性

单根性:每个类最多只能拥有一个父类

传递性:如果A是B的父类,并且B是C的父类,则,可以认为A也是C的父类