概述
js开发中存在的问题
- 使用了不存在的变量、函数或成员;
- 把一个不确定的类型当做一个确定的类型处理;
- 在使用null或undefined的成员。(TypeError: Cannot- read property 'name' of undefined,在统计的前端开发排名前10的错误中,这个错误排第一);
js和ts代码对比
- 看下面简单的一段程序,将姓名的首字母大写后反转返回;
下面的程序中有几处错误,如果在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(' ');
- 现在什么都不用做,只把js后缀换成ts,错误立刻就显示出来了,ts不会等到执行时才报错。
- 点击快速修复
- 还有错误,提示myname可能是string或者number,number上没有split方法。加判断
- 还有错误,一直点快速修复
- 完美,错误全部修复
js的原罪
- js语言本身的特点,决定了该语言无法适应大型的复杂的项目;
- 弱类型:某个变量,可以随时更换类型;
- 解释性:错误发生的时间,是在运行时;
前端开发中,大部分时间都是在排错;
TypeScript (ts = js + 类型系统)
- TypeScript是js的超集,是一个可选的、静态的类型系统;
- 超集:包含的关系,整数是正整数的超集;
- 类型系统:对代码中所用的标识符(变量、函数、参数、返回值)进行类型检查;
- 可选的,学习曲线相对平滑;
- ts不参与任何运行时的类型检查;
- ts在编写时报错,js在执行时报错;
ts的常识
- 2012年微软发布;
- Anders Hejlsberg 负责开发ts项目;
- 开源的,拥抱ES标准;
额外的惊喜
- 有了类型检查,无形中增强了面向对象的开发;
- js中也有类和对象,js支持面向对象开发,没有类型检查,很多面向对象的场景实现起来会有诸多问题;
- 使用ts后,可以编写出完善的面向对象代码;
参考文献
在node中搭建ts开发环境
安装TypeScript
npm i typescript -g
将ts编译成js
tsc index.ts
默认情况下,TS会做出下面几种假设:
- 假设当前的执行环境是浏览器环境(Dom);
- 如果代码中没有使用模块化语句(import、export),便认为该代码是时全局执行;
- 默认编译的目标代码是ES3;
解决方法
- 使用tsc命令行的时候,加上选项参数;
- 使用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文件
基本类型检查
类型约束和编译结果对比
编译结果中没有类型约束信息;
如何进行类型约束
- ts 是一个可选的静态类型系统;
- ts 在很多场景中可以完成类型推导;
- any 表示任意类型,ts 不进行类型约束;
- 如何区分数字字符串和数字,关键看怎么读?如果按照数字的方式朗读,则为数字,否则为字符串
基本类型检查
- number
- string
- boolean
- array
- object
- null 和 undefined 是所有其它类型的子类型,它们可以复制给其它类型;
"strictNullChecks": true, 可以获得更严格的空类型检查,null 和 undefined 只能赋值给自身
其它常用类型
- 联合类型:多种类型任选其一 ;
- 配合类型保护进行判断;
- 类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof 可以触发类型保护;
let name: string | undefined = undefined;
if (typeof name === "string") {
name.toUpperCase();
};
- void 类型:通常用于约束函数的返回值,表示该函数没有任何返回;
- never 类型:通常用于约束函数的返回值,表示该函数永远不会结束;
function demo () {
throw new Error("错误");
console.log("永远不会执行");
};
function demo1 () {
while(true){
cconsole.log('never')
}
};
- 字面量类型: 使用一个值进行约束
let arr: []; // arr永远只能取值为一个空数组
let gender: '男' | '女';
let user: {
name: string
age: number
} // 一个对象,必须要name和age
- 元祖类型(Tuple):一个固定类型的数组,并且数组中每一项的类型确定
let arr: [string, number];
arr = ['zy', 21];
- 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[];
}
函数的相关约束
- 函数重载:在函数实现之前,对函数调用的多种方式进行声明
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);
- 可选参数和默认值:可以在某些参数后加上问号,表示该参数可以不用传递
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());
打印结果
扩展类型-枚举
枚举
定义:枚举是扩展类型中的一种,枚举通常用于约束某个变量的取值范围。
字面量和联合类型配合使用,也可以达到相同的目的。
扩展类型:类型别名、枚举、接口、类
字面量类型的问题
- 在类型约束位置,会产生重复代码;
let gender: "男" | "女";
gender = "男";
gender = "女";
function user (g: "男" | "女) {};
可以使用类型别名解决问题
type Gender = "男" | "女" ; // 将 “男” | “女” 抽离成类型别名,避免代码重复
let gender: Gender;
gender = "男";
gender = "女";
function user (g: Gender) {};
- 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。(无法用类型别名解决);
- 字面量类型不会进入到编译结果;
枚举可以完美的解决以上的问题。
如何定义一个枚举
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;
枚举的规则
- 枚举的字段值可能是字符串或数字
- 数字枚举的值会自动递增
- 被数字枚举约束的变量,可以直接赋值为数字
企业开发经验
- 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
- 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值
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;
let p: Permission = Permission.Read | Permission.Write;
- 如何判断是否拥有某个权限 与运算:参加运算的两个数据,按二进制位进行“与”运算,全部为1才得1;
function hasPermission(target: Permission, per: Permission): boolean {
return (target & per) === per;
}
// 例:判断p是否有可读的权限
hasPermission(p, Permission.Read);
- 如何删除某个权限 // 异或运算:参加运算的两个数据,按二进制位进行“与”运算,相同取零,不同取一;
p = p ^ Permission.Write;
模块化
关于模块化的相关配置:
| 配置名称 | 含义 |
|---|---|
| module | 设置编译结果中使用的模块化标准 |
| moduleResolution | 设置解析模块的模式 |
| noImplicitUseStrict | 编译结果中不包含"use strict" |
| removeComments | 编译结果移除注释 |
| noEmitOnError | 错误时不生成编译结果 |
| esModuleInterop | 启用es模块化交互非es模块导出 |
TS中如何书写模块化语句
最佳实践:TS中,导入和导出模块,统一使用ES6的模块化标准
不使用ES6模块化标准(不推荐)
# 编译结果中的模块化
可以在tsconfig.json文件配置
TS中的模块化在编译结果中:
- 如果编译结果的模块化标准是ES6: 没有区别
- 如果编译结果的模块化标准是commonjs:导出的声明会变成exports的属性,默认的导出会变成exports的default属性;
# 如何在TS中书写commonjs模块化代码
导出:export = xxx
导入:import xxx = require("xxx")
# 模块解析
模块解析:应该从什么位置寻找模块
TS中,有两种模块解析策略
- classic:经典
- node:node解析策略(唯一的变化,是将js替换为ts)
- 相对路径```require("./xxx")```
- 非相对模块```require("xxx")```
接口
接口:inteface
扩展类型:类型别名、枚举、接口、类
TypeScript的接口:用于约束类、对象、函数的契约(标准)
契约(标准)的形式:
- API文档,弱标准
- 代码约束,强标准
和类型别名一样,接口,不出现在编译结果中
-
接口约束对象
-
接口约束函数
类型兼容性
B->A,如果能完成赋值,则B和A类型兼容
鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可
-
基本类型:完全匹配
-
对象类型:鸭子辨型法
类型断言
当直接使用对象字面量赋值的时候,会进行更加严格的判断
- 函数类型
参数:传递给目标函数的参数可以少,但不可以多
返回值:要求返回必须返回;不要求返回,你随意;
TS中的类
面向对象思想
基础部分,学习类的时候,仅讨论新增的语法部分。
属性
使用属性列表来描述类中的属性
属性的初始化检查
strictPropertyInitialization:true
属性的初始化位置:
- 构造函数中
- 属性默认值
属性可以修饰为可选的
属性可以修饰为只读的
使用访问修饰符
访问修饰符可以控制类中的某个成员的访问权限
- public:默认的访问修饰符,公开的,所有的代码均可访问
- private:私有的,只有在类中可以访问
- protected:暂时不讲
Symble
属性简写
如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写
访问器
作用:用于控制属性的读取和赋值
泛型
有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)
泛型:是指附属于函数、类、接口、类型别名之上的类型
泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型
很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型
如果无法完成推导,并且又没有传递具体的类型,默认为空对象
泛型可以设置默认值
在函数中使用泛型
在函数名之后写上<泛型名称>
如何在类型别名、接口、类中使用泛型
直接在名称后写上<泛型名称>
泛型约束
泛型约束,用于现实泛型的取值
多泛型
深入理解类和接口
面向对象概述
为什么要讲面向对象
- TS为前端面向对象开发带来了契机
JS语言没有类型检查,如果使用面向对象的方式开发,会产生大量的接口,而大量的接口会导致调用复杂度剧增,这种复杂度必须通过严格的类型检查来避免错误,尽管可以使用注释或文档或记忆力,但是它们没有强约束力。
TS带来了完整的类型系统,因此开发复杂程序时,无论接口数量有多少,都可以获得完整的类型检查,并且这种检查是据有强约束力的。
- 面向对象中有许多非常成熟的模式,能处理复杂问题
在过去的很多年中,在大型应用或复杂领域,面向对象已经积累了非常多的经验。
什么是面向对象
面向对象:Oriented(基于) Object(事物),简称OO。
是一种编程思想,它提出一切以类对切入点思考问题。
其他编程思想:面向过程、函数式编程
学开发最重要最难的是什么?思维
面向过程:以功能流程为思考切入点,不太适合大型应用
函数式编程:以数学运算为思考切入点
面向对象:以划分类为思考切入点。类是最小的功能单元
类:可以产生对象的模板。
如何学习
- TS中的OOP (面向对象编程,Oriented Object Programing)
- 小游戏练习
理解 -> 想法 -> 实践 -> 理解 -> ....
类的继承
继承的作用
继承可以描述类与类之间的关系
坦克、玩家坦克、敌方坦克 玩家坦克是坦克,敌方坦克是坦克
如果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的父类