0 TypeScript 介绍
TypeScript 是微软开发的开源编程语言,在 JavaScript 的基础上添加了静态类型和基于类的面向对象编程,是 JavaScript 的超集。
TypeScript 文件通过其编译器转译为 JavaScript 代码运行。
该语言作者是 Anders Hejlsberg,他也是 C# 的首席架构师。
TypeScript 和 JavaScript 对比
TypeScript | JavaScript |
---|---|
JS 超集,解决大型项目的代码复杂性 | 脚本语言,创建动态网页 |
需要编译成 JS 再运行 | 解释型语言,直接运行 |
强类型、有静态/动态类型 | 弱类型,无静态类型 |
支持模块、泛型和接口 | 不支持 |
1 基础类型
TypeScript 声明变量有 3 种形式,分别是 let
var
const
。
var
关键字声明,作用于函数内,存在变量提升(即声明行为发生在作用域最开始)。let
关键字声明,作用于代码块{}
内,不存在变量提升。const
关键字声明同let
,但不允许重新分配变量,用来声明常量。
let 变量名: 类型 = 值; // 赋值并定义类型
let a: number = 7;
let 变量名: 类型; // 只定义类型
let a: boolean;
let 变量名 = 值; // 只赋值,任意类型
let a = 7;
let 变量名; // 不赋值不定义
let a;
因为 var
声明会出现变量提升的作用,其变量对整个函数,如果在 for
循环等地方涉及输出或者计数时会出现原有的赋值被覆盖的问题,而且它可以重复定义同一个变量。这两种情况都不会让编译器检查出来,但是并不符合开发者原本的意图。
更提倡使用 let
声明,符合一般逻辑。
如果一个变量不需要对它将进行写入,那么采用 const
声明会符合最小特权原则,也更容易推测数据的流动。
类型断言(Type Assertion)——更改类型
更改已有的变量类型在 TypeScript 中称为类型断言。更改方式为 <更改的类型> 变量名
或者 变量名 as 类型
首先定义一个任意类型的变量 a,它并非字符串,不能被字符串赋值,也不能应用字符串相关的函数进行处理。
let a: any = 'this is a string' // 此时 a 为任意类型而非字符串,无法被字符串赋值
let strLength: number = (<string>a).length; // 类型断言改变类型,以应用字符串的函数
let strLength: number = (a as string).length // as 的断言方法
这个过程编译成 JavaScript 会重新定义一遍改变量。
TypeScript 类型表
类型 | 示例 |
---|---|
基本类型 | |
— | — |
boolean | x: boolean = true |
number | x: number = 7 |
string | x:string = '10' |
undefined | x: undefined = undefined |
null | x: null = null |
引用类型&其他类型 | |
— | — |
object | x: object = {age: '14', color: 'white'} |
array | x: array = [1, '2', 3.0] |
function | x: function = (args) => {console.log(args)} |
symbol | x: symbol = Symbol('id') |
TS 补充类型 | |
— | — |
any | x: any = null |
never | function error(msg): never => {throw new Error(msg)} |
enum | enum Color {red = 1, green, blue} |
tuple | x: [string, number] = ['name', 12] |
类型注解
TS 的声明变量类型的方式是注解,在变量后面加上 : 类型
,使用小写。
//字符串注解
const xxx: string = 'string type';
// 函数参数类型
function params(value: string){ // 参数为 string 类型
console.log(value)
}
// 函数返回值类型注解
function returnValue(): string{ // 函数的返回值为 string 类型
return 'value is string'
}
boolean 布尔
基本布尔 true/false
类型,在 JavaScript 和 TypeScript 中都是 boolean
。
let isDone: boolean = false;
number 数字
JS 和 TS 中的所有数字都是浮点数,类型为 number
。支持二/八/十/十六进制。
let decLiteral: number = 7;
let hexLiteral: number = 0xf00d; // 十六进制
let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744; // 八进制
string 字符串
JavaScript 同 TypeScript,字符串用单双引号都可以。
let name1: string = "miao"
var name1 = "miao"
模板字符串用来定义多行文本和内嵌表达式,这种字符串用反引号包围,以 ${expr}
形式嵌入表达式。
let name1: string = `miao`;
let age: number = 18;
let sentence: string = `Hello, my name is ${name1}.
I'll be ${age + 1} years old next month.`;
等同于
let sentence: string = "Hello, my name is " + name1 + "\n" + "I'll be " + (age + 1) + " years old next month.";
Array 数组
TypeScript 数组同 JS,有两种方式定义。
- 在元素类型后面加上
变量名: 类型[]
- 泛型数组
变量名: Array<元素类型>
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3]; // 泛型
Tuple 元组
TypeScript 的元组可以表示已知元素数量和类型的数组,各元素的类型可不相同。
let x: [string, number];
x = ['hello', 10]; // 必须和声明时的类型相同
当访问元素越界的时候,会使用联合类型替代,但必须是已有的类型。
x[3] = 'world'; //OK 赋值给(string | number)类型
x[6] = true; // Error 布尔不在已有的类型中,不是这个联合类型 string | number 范围
enum 枚举
enum
类型是对 JavaScript 标准数据类型的补充,其特性同 C#,可以用枚举类型为一组数值赋予名字。
enum Color {Red = 1, Green, Blue} // 没有分号
let c:Color = Color.Red;
let colorName: string = Color[2];
console.log(colorName); // 'Green'
默认情况下,枚举的数值从 0 开始增长,即未指定值时,Red = 0, Green = 1, Blue = 2 依次增长。
any 动态类型
在不清楚变量类型的时候可以使用 any
来声明类型,此时类型检查器不会对这些值进行检查,它们直接通过编译阶段。
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;//
本质上,所有的类型都是 any
的子类型,any
是 TypeScript 的一个顶级类型,是一个类型系统逃逸舱。
需要注意的是,在使用该类型时因其过高的级别,无法使用 TypeScript 的很多保护机制,会产生一定的风险。
unknown 动态类型
unknown
类型是有保护的 any
类型,二者特性基本相同。所有类型都可以赋值给 unknown
,其是 TypeScript 的另一种顶级类型。
let value: unknown;
虽然 unknown
类型的变量可以被赋予任何值,但其只能赋值给 any
或者 unknown
类型本身。
void 空
void
类型表示没有任何类型,只能被赋值为 undefined
和 null
。
let unusable: void = undefined;
其在没有返回值的时候定义函数很有用。
另外,undefined
和 null
也是各自的类型,其既可以当值赋给其他类型,也可以作为类型声明。可以认为其是所有类型的子类型。
never 不存在
never
类型表示不存在的值的类型,可以认为错误的返回值类型为 never
。其是任何类型的子类型,可以给任何类型赋值。但没有类型是它的子类型,any
也不可以给 never
赋值。
使用 never
可以避免出现新增联合类型没有对应的实现,是用来保证类型安全的重要方式。
object 其他
object
类型是除了 number
, string
, boolean
, symbol
, null
, undefined
以外的类型。
2 对象属性/方法
JavaScript 的各种衍生类型是基于 Object 构造出来的,所以对象的操作方式也适用于数组、元组等数据类型。
点表示
对象的名字是一个命名空间(namespace),访问命名空间内部采用 命名空间.项目
的方式。
person.age; // 访问属性
person.interests[1]; // 访问数组子元素
person.bio() // 方法调用
使用创建对象的方式可以方便地访问对象的属性,类似于结构体。
const message = {
first: 'Hello',
second: 'World'
} // 创建对象
console.log(message.first);
console.log(message.second);
// HelloWorld
中括号
中括号 []
是另一种访问对象属性的方式,对象.项目
等价于 对象['项目']
。
person['age']; // person.age
person['name']['first']; // person.name.first
在构造数组或者类数组格式的数据时,只能用中括号的形式访问元素,不能用点方式。
const message = ['Hello', 'world'];
console.log(message[0]); // Hello
对象成员
对象成员即对象中的各个值。用以上两种方法既可以更新已存在的成员属性,也可以创建新的成员。
person.age = 45;
person['message']['last'] = 'Hello';
person.farewell = function() {alert("Farewell!")}
console.log(person.message.last); // Hello
person.function(); // Farewell!
3 解构与展开
解构数组
解构数组是最简单的结构。表现出来就是可以对应位置进行定义和赋值。
let input = [1, 2];
let [a, b] = input;
console.log(a); // 1
console.log(b); // 2
也可以直接进行交换
// swap variables
[a, b] = [b, a];
函数参数也可以进行解构
function f([a, b]: [number, number]){} // 函数解构数组
数组中使用 ...
语法可以创建剩余变量,即剩下的数组被赋给该变量。如果被赋值的变量数少于数组元素,后面的剩余变量会被忽略。数组赋值的时候会跳过留空的变量。
let [a, ...rest] = [1, 2, 3, 4];
// a = 1
//rest = [2, 3, 4]
let [a] = [1, 2, 3, 4];
// a = 1 剩下的变量被忽略
let [, b, ,d] = [1, 2, 3, 4];
// b = 2
// d = 4
对象解构
对象也可以进行解构。
let o = {
a: "foo",
b: 12,
c: "bar"
};
let {ans1, ans2} = o;
// ans1 = "foo"
// ans2 = 12
... 记录剩余变量
使用 ...
语法同样也可以记录剩余变量,剩余变量是对象。
let {x, ...rest} = o; // o 同上
该赋值得到两个变量:x = "foo"
rest = {12, "bar"}
。
类注解赋值
另一种对象里的属性给变量赋值类似于类型注释,在这里要注意区分:
let {a:new1, b:new2} = o; // o 同上
这个这个注释会得到新的变量 new1 = o.a
new2 = 0.b
默认值
在设置属性的时候可以设置默认值,在输入 undefined 的时候使用。
function keepWholeObject(wholeObject: {a:string, b?:number}){
let {a, b = 1001} = wholeObject;
}
此时,b
为 undefined
时,keepWholeObject
函数的变量 wholeObject
的属性 a
b
都会有值 1001。
函数声明
解构用于函数声明。
type C = {a: string, b?:number}
// 普通写法
function f(C){}
// 解构函数
function f({a, b}: C):void
{}
// 函数返回值为void,是类型注释
// 函数参数为解构声明
变量展开
展开的操作和解构正好相反。可以将一个数组展开成另一个数组,或者将一个对象展开成另一个对象。
以下是展开数组示例:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
// bothPlus = [0, 1, 2, 3, 4, 5]
这种展开操作创建了被展开对象的浅拷贝,被展开的对象不会发生改变。
展开对象要略微复杂一些,展开对象后面的属性会覆盖前面的属性。展开是从左向右进行处理的,排在后面的属性会覆盖掉前面出现过的属性。
let defaults = {food:"spicy", price = 10, ambiance: "noisy"};
let search = {...defaults, food:"rich"};
// search = {food:"rich", price = 10, ambiance: "noisy"}
上面的例子中,因为 food
属性发生了重复,排在后面的值 “rich” 就覆盖掉了前面的。如果将展开放在后面的话,”rich” 就会被 “spicy” 覆盖。
let search = {food:"rich", ...defaults};
// search = {food:"spicy", price = 10, ambiance: "noisy"}
4 类
TypeScript 在 JavaScript 中使用函数和基于原型的继承的基础上添加了语法糖——类,用来创建可重复使用的组件。
类声明
使用 class 父类{}
的方式来定义类。父类的名字一般首字母大写。
class Greeter{
greeting: string; // 属性
constructor(message: string){ // 构造函数
this.greeting = message;
}
greet(){ // 方法
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world"); // greeter 实例
该示例声明了一个 Greeter
类。类中有 3 个成员:属性 greeting
,构造函数 constructor()
,方法 greet()
。
- 属性:定义在类中的变量,在类的域中的所有方法都可以使用,在访问该变量的时候要用
this.变量名
的形式。 - 构造函数:类初始化时自动执行的方法。采用
constructor(){}
的形式来构造。 - 方法:方法是一个函数,基于该类构造的实例可以用点表示法
实例名.方法()
使用类中的所有方法。
最后一行中使用 new
构造了 Greeter
类的一个实例。实例创建一个 Greeter
类型的新对象,并执行构造函数来初始化。
继承
TypeScript 支持面向对象。基于类的程序设计最基本的模式就是使用继承来扩展现有的类。
用 class 子类 extends 父类{}
的方法来继承。
// 父类
class Animal{
// 构造函数 用于记录 name
name: string;
constructor(theName: string){
this.name = theName;
}
// 方法 move,默认参数 0
move(distance: number = 0){
console.log(`${this.name} moved ${distance}m.`); // 模板字符串反引号嵌入输出
}
}
// 子类
class Snake extends Animals{
// 构造函数 初始化的时候会自动声明一个 name 变量
constructor(name: string){
super(name);
}
// 方法 move,和父类方法区分
move(distance: number = 5){ // 默认值为5
console.log('Slithering...');
super.move(distance); // 使用父类的 move() 方法
}
}
let a = new Snake("The Python snake");
a.move();
该示例展示了类从基类继承了属性和方法。Animal
是父类(超类),Dog
是子类(派生类)。子类有父类的所有功能,因此 Dog
也有 Animal
中的方法 move()
,同时多了自己的新方法 bark()
。
当父类有构造函数时,子类就必须有构造函数,且调用 super()
。如果在构造函数里访问 this
属性之前,也必须调用 ·super()·
修饰符
public
TypeScript 中,所有成员默认为 public
,不用特别修饰。可以自由访问程序里定义的成员。
C# 要求必须明确地使用
public
指定成员可见,作以区分。
private
当成员被标记为 private
时,只能在其所在的类中访问,无法在外部访问。该成员是声明它的类所独有的。
私有变量命名一般会在前面加一个下划线 _私有变量
。
class Animal{
private _name: string;
constructor(theName: string){this.name = theName;}
}
new Animal("Cat").name; // error 'name' 是 Animal 类私有变量,只能在其中使用
protected
protected
同 private
,但可以在子类中访问。
class Person{
protected name: string; // name 变量私有,但可以继承
constructor(name: string){this.name = name};
}
class Employee extends Person{
private department: string;
constructor(name: string, department: string){
super(name); // name 变量由 protected 修饰,因此子类可用
this.department = department;
}
public getElevatorPitch(){
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name) // error name 变量是 protected 可继承私有
name 属性由 protected
修饰,Person 类之外是无法使用 name 变量,保证该属性不会被直接外部读取到。但是 Employee 类可以使用 name 属性(作为方法中的变量),因为它是 Person 的子类。
readonly
可以使用 readonly
修饰成员将其设置为只读。只读属性必须在声明时或构造函数里被初始化。
class Octopus{
readonly name: string;
readonly numberOfLegs: number = 8; // 声明时初始化只读
constructor(theName: string){
this.name = theName; // 构造函数时初始化只读
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error name 属性是只读的
get/set
TypeScript 可以通过 get/set
截取对对象成员的访问。这是一个存取器功能,get
设置一个存取器,从外部获取数据后保存,set
从保存中取出来使用。
没有存取器的类在设置 public
变量后可以很轻易地修改其中的成员。
// 创建一个 Employee 类
class Employee{
fullName: string; // 属性 fullName
}
let employee = new Employee(); // 创建实例 employee
employee.fullName = "Bob Smith";
if (employee.fullName){
console.log(employee.fullName);
}
上面的例子中 fullName
属性可以在外部随意设置。在下面的版本中加入了存取器功能,就可以设置私有的 _fullName
属性,阻止外部访问,在修改私有属性前通过存取器让访问的操作出现在内部,这样就可以在访问前做出如设置验证的额外操作。
使用 get 存取器(){return this.私有变量}
来设置对应私有变量的存取器。
使用 set 存取器(参数){使用}
将注入存取器的数据赋予参数,并进行使用。
let passport = "123456"; // 创建一个修改信息的验证密码
class Employee{
private _fullName: string; // 私有变量 _fullName,阻止对该属性的外部修改
// 设置存取器 fullName 来获得外部变量
get fullName(): string{
return this._fullName; // get 方法返回可能被修改的那个私有变量
}
// 取出存取器 fullName 的参数,并进行使用
set fullName(newName: string){ // 将存取器中保存的字符串赋予 newName 参数
if (passwords && passwords == "123456"){
this._fullName = newName; // 当密码正确的时候,对_fullName 进行修改
}
else{
console.log("密码错误,没有权限修改");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith"; // 存取器可以看做是一个属性
if (employee.fullName){
alert(employee.fullName);
}
存取器可以直接视为一个属性或者变量,使用 实例.存取器 = 注入的数据
向其中注入外部数据。存取器的作用是让外部环境的数据进入内部环境,这样就能获得内部环境的权限进行操作,比如对 private
修饰的私有变量赋值。
存取器要求编译器在 ES5 以上的版本。
只有 get
的存取器会被自动推断为 readonly 属性,用户在使用该属性时会被提醒。
static
类的静态成员存在于类本身而非实例上,使用 static
创建静态成员。
// 类 用于存储路径
class Router{
static baseRoute = '/basePath'; // 静态属性
// 方法 计算路径和
calculateRoute(path: string){
return Router.baseRoute + this.commonPrefix + path
}
// 构造函数 初始化时定义公共变量 commonPrefix
constructor (public commonPrefix: string){}
}
let route1 = new Router('/api'); // 实例 route1,赋值给 route1.commonPrefix
let route2 = new Router('/page'); // 实例 route2,赋值给 route2.commonPrefix
console.log(route1.calculateRoute('/main')); // 最终路径 /basePath/api/main
console.log(route2.calculateRoute('/user')); // 最终路径 /bathPath/api/main
没有参数的类声明由构造函数声明了属性变量后,在创建实例时会将实例参数赋予构造函数声明的属性变量。
abstract
抽象类是作为其他子类的基类(父类)使用的,一般不会被直接实例化。抽象类比接口包含了成员的实现细节。
abstract
关键字用于定义抽象类,或者在类的内部定义抽象方法。
abstract class Animals{
abstract makeSound(): void;
move(): void{
console.log("");
}
}
5 接口 interface
对数据的结构进行类型检查是 TypeScript 的核心原则之一,称为“鸭式辨型法”或“结构性子类型化”。接口的作用是为这些类型命名,并未代码定义契约。
function printLabel(labelledObj:{label: string}){
console.log(labelledObj.label);
}
let myObj = {size:10, label:"Size 10 Object"};
printLabel(myObj);
首先,类型检查器检查函数 printLabel
的调用。其有 1 个参数,为对象 labelledObj
。该对象有名为 label
的类型为 string
的参数。
在传入对象参数的时候可以包含很多类型,大多数情况下编译器只会检查必须的属性是否存在且匹配。
接口使用 interface 接口名{}
的形式定义。使用接口重新描述上面的例子:
// 接口 LabelledValue
interface LabelledValue{
label: string;
}
// 定义一个函数 printLabel 使用接口传入参数的类型
function printLabel(labelledObj: LabelledValue){
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
上述示例中,接口 LabelledValue
是一个对象,其中有属性变量 label
,类型为字符串。接口被传入了函数 printLabel
的参数中,即只要这个函数的参数符合接口里面的定义,就可以被传入。
对象变量 myObj
里面有符合接口定义的参数 label:"Size 10 Object"
,即便其还有其他参数,接口在传入的时候会忽略掉不符合参数,只用有用的。
类型检查器同样也不会检查属性的顺序,只要存在即可。
可选属性
接口中的属性不全是必须存在的,这是可选属性特性。可选属性在应用“option bags”模式的时候很常用,即给函数传入的参数对象中只有部分的属性有赋值的情况。
interface SquareConfig{
color?: string; // ? 标注可选属性
width?: number;
}
// 该函数的参数 config 使用了上面的接口,定义了两个可选属性
function createSquare(config: SquareConfig): {color:string; area:number}{
let newSquare = {color: "white", area: 100};
if (config.color){ // 使用了可选属性 color
newSquare.color = config.color;
}
if (config.width){ // 使用了可选属性width
newSquare.area = config.width*config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
可选属性可以对可能存在的属性进行预定义,也可以捕获引用不存在属性时的错误。
只读属性
使用 readonly
规定只读属性,这些属性只能在对象创建时修改,其他时间只能读不能写。
interface Point{
readonly x: number;
readonly y: number;
}
let p1: Point = {x: 10, y: 20};
评论