TypeScript语法速成
前言
对于没有接触过JS以外语言的读者,在学习TS前,需要知道以下几点
-
TS是JS的超集,它本身不是一种独立的语言
-
TS的作用是规范代码,限制因JS灵活的特性而写出一些吊炸天的代码
-
学习TS要结合编译后的JS一起理解,一些看上去很奇妙的代码,编译后平平无奇
-
TS多用于大型项目,必然结合框架使用,只学TS是毫无意义的
相关命令
npm install -g typescript
安装
tsc xxx.ts
会编译出名为 xxx.js
的文件
node xxx.js
运行
变量声明
我们知道相比JS,TS在声明变量时多了类型,不过与其他强类型语言不同,TS不是新的语言,它只是一种规范和超集,所以不会失去JS原有的特性,比如全局变量和局部变量,所以var
和let
仍然是在的,声明的时候只是多了个:type
,比如以下几种类型的变量声明:
var a : string = "string";
var b : number = 0;
var c : any = 0; //any代表任意类型
var d = 0; //不写就默认是any
a = 1; //error: a是string类型,不能赋值为number
b = 1; //正确,b本来就是number类型
c = "string"; //error:c虽然是any类型,但一旦赋值就确定了类型,首次赋值是个number,以后都是number
d = "string"; //同理
从以上可以看出,TS的类型声明都是在变量名后加:类型
,如果赋值的类型和变量的类型不一样,编译就会报错,注意是编译报错,JS本身是允许我们随意赋值的,请再回顾一下前言中的内容,相信您会有更好的理解。
以上代码在JS里也就长这样:
var a = "string";
var b = 0;
var c = 0;
var d = 0;
a = 1;
b = 1;
c = "string";
d = "string";
联合类型&类型推断
我们已经知道了any类型的存在,它允许我们在首次为其赋值时,值可以是任意类型。那么思考一下,既然any那么方便,而且声明时可以省略不写,项目中全部类型都用any不就好了?
-
可以,但又不完全可以,因为这样做的话,使用TS还有什么意义呢?TS本来就是对JS的一种规范用法,最终是编译成JS的,那还不如直接用JS开发。
既然这样,为什么要有any这个类型?
-
因为我可以不用,但不能没有。而且确实有些场景会需要一个变量,接受任意类型的值,当然这种情况是越少越好的,在能明确知道一个变量会接受什么类型的值时,尽量不要用any。
假如我们知道一个变量只接受特定几种类型,比如number和string时,使用any显得很浪费,我们可以使用联合类型,所谓联合类型,其实就是为一个变量多设置几个类型关键词,当然这不代表这个变量是多种类型的,只是放宽了规范而已。
//TS
var c : number | boolean;
c = 1;
c = true;
//JS
var c;
c = 1;
c = true;
从上面可以看到,无论变量c是什么类型,编译成JS后,就只是个var c
,所以说TS只是一种规范,确保在开发时候不会用到过多的隐式转换,将大量的bug扼杀在开发阶段。
顺便讲一下类型推断,所谓类型推断其实就是TS对于未声明类型的一种判断机制,看下面代码很快就能理解:
var e ; //e的类型是any
e = 0;
e = "a";
var f = 0; //f的类型是number
f = "str"; //error
类型断言
类型断言可以理解为类型转换,当一个变量A的类型不确定,但又得赋值给另一个变量B时,得保证A的类型和B是相同的,不过TS中的类型断言并不强大,在c#中,类型断言可以作为if的条件,而且是可以隐式转换的,TS中似乎杜绝了隐式转换,但又不是不能做到,所以这个功能在我看来有点奇妙,可以转换但又不能直接转换,断言的类型不对就会报错,那类型对了我还断言干嘛?可能是我还没接触到使用场景。
var c : number | boolean;
var d ;
d = c as number;
c = 1;
d = c as number;
c = true;
d = c as number; //error
var a : number = 0;
var x : string = a as unknown as string; //先转换成unknown再转换成string
var y : string = <string> <any> a; //泛型写法,原理同上
var z : string = a as any as string; //先转换成any再转换成string
函数
function greet():string { // 有返回值就把返回类型写在()后
return "Hello World"
}
function caller(str:string) { //参数
console.log(str)
}
数组
var arr : number = [1,2,3,4];
var arr1 : number = [1,2,3,"4"]; //error
var arr2 : any = [1,"2",true];
var arr3 = [1,"2",true];
//TS中有种叫元组的,其实就是JS的数组,以上arr2、arr3都是
接口
接口是TS中的部分,JS中没有接口,但最后还是会编译成JS的代码,TS的接口像C#中的抽象类,又有点像结构体,和数组配合可以将索引设为数字或字符串,这点又像Lua中的表,但本质上来说,就是JS中的对象,JS的对象本来就很灵活。
接口的作用是为数据提供一个模板,数据格式严格按模板来,而接口自身不做实现:
interface A { //接口A
firstName:string,
lastName:string,
sayHi: ()=>string
}
var B:A = {
firstName:"Tom",
lastName:"Hanks",
sayHi: ():string =>{return "Hi there"}
}
var C:A = {
firstName:"Jim",
lastName:"Blakes",
sayHi: ():string =>{return "Hello!!!"}
}
同样的,接口中的类型也可以是多样的:
interface RunOptions {
program:string;
commandline:string[]|string|(()=>string);
} //commandline可以是一个string类型的数组,也可以是string,也可以是函数
接口和数组可以设计关联数组:
//example1
interface A {
[index:number]:string
}
var list1:A = ["John","Bran"]
//example2
interface B {
[index:string]:number
}
var list2:B;
agelist["John"] = 15 // 正确
agelist[2] = "nine" // 错误
接口B可以继承自接口A,用extends
关键字继承:
interface A {
age:number
}
interface B extends A {
instrument:string
}
var drummer = <B>{}; //泛型写法,看个人喜好
drummer.age = 27
drummer.instrument = "Drums"
interface C extends E,F,G{ //继承多个接口
//...
}
类
这其实就是ES6的内容,简单过一下,如果您对类不熟悉,先记住以下三点类的组成部分:
-
字段 − 字段是类里面声明的变量。字段表示对象的有关数据。
-
构造函数 − 类实例化时调用,可以为类的对象分配内存。
-
方法 − 方法为对象要执行的操作。
class Car {
// 字段
engine:string;
// 构造函数,只要创建了类的实例,就会自动执行。可以不写,构造函数是默认存在的
constructor(engine:string) {
this.engine = engine
}
// 方法
disp():void {
console.log("发动机为 : "+this.engine)
}
}
编绎成JS后:
var Car = /** @class */ (function () {
// 构造函数
function Car(engine) {
this.engine = engine;
}
// 方法
Car.prototype.disp = function () {
console.log("发动机为 : " + this.engine);
};
return Car;
}());
顺便复习一下为什么方法要写在prototype中,拿上面的代码举例,因为在new一个Car对象时,会为engine开辟一块内存,再new一个Car,会再开一块内存,而protoype中的内容不会重复开辟内存空间,方法一般是可重用的,所以为了节约空间,会写在protoype中。
var a = new Car("a");
var b = new Car("B");
//a.engine !== b.engine
//a.disp === b.disp
这个prototype的特性其实和Static很像,不过TS中的Static并不是用protoype实现的,先说一下Static是什么。Static是类中静态成员的关键字,给静态成员赋值不需要实例化对象,直接类名.成员名
即可,这就意义着它像全局变量。
class StaticMem {
static num:number;
static disp():void {
console.log(StaticMem.num)
}
}
StaticMem.num = 12 // 初始化静态变量
StaticMem.disp() // 调用静态方法
//编译成JS
var StaticMem = /** @class */ (function () {
function StaticMem() {
}
StaticMem.disp = function () {
console.log(StaticMem.num);
};
return StaticMem;
}());
StaticMem.num = 12; // 初始化静态变量
StaticMem.disp(); // 调用静态方法
访问修饰符是用于设定类中成员、方法可访问程度的关键字
-
public(默认) : 公有,可以在任何地方被访问。
-
protected : 受保护,可以被其自身以及其子类和父类访问。
-
private : 私有,只能被其定义所在的类访问。
class A{
private a:number = 0;
b:string;
}
var c = new A();
c.a //error:不可访问
c.b //可以访问
类之间可以继承,但只能单继承,子类默认拥有父类的成员和方法,如果父类中有一个A方法,子类中也写了一个A方法,也就是方法名称相同,那么这就叫重写;如果子类中写super.A()
则是调用父类中的A方法。
class Parent {
A():void {
}
}
class Child extends Parent {
A():void {
super.A() // 调用父类的函数
//...自己的逻辑
}
}
命名空间
命名空间主要是为了处理变量名重复的问题,如下代码,看一眼应该就理解了:
namespace SomeNameSpaceName {
export interface ISomeInterfaceName { }
export class SomeClassName { }
}
现在有一种情况,几个不同的文件,用着同一个命名空间,而且会用到其他文件相同命名空间下的变量,该怎么做?用<reference path = "A.ts" />
引用:
//A.ts
namespace Drawing {
export interface IShape {
draw();
}
}
//B.ts
/// <reference path = "A.ts" />
namespace Drawing {
export Circle extends IShape {
}
}
//run.ts
/// <reference path = "A.ts" />
/// <reference path = "B.ts" />
//用到什么就引用什么
命名空间还可以嵌套,不过咱还是别整这些花里胡哨的,了解即可。
文章中还有一些没有讲到的内容,比如对象,循环之类 ,这些都是JS中差不多的东西,此文主要是方便迅速熟悉TS,由于只是讲语法相关内容,所以声明文件