01 初识 TypeScript
1.1 TypeScript 的介绍
-
TypeScript 是一种由微软开发的开源、跨平台的编程语言。它是JavaScript的超集,最终会被编译为JavaScript代码
-
2012年10月,微软发布了首个公开版本的TypeScript,2013年6月19日,在经历了一个预览版之后微软正式发布了正式版TypeScript
-
TypeScript的作者是安德斯·海尔斯伯格,C#的首席架构师。它是开源和跨平台的编程语言
-
TypeScript扩展了JavaScript的语法,所以任何现有的JavaScript程序可以运行在TypeScript环境中
-
TypeScript是为大型应用的开发而设计,并且可以编译为JavaScript
-
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持,它由 Microsoft 开发,代码开源于 GitHub 上
-
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持,它由 Microsoft 开发,代码开源于 GitHub 上
1.2 TypeScript 的特点
TypeScript 主要有 3 大特点:
-
始于JavaScript,归于JavaScript
- TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的JavaScript 引擎中
-
强大的类型系统
- 类型系统允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构
-
先进的 JavaScript
- TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件
TypeScript 在社区的流行度越来越高,它非常适用于一些大型项目,也非常适用于一些基础库,极大地帮助我们提升了开发效率和体验
1.3 安装 TypeScript
- 命令行运行如下命令,全局安装 TypeScript
npm install -g typescript
- 安装完成后,在控制台运行如下命令,检查安装是否成功
tsc -V
1.4 第一个 TypeScript 程序
1.4.1 编写 TS 程序
src/helloworld.ts
function greeter (person) {
return 'Hello, ' + person
}
let user = 'Yee'
console.log(greeter(user))
1.4.2 手动编译代码
-
我们使用了
.ts
扩展名,但是这段代码仅仅是 JavaScript 而已 -
在命令行上,运行 TypeScript 编译器
tsc helloworld.ts
-
输出结果为一个
helloworld.js
文件,它包含了和输入文件中相同的 JavsScript 代码 -
在命令行上,通过 Node.js 运行这段代码
node helloworld.js
- 控制台输出
Hello, Yee
1.4.3 vscode 自动编译
- 生成配置文件 tsconfig.json
tsc --init
- 修改 tsconfig.json 配置
"outDir": "./js",
"strict": false,
- 启动监视任务
终端 ==> 运行任务 ==> 监视 tsconfig.json
1.4.4 类型注解
- 接下来让我们看看 TypeScript 工具带来的高级功能。给
person
函数的参数添加: string
类型注解,如下
function greeter (person: string) {
return 'Hello, ' + person
}
let user = 'Yee'
console.log(greeter(user))
- TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。 在这个例子里,我们希望
greeter
函数接收一个字符串参数。 然后尝试把greeter
的调用改成传入一个数组:
function greeter (person: string) {
return 'Hello, ' + person
}
let user = [0, 1, 2]
console.log(greeter(user))
- 重新编译,你会看到产生了一个错误:
error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.
-
类似地,尝试删除
greeter
调用的所有参数。 TypeScript 会告诉你使用了非期望个数的参数调用了这个函数。 在这两种情况中,TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。 -
要注意的是尽管有错误,
greeter.js
文件还是被创建了。 就算你的代码里有错误,你仍然可以使用 TypeScript。但在这种情况下,TypeScript 会警告你代码可能不会按预期执行。
1.4.5 接口
- 让我们继续扩展这个示例应用。这里我们使用接口来描述一个拥有
firstName
和lastName
字段的对象。 在TypeScript
里,只在两个类型内部的结构兼容,那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用implements
语句。
interface Person {
firstName: string
lastName: string
}
function greeter (person: Person) {
return 'Hello, ' + person.firstName + ' ' + person.lastName
}
let user = {
firstName: 'Yee',
lastName: 'Huang'
}
console.log(greeter(user))
1.4.6 类
-
最后,让我们使用类来改写这个例子。 TypeScript 支持 JavaScript 的新特性,比如支持基于类的面向对象编程
-
让我们创建一个
User
类,它带有一个构造函数和一些公共字段。因为类的字段包含了接口所需要的字段,所以他们能很好的兼容 -
还要注意的是,我在类的声明上会注明所有的成员变量,这样比较一目了然
class User {
fullName: string
firstName: string
lastName: string
constructor (firstName: string, lastName: string) {
this.firstName = firstName
this.lastName = lastName
this.fullName = firstName + ' ' + lastName
}
}
interface Person {
firstName: string
lastName: string
}
function greeter (person: Person) {
return 'Hello, ' + person.firstName + ' ' + person.lastName
}
let user = new User('Yee', 'Huang')
console.log(greeter(user))
- 重新运行
tsc greeter.ts
,你会看到 TypeScript 里的类只是一个语法糖,本质上还是JavaScript
函数的实现
1.5 使用webpack打包TS
1.5.1 下载依赖
yarn add -D typescript
yarn add -D webpack webpack-cli
yarn add -D webpack-dev-server
yarn add -D html-webpack-plugin clean-webpack-plugin
yarn add -D ts-loader
yarn add -D cross-env
1.5.2 入口 JS:src/main.ts
// import './01_helloworld'
document.write('Hello Webpack TS!')
1.5.3 index 页面:public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webpack & TS</title>
</head>
<body>
</body>
</html>
1.5.4 build/webpack.config.js
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const isProd = process.env.NODE_ENV === 'production' // 是否生产环境
function resolve (dir) {
return path.resolve(__dirname, '..', dir)
}
module.exports = {
mode: isProd ? 'production' : 'development',
entry: {
app: './src/main.ts'
},
output: {
path: resolve('dist'),
filename: '[name].[contenthash:8].js'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
include: [resolve('src')]
}
]
},
plugins: [
new CleanWebpackPlugin({
}),
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
devtool: isProd ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',
devServer: {
host: 'localhost', // 主机名
stats: 'errors-only', // 打包日志输出输出错误信息
port: 8081,
open: true
},
}
1.5.5 配置打包命令
"dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
1.5.6 运行与打包
yarn dev
yarn build
02 TypeScript 常用语法
2.1 基础类型
2.1.1 布尔值
- 最基本的数据类型就是简单的 true/false 值,在 JavaScript 和 TypeScript 里叫做
boolean
(其它语言中也一样)
// 基础类型
(() => {
// 布尔类型 boolean
// let 变量名:数据类型 = 值
let isDone: boolean = false;
console.log(isDone)
})()
2.1.2 数字类型
- 和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量
// 基础类型
(() => {
// 数字类型 number
// let 变量名:数据类型 = 值
let a1: number = 10 // 十进制
let a2: number = 0b1010 // 二进制
let a3: number = 0o12 // 八进制
let a4: number = 0xa // 十六进制
console.log(a1)
console.log(a2)
console.log(a3)
console.log(a4)
})()
2.1.3 字符串类型
- JavaScript 程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用
string
表示文本数据类型。 和 JavaScript 一样,可以使用双引号("
)或单引号('
)表示字符串
// 基础类型
(() => {
// 字符串类型 string
// let 变量名:数据类型 = 值
let name: string = 'tom'
name = 'jack'
// name = 12 // error
console.log(`My name is ${name}`)
// 字符串和数字之间能够拼接
let str: string = '我有这么多钱'
let num: number = 2361
console.log(str + num)
})()
2.1.4 undefined 和 null
- TypeScript 里,
undefined
和null
两者各自有自己的类型分别叫做undefined
和null
。 它们的本身的类型用处不是很大 - 默认情况下
null
和undefined
是所有类型的子类型。 就是说你可以把null
和undefined
赋值给number
类型的变量
// 基础类型
(() => {
// undefined和null
// let 变量名:数据类型 = 值
let u: undefined = undefined
let n: null = null
console.log(u)
console.log(n)
// undefined和null:都可以作为其他类型的子类型,可以赋值作为其他类型的变量,需要在tsconfig.json关闭严格模式
let un: number = undefined
let nn: number = null
console.log(un)
console.log(nn)
})()
2.1.5 数组
-
TypeScript 像 JavaScript 一样可以操作数组元素。 有两种方式可以定义数组
-
第一种,可以在元素类型后面接上
[]
,表示由此类型元素组成的一个数组 -
第二种,使用数组泛型,
Array<元素类型>
// 基础类型
(() => {
// 数组类型:只能定义统一的数据类型,个数无限制
// 方法一
// let 变量名: 数据类型[] = [值1,值2,值3,...]
let list1: number[] = [1, 2, 3]
console.log(list1)
// 方法二:泛型
// let 变量名<数据类型> = [值1,值2,值3,...]
let list2: Array<number> = [1, 2, 3]
console.log(list2)
})()
2.1.6 元组 Tuple
- 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
// 基础类型
(() => {
// 元祖类型:定义数组时,类型和数据的个数就已经被限定
// let 变量名: [数据类型1,数据类型2,数据类型3] = [值1,值2,值3]
let t1: [string, number, boolean]
t1 = ['hello', 10, false]
// t1 = [10, true, 'hello'] // error
console.log(t1)
})()
2.1.7 枚举
enum
类型是对 JavaScript 标准数据类型的一个补充。 使用枚举类型可以为一组数值赋予友好的名字- 默认情况下,从
0
开始为元素编号,可以手动的指定成员的数值,部分赋值和全部赋值都可以 - 枚举类型提供的一个便利是可以由枚举的值得到它的名字
// 基础类型
(() => {
// 枚举类型:每个元素有自己的编号,默认从0开始依次递增,可以指定成员的值
enum Color {
Red,
Green,
Blue
}
// 根据特定的名称得到对应的枚举数值
let myColor: Color = Color.Green // 0
console.log(myColor, Color.Red, Color.Blue)
})()
2.1.8 any
-
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用
any
类型来标记这些变量 -
在对现有代码进行改写的时候,
any
类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。并且当你只知道一部分数据的类型时,any
类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据
// 基础类型
(() => {
// any类型:任意类型,可以配合数组
let notSure: any = 4
console.log(notSure)
notSure = 'maybe a string'
console.log(notSure)
notSure = false
console.log(notSure)
})()
2.1.9 void
- 某种程度上来说,
void
类型像是与any
类型相反,它表示没有任何类型
。 当一个函数没有返回值时,通常会见到其返回值类型是void
- 声明一个
void
类型的变量没有什么大用,因为只能为它赋予undefined
和null
// 基础类型
(() => {
// void类型:表示没有任何类型,一般用来说明函数的返回值不能是undefined和null之外的值
function fn(): void {
console.log('fn()')
// return
// return undefined
// return null
// return 1 // error
}
console.log(fn())
})()
2.1.10 object
object
表示非原始类型,也就是除number
,string
,boolean
之外的类型- 使用
object
类型,就可以更好的表示像Object.create
这样的API
// 基础类型
(() => {
// object类型
function getObj(obj: object): object {
console.log('getObj()', obj)
return {
name: 'lemon',
age: 18
}
// return undefined
// return null
}
// console.log(getObj('abc')) // error
console.log(getObj({name: 'popcorn', age: 18}))
console.log(getObj(new String('abc')))
console.log(getObj(String))
})()
2.1.11 联合类型
- 联合类型(Union Types)表示取值可以为多种类型中的一种
// 基础类型
(() => {
// 联合类型:取值可以为多种类型中的一种
// 需求1:定义一个一个函数得到一个数字或字符串值的字符串形式值
function getString(x: number | string) : string {
return x.toString()
}
console.log(getString(123))
console.log(getString('123'))
// 需求2: 定义一个一个函数得到一个数字或字符串值的长度
function getLength(x: number | string) {
// 一个问题:如果x本身就是string类型,是没有必要调用toString()方法
return x.toString().length
// 下列代码会报错,使用类型断言解决
// return x.length // error
// if (x.length) { // error
// return x.length
// } else {
// return x.toString().length
// }
}
console.log(getLength(123))
console.log(getLength('123'))
})()
2.1.12 类型断言
- 通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查
- 类型断言有两种形式。 其一是“尖括号”语法,另一个为
as
语法
// 基础类型
(() => {
// 类型断言:可以用来手动指定一个值的类型
// 方式一: <类型>值
// 方式二: 值 as 类型,tsx中只能用这种方式
// 需求2: 定义一个函数得到一个字符串或者数值数据的长度
function getLengthSolve(x: number | string) {
if ((<string>x).length) {
return (x as string).length
} else {
return x.toString().length
}
}
console.log(getLengthSolve(123))
console.log(getLengthSolve('123'))
})()
2.1.13 类型推断
- 类型推断:TS 会在没有明确的指定类型的时候推测出一个类型
- 有下面 2 种情况
- 定义变量时赋值了,推断为对应的类型
- 定义变量时没有赋值,推断为 any 类型
// 基础类型
(() => {
// 类型推断
// 定义变量时赋值了,推断为对应的类型
let a = 123 // number
// a = 'abc' // error
console.log(a)
// 定义变量时没有赋值,推断为any类型
let b // any类型
b= 123
b= 'abc'
console.log(b)
})()
2.2 接口
TypeScript 的核心原则之一是对值所具有的结构进行类型检查。我们使用接口(Interfaces)来定义对象的类型,接口是对象的状态(属性)和行为(方法)的抽象(描述)
2.2.1 接口初探
- 需求:创建人的对象,需要对人的属性进行一定的约束
- 类型检查器会查看对象内部的属性是否与 IPerson 接口描述一致,如果不一致就会提示类型错误
// 接口:定义对象的类型,对值所具有的结构进行类型检查
(() => {
/*
* 在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型
* 接口:是对象的状态(属性)和行为(方法)的抽象(描述)
* 接口类型的对象
* 多了或者少了属性是不允许的
* 可选属性: ?
* 只读属性: readonly
*/
/*
* 需求: 创建人的对象, 需要对人的属性进行一定的约束
* id是number类型,必须有,只读的
* name是string类型,必须有
* age是number类型,必须有
* sex是string类型,可以没有
*/
// 定义接口,作为person对象的类型使用,限定或者是约束该对象中的属性数据
interface IPerson {
readonly id: number
name: string
age: number
sex?: string
}
// 定义对象,该对象的类型就是我们定义的接口
const person1: IPerson = {
id: 1,
name: 'tom',
age: 20,
sex: '男'
}
})()
2.2.2 可选属性
- 接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在
- 带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个
?
符号 - 可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误
interface IPerson {
id: number
name: string
age: number
sex?: string
}
const person2: IPerson = {
id: 1,
name: 'tom',
age: 20,
// sex: '男' // 可以没有
}
2.2.3 只读属性
- 一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用
readonly
来指定只读属性 - 一旦赋值后再也不能被改变了
interface IPerson {
readonly id: number
name: string
age: number
sex?: string
}
const person2: IPerson = {
id: 2,
name: 'tom',
age: 20,
// sex: '男' // 可以没有
// xxx: 12 // error 没有在接口中定义, 不能有
}
person2.id = 2 // error
readonly vs const
- 最简单判断该用
readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const
,若做为属性则使用readonly
2.2.4 函数类型
- 接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型
- 为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型
- 这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量
// 函数类型
// 为了使用接口表示函数类型,我们需要给接口定义一个调用签名
// 它就像是一个只有参数列表和返回值类型的函数定义,参数列表里的每个参数都需要名字和类型
(() => {
// 函数类型:通过接口的方式作为函数的类型来使用
interface SearchFunc {
// 定义一个调用签名
(source: string, subString: string): boolean
}
const mySearch: SearchFunc = function (source: string, sub: string): boolean {
return source.search(sub) > -1
}
console.log(mySearch('abcd', 'bc'))
})()
2.2.5 类类型
- 类实现接口
- 与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约
- 一个类可以实现多个接口
- 接口继承接口
- 和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里
// 类类型:类的类型可以通过使用接口实现
(() => {
// 一个类实现一个接口
interface IFly {
// 该方法没有任何实现
fly(): any
}
class Person1 implements IFly {
fly() {
console.log('I can fly')
}
}
const person1 = new Person1()
person1.fly()
// 一个类可以被多个接口进行约束,注意:接口中的内容都要真正实现
interface ISwim{
swim(): void
}
class Person2 implements IFly, ISwim {
fly() {
console.log('I can fly');
}
swim() {
console.log('I can swim')
}
}
const person2 = new Person2()
person2.fly()
person2.swim()
// 接口可以继承接口
interface ISkill extends IFly, ISwim {
}
class Person3 implements ISkill {
fly() {
console.log('I can fly');
}
swim() {
console.log('I can swim')
}
}
const person3 = new Person3()
person3.fly()
person3.swim()
})()
2.3 类
- 对于传统的 JavaScript 程序我们会使用
函数
和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员使用这些语法就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。从 ECMAScript 2015,也就是 ES6 开始, JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本
// 类:可以理解为模板,通过模板可以实例化对象
// 面向对象的编程思想
(() => {
// ts中的类
class Person {
// 声明属性
name: string
age: number
gender: string
// 构造方法
constructor (name: string = 'lemon', age: number = 18, gender: string = 'female') {
this.name = name
this.age = age
this.gender = gender
}
// 一般方法
sayHi (): string {
return `My name is ${this.name}, I am ${this.age} years old, I am ${this.gender}`
}
}
const person1 = new Person()
console.log(person1.sayHi())
const person2 = new Person('popcorn', 23, 'male')
console.log(person2.sayHi())
})()
- 如果你使用过 C# 或 Java,你会对这种语法非常熟悉
- 你会注意到,我们在引用任何一个类成员的时候都用了
this
。 它表示我们访问的是类的成员 - 后面我们使用
new
构造了Person
类的一个实例。它会调用之前定义的构造函数,创建一个Person
类型的新对象,并执行构造函数初始化它 - 最后通过 Person 对象调用其
sayHi
方法
2.3.1 继承
- 在 TypeScript 里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类
// 类的继承
// A类继承了B类
// A类 ==> 子类/派生类
// B类 ==> 基类/父类/超类
(() => {
// 定义一个基类/父类/超类
class Person {
// 声明属性
name: string
age: number
gender: string
// 构造方法
constructor (name: string = 'lemon', age: number = 18, gender: string = 'female') {
this.name = name
this.age = age
this.gender = gender
}
// 一般方法
sayHi (str: string) {
console.log(`My name is ${this.name}, I am ${this.age} years old, I am ${this.gender}, I like ${str}`)
}
}
// 定义一个子类/派生类
class Student extends Person {
constructor (name: string, age: number, gender: string) {
// 调用父类中的构造函数,使用surper()
super(name, age, gender)
}
// 调用父类中的方法,使用super.
sayHello() {
console.log('Sutdent---sayHi')
super.sayHi('swim')
}
}
const person = new Person()
person.sayHi('fly')
const student = new Student('popcorn', 23, 'male')
student.sayHello()
})()
- 这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,
Student
是一个 派生类,它派生自Person
基类,通过extends
关键字。 派生类通常被称作子类,基类通常被称作超类 - 因为
Student
继承了Person
的功能,因此我们可以创建一个Student
的实例,它能够sayHi()
2.3.2 多态
- 父类型的引用指向子类型的对象,不同类型对象针对相同的方法产生了不同行为
// 多态:父类型的引用指向子类型的对象,不同类型对象针对相同的方法产生了不同行为
(() => {
// 定义一个父类
class Animal {
name: String
constructor(name: string) {
this.name = name
}
run(distance: number = 5) {
console.log(`${this.name},run ${distance}`)
}
}
// 定义一个子类
class Dog extends Animal {
constructor(name: string){
// 调用父类构造函数实现子类中属性的初始化操作
super(name)
}
// 重写父类的方法
run(distance: number = 100) {
console.log(`${this.name},run ${distance}`)
}
}
// 定义一个子类
class Pig extends Animal {
constructor(name: string) {
// 调用父类构造函数实现子类中属性的初始化操作
super(name)
}
// 重写父类的方法
run(distance: number = 200) {
console.log(`${this.name},run ${distance}`)
}
}
const animal = new Animal('animal')
animal.run()
const dog = new Dog('dog')
dog.run()
const pig = new Pig('pig')
pig.run()
// 多态:父类型的引用指向子类型的对象,不同类型对象针对相同的方法产生了不同行为
// 父子关系:使用父类的类型创建子类的对象
const dog1:Animal = new Dog('小狗')
dog1.run()
const pig1:Animal = new Pig('小猪')
dog1.run()
// 该函数需要的参数是Animal类型的
function showRun(ani: Animal) {
ani.run()
}
showRun(dog1)
showRun(pig1)
})()
- 这个例子展示了一些上面没有提到的特性。 这一次,我们使用
extends
关键字创建了 Animal 的两个子类:Dog
和Pig
- 与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用
super()
,它会执行基类的构造函数。 而且,在构造函数里访问this
的属性之前,我们 一定要调用super()
。 这个是 TypeScript 强制执行的一条重要规则 - 这个例子演示了如何在子类里可以重写父类的方法。
Dog
和Pig
类都创建了run
方法,它们重写了从Animal
继承来的run
方法,使得run
方法根据不同的类而具有不同的功能。注意,即使Dog1
和Pig1
被声明为Animal
类型,但因为它的值是Dog
和Pig
,调用run()
时,它会调用Dog
和Pig
里重写的方法
2.3.3 公共,私有与受保护的修饰符
- 默认为 public
- 在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用
public
来做修饰;例如,C# 要求必须明确地使用public
指定成员是可见的。 在 TypeScript 里,成员都默认为public
- 在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用
- private
- 当成员被标记成
private
时,它就不能在声明它的类的外部访问
- 当成员被标记成
- protected
protected
修饰符与private
修饰符的行为很相似,但有一点不同,protected
成员在派生类中仍然可以访问
// 类中成员的修饰符:描述类中成员的可访问性
// public ==> 类成员默认的修饰符,代表是公共的,任何位置都可以访问类中的成员
// protected ==> 外部无法访问类中的属性,子类可以该成员数据
// private ==> 外部无法访问类中的属性,子类也无法访问该成员数据
(() => {
class Person {
public name: string
protected gender: string
private age: number
public constructor(name: string, gender: string, age: number) {
this.name = name
this.gender = gender
this.age = age
}
public eat() {
console.log('Person---eat', this.name, this.gender, this.age)
}
}
class Sutudent extends Person{
constructor(name: string, gender: string, age: number) {
super(name, gender, age)
}
play() {
// protected 子类可以该成员数据
console.log('Studnet---play', this.gender)
}
sing() {
// private 子类也无法访问该成员数据
// console.log('Studnet---sing',this.age) //error
}
}
const person = new Person('lemon', 'female', 18)
const student = new Sutudent('popcorn', 'male' , 22)
// public 类外部可以访问类中的属性成员
console.log(person.name)
person.eat()
// protected 外部无法访问类中的属性
// console.log(person.age) // error
// console.log(student.age) // error
// protected 子类可以该成员数据
student.play()
// private 外部无法访问类中的属性
// console.log(person.age) // error
// console.log(student.age) // error
// student.sing() // error
})()
2.3.4 readonly 修饰符
- 可以使用
readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化
// readonly ==> 只读修饰符,该属性成员不能在外部修改
// 只有在构造函数里可以对传入的只读的属性成员数据进行修改,不管是否传入
// 构造函数参数一旦使用readonly/public/protected/private,那么Person中就有了所修饰的属性成员
(() => {
class Person {
// readonly name: string
// constructor(name: string){
// this.name = name
// this.name = 'popcorn'
// }
// 构造函数参数一旦设置了readonly
// 那么Person中就有了一个name的属性成员
// name参数可以叫参数属性
// 外部依然无法修改该属性
// constructor(name: string){ // error
constructor(readonly name: string) {
this.name = name
}
sayHi() {
// 类中的普通方法不能修改readonly修饰的成员属性
// this.name = 'popcorn' // error
console.log('Hi',this.name)
}
}
const person: Person = new Person('lemon')
// person.name = 'popcorn' // error
console.log(person)
console.log(person.name)
})()
参数属性
- 一般我们必须在
Person
类里定义一个只读成员name
和一个参数为name
的构造函数,并且立刻将name
的值赋给this.name
,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员
class Person2 {
constructor(readonly name: string) {
}
}
const p = new Person2('jack')
console.log(p.name)
- 注意看我们是如何舍弃参数
name
,仅在构造函数里使用readonly name: string
参数来创建和初始化name
成员。 我们把声明和赋值合并至一处 - 参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用
private
限定一个参数属性会声明并初始化一个私有成员;对于public
和protected
来说也是一样
2.3.5 存取器
- TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问
// 存取器:有效控制对对象中成员的访问,通过getters和setters进行操作
(() => {
// 外部传入姓名数据,使用set和get控制姓名数据,外部也可以进行修改操作
class Person {
firstName: string
lastName: string
constructor(firstName: string, lastName: string) {
this.firstName = firstName
this.lastName = lastName
}
// 姓名成员属性(外部可以访问和修改)
// 读取器
get fullName() {
console.log('get')
return this.firstName + '---' + this.lastName
}
// 设置器
set fullName(val) {
console.log('set')
const name = val.split('---')
this.firstName = name[0]
this.lastName = name[1]
}
}
const person: Person = new Person('东方', '不败')
console.log(person)
console.log(person.fullName)
person.fullName = '诸葛---亮'
console.log(person.fullName)
})()
2.3.5 静态成员
- 到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上
// 静态成员:在类中通过static修饰的属性及方法 ==> 静态属性/静态方法 ==> 静态成员
// 通过 类名.静态属性/静态方法 操作
(() => {
class Person{
name: string
// 静态属性
static gender: string = 'female'
// 构造函数不能通过static来修饰
constructor(name: string) {
this.name = name
// 此时this是实例对象,gender是静态属性,不能通过实例对象直接调用
// this.gender = gender // error
}
sayHi(){
console.log('Hi')
}
// 静态方法
static sayHello() {
console.log('Hello')
}
}
const person: Person = new Person('lemon')
// 通过实例对象调用的属性(实例属性)
console.log(person.name)
// 通过实例对象调用的方法(实例方法)
person.sayHi()
// 通过 类名.静态属性 的方式访问/修改该成员数据
console.log(Person.gender)
Person.gender = 'male'
console.log(Person.gender)
// 通过 类名.静态方法 的方式调用/修改该成员方法
Person.sayHello()
Person.sayHello = function () {
console.log('sayHello')
}
Person.sayHello()
})()
2.3.6 抽象类
- 抽象类做为其它派生类的基类使用。 它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节
abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法
// 抽象类:包含抽象方法(抽象方法一般没有任何的具体内容的实现)
// 也可以包含实例方法,抽象类不能被实例化,为了让子类进行实例化即实现内部的抽象方法
// 抽象类的目的是为子类服务的
(() => {
abstract class Animal {
// 抽象属性
abstract name: string
// 抽象方法
// 报错 ==> eat不能有具体的实现
// abstract eat(){
// console.log('eat') // error
// }
abstract eat()
// 实例方法
sayHi() {
console.log('Hi')
}
}
class Dog extends Animal {
name: string = 'dog'
// 重新实现抽象类中的方法
eat() {
console.log('dog---eat')
}
}
// 不能实例化抽象类的对象
// const animal: Animal = new Animal() // error
const dog: Dog = new Dog()
console.log(dog.name)
dog.eat()
// 调用抽象类中的实例方法
dog.sayHi()
})()
2.4 函数
-
函数是 JavaScript 应用程序的基础,它帮助你实现抽象层,模拟类,信息隐藏和模块。在 TypeScript
里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。TypeScript 为 JavaScript
函数添加了额外的功能,让我们可以更容易地使用 -
和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数
// 函数:封装重复代码
(() => {
// js 中的书写方式在ts中同样适用
// 函数声明,命名函数
function add1(x, y) {
return x + y
}
// 函数表达式,匿名函数
const add2 = function(x, y) {
return x + y
}
console.log(add1(1, 1))
console.log(add2(2, 2))
// ts中的书写方式
// 函数声明,命名函数
// function 函数名(参数1: 数据类型, 参数2: 数据类型,...): 返回值数据类型{ 函数体 }
function add3(x: string, y: string): string {
return x + y
}
// 函数表达式,匿名函数
// 函数名 = function(参数1: 数据类型, 参数2: 数据类型,...): 返回值数据类型{ 函数体 }
const add4 = function(x: number, y: number): number {
return x + y
}
// 函数完整写法
// 函数类型:(参数1: 数据类型, 参数2: 数据类型,...) => 返回值数据类型
// 符合设定的函数类型的值:function(参数1: 数据类型, 参数2: 数据类型,...): 返回值数据类型{ 函数体 }
const add5:(x: number, y: number) => number = function name(x: number, y: number): number {
return x + y
}
console.log(add3('3', '3'))
console.log(add4(4, 4))
console.log(add5(5, 5))
})()
2.4.1 可选参数和默认参数
- TypeScript 里的每个函数参数都是必须的。 这不是指不能传递
null
或undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致 - JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是
undefined
。 在TypeScript 里我们可以在参数名旁使用?
实现可选参数的功能 - 在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是
undefined
时。 它们叫做有默认初始化值的参数
// 可选参数:函数声明时参数名后面使用?修饰
// 默认参数:函数声明时给参数赋值
(() => {
// 定义函数:传入姓氏和名字,可以得到姓名
// 需求:传入姓氏和名字返回姓名;传入姓氏返回姓氏;不传入任何内容返回默认姓氏
const getFullName = function(firstName: string = 'lemon', lastName?: string): string {
if (lastName) {
return firstName + '-' + lastName
}
else {
return firstName
}
}
console.log(getFullName())
console.log(getFullName('pop'))
console.log(getFullName('pop', 'corn'))
})()
2.4.2 剩余参数
- 必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用
arguments
来访问所有传入的参数 - 在 TypeScript 里,你可以把所有参数收集到一个变量里,剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号(
...
)后面给定的名字,你可以在函数体内使用这个数组
// 剩余参数(rest参数):放在参数的最后
(() => {
function showMsg(str: string, ...args: string[]) {
console.log(str)
console.log(args)
}
showMsg('a', 'b', 'c', 'd', 'e')
})()
2.4.3 函数重载
- 函数重载:函数名相同,而形参不同的多个函数
- 在JS中,由于弱类型的特点和形参与实参可以不匹配,是没有函数重载这一说的,但在TS中,与其它面向对象的语言(如 Java)就存在此语法
// 函数重载:函数名相同,函数参数及个数不同
(() => {
// 函数重载声明解决
function add(x: string, y: string): string
function add(x: number, y: number): number
// 需求:add函数接受2个string类型参数拼接或者2个number类型参数相加
// 函数声明
function add (x: string | number, y: string | number): string | number {
if (typeof x === 'string' && typeof y === 'string') {
return x + y // 字符串拼接
} else if (typeof x === 'number' && typeof y === 'number') {
return x + y // 数字相加
}
}
// 2个参数都是string,合法
console.log(add('诸葛', '亮'))
// 2个参数都是number,合法
console.log(add(10, 20))
// 传入的是非法数据,ts应该提示出错误信息内容(报红),添加函数重载声明解决
// console.log(add('哈哈哈', 384)) // error
// console.log(add(283, '哼哼哼')) // error
})()
2.5 泛型
指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定具体类型的一种特性
// 定义函数/接口/类时不能预先确定数据类型,而是在使用函数/接口/类时才能确定数据类型
(() => {
// 需求:定义函数,传入两个参数,第一个是任意类型数据,第二个是数量
// 函数作用:根据数量产生对应个数的数据,存放在一个数组中
function getArr1(value: any, count: number): any[] {
const arr: any[] = []
for(let i = 0; i < count; i++) {
arr.push(value)
}
return arr
}
const arr1 = getArr1(2363.2736, 4)
const arr2 = getArr1('vsdv', 10)
console.log(arr1)
console.log(arr2)
console.log(arr1[0].toFixed(2)) // 没有任何智能的提示信息(方法名字的提示/错误信息)
console.log(arr2[0].split('')) // 没有任何智能的提示信息(方法名字的提示/错误信息)
// 泛型
function getArr2 <T> (value: T, count: number): T[] {
// const arr: T[] = []
const arr: Array<T> = []
for(let i = 0; i < count; i++) {
arr.push(value)
}
return arr
}
const arr3 = getArr2<number>(35416.3738, 6)
const arr4 = getArr2<string>('dfsdfv', 7)
console.log(arr3)
console.log(arr4)
console.log(arr3[0].toFixed(2))
console.log(arr4[0].split(''))
})()
2.5.1 多个泛型参数的函数
- 一个函数可以定义多个泛型参数
// 多个泛型参数的函数
(() => {
function getMsg <K, V> (value1: K, value2: V): [K, V] {
return [value1, value2]
}
const arr1 = getMsg <string, number> ('jack', 132.3827)
console.log(arr1[0].split(''), arr1[1].toFixed(2))
})()
2.5.2 泛型接口
- 在定义接口时,为接口中的属性或方法定义泛型类型
- 在使用接口时,再指定具体的泛型类型
// 在定义接口时,为接口中的属性或方法定义泛型类型
// 在使用接口时,再指定具体的泛型类型
(() => {
// 定义一个泛型接口
interface IBaseCURD<T> {
data: Array<T>
add: (user: T) => T
getUserId: (id: number) => T
}
// 需求:定义一个类,用来存储用户的相关信息(id,名字,年龄)
// 定义一个用户信息类
class User {
id?: number
name: string
age: number
constructor(name: string, age: number){
this.name = name
this.age = age
}
}
// 定义一个类,可以针对用户信息对象进行增加及查询操作
// 通过一个类的实例对象,调用add方法可以添加多个用户信息对象,调用getUserId方法可以根据id获取指定用户信息对象
class UserCRUD implements IBaseCURD<User> {
data: Array<User> = []
add(user: User): User {
user.id = Date.now() + Math.random()
this.data.push(user)
return user
}
getUserId(id: number): User {
return this.data.find(user => user.id === id)
}
}
const userCRUD: UserCRUD = new UserCRUD()
userCRUD.add(new User('lemon', 22))
userCRUD.add(new User('popcorn', 21))
const { id } = userCRUD.add(new User('rose', 34))
console.log(userCRUD)
const user = userCRUD.getUserId( id )
console.log(user)
})()
2.5.3 泛型类
- 在定义类时,为类中的属性或方法定义泛型类型,在创建类的实例时,再指定特定的泛型类型
// 泛型类
// 定义一个类,类中的属性值的类型是不确定的,方法中的参数及返回值的类型也是不确定的
(() => {
// 定义一个泛型类
class Genderic<T> {
// 默认属性的值的类型是泛型类型
defaultValue: T
add: (x: T, y: T) => T
}
const g1: Genderic<number> = new Genderic<number>()
// 设置属性值
g1.defaultValue = 100
// 相加的方法
g1.add = function (x, y) {
return x + y
}
console.log(g1.add(10, 20))
const g2: Genderic<string> = new Genderic<string>()
// 设置属性值
g2.defaultValue = 'hhh'
// 相加的方法
g2.add = function (x, y) {
return x + y
}
console.log(g2.add('ddd', 'sss'))
})()
2.5.4 泛型约束
- 如果我们直接对一个泛型参数取
length
属性, 会报错,因为这个泛型根本就不知道它有这个属性
// 没有泛型约束
function fn <T>(x: T): void {
// console.log(x.length) // error
}
- 我们可以使用泛型约束来实现
- 我们需要传入符合约束类型的值,必须包含
length
属性
// 如果我们直接对一个泛型参数取 length 属性, 会报错, 因为这个泛型根本就不知道它有这个属性
// 我们可以使用泛型约束来实现
(() => {
// 定义一个接口,用来约束将来某个类型中必须要有length这个属性
interface ILengrh {
length: number
}
function getLength<T extends ILengrh> (x: T): number {
return x.length // T 不使用ILength约束时会报错
}
console.log(getLength<string>('wgsiadnhqdbhjq'))
// console.log(getLength<number>(122)) //error
})()
2.6 其他
2.6.1 声明文件
- 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能
- 什么是声明语句
- 假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过
<script>
标签引入jQuery
,然后就可以使用全局变量$
或jQuery
了 - 但是在 ts 中,编译器并不知道
$
或jQuery
是什么东西
- 假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过
/*
* 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
* 声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
* declare var jQuery: (selector: string) => any
* 声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
* 下载声明文件: npm install @types/jquery --save-dev
*/
jQuery('#foo')
// ERROR: Cannot find name 'jQuery'
- 这时,我们需要使用
declare var
来定义它的类型
declare var jQuery: (selector: string) => any
jQuery('#foo')
declare var
并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是
jQuery('#foo');
-
一般声明文件都会单独写成一个
xxx.d.ts
文件 -
创建
01_jQuery.d.ts
,将声明语句定义其中,TS编译器会扫描并加载项目中所有的 TS 声明文件
declare var jQuery: (selector: string) => any
-
很多的第三方库都定义了对应的声明文件库,库文件名一般为
@types/xxx
,可以在 https://www.npmjs.com/package/package 进行搜索 -
有的第三库在下载时就会自动下载对应的声明文件库(比如:webpack),有的可能需要单独下载(比如jQuery / react)
// 引入第三方库 ==> jQuery
// import jQuery from 'jquery'
// 使用jQuery
// jQuery('选择器')
/*
* 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
* 声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
* declare var jQuery: (selector: string) => any;
* 声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
* 下载声明文件: npm install @types/jquery --save-dev
*/
2.6.2 内置对象
-
JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型
-
内置对象是指根据标准在全局作用域(Global)上存在的对象
-
这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准
-
ECMAScript 的内置对象:Boolean、Number、String、Date、RegExp、Error
-
BOM 和 DOM 的内置对象:Window、Document、HTMLElement、DocumentFragment、Event、NodeList
// 内置对象
(() => {
/* 1. ECMAScript 的内置对象 */
let b: Boolean = new Boolean(1)
let n: Number = new Number(true)
let s: String = new String('abc')
let d: Date = new Date()
let r: RegExp = /^1/
let e: Error = new Error('error message')
b = true
// let bb: boolean = new Boolean(2) // error
/* 2. BOM 和 DOM 的内置对象 */
const div: HTMLElement = document.getElementById('test')
const divs: NodeList = document.querySelectorAll('div')
document.addEventListener('click', (event: MouseEvent) => {
console.dir(event.target)
})
const fragment: DocumentFragment = document.createDocumentFragment()
})()
```typescript
declare var jQuery: (selector: string) => any
-
很多的第三方库都定义了对应的声明文件库,库文件名一般为
@types/xxx
,可以在 https://www.npmjs.com/package/package 进行搜索 -
有的第三库在下载时就会自动下载对应的声明文件库(比如:webpack),有的可能需要单独下载(比如jQuery / react)
// 引入第三方库 ==> jQuery
// import jQuery from 'jquery'
// 使用jQuery
// jQuery('选择器')
/*
* 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
* 声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
* declare var jQuery: (selector: string) => any;
* 声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
* 下载声明文件: npm install @types/jquery --save-dev
*/
2.6.2 内置对象
-
JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型
-
内置对象是指根据标准在全局作用域(Global)上存在的对象
-
这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准
-
ECMAScript 的内置对象:Boolean、Number、String、Date、RegExp、Error
-
BOM 和 DOM 的内置对象:Window、Document、HTMLElement、DocumentFragment、Event、NodeList
// 内置对象
(() => {
/* 1. ECMAScript 的内置对象 */
let b: Boolean = new Boolean(1)
let n: Number = new Number(true)
let s: String = new String('abc')
let d: Date = new Date()
let r: RegExp = /^1/
let e: Error = new Error('error message')
b = true
// let bb: boolean = new Boolean(2) // error
/* 2. BOM 和 DOM 的内置对象 */
const div: HTMLElement = document.getElementById('test')
const divs: NodeList = document.querySelectorAll('div')
document.addEventListener('click', (event: MouseEvent) => {
console.dir(event.target)
})
const fragment: DocumentFragment = document.createDocumentFragment()
})()