TypeScript学习笔记之三

734 阅读9分钟

函数

函数是JavaScript中极其重要的一部分,我们一直把函数称之为一等公民,它是任何应用程序的基本构建块。

Functions

和JavaScript一样,TypeScript创建的函数可以分为命名函数和匿名函数。

  • JavaScript两种创建函数的方法,代码如下:

    //命名函数
    function add(x,y){
        return x + y;
    };
    
    //匿名函数
    let add2 = function(x,y){
        return x + y;
    };
    

TypeScript的功能类型

  • 标注类型

    以上面的JavaScript创建函数的两种方式为例子,代码如下:

    //命名函数
    function add(x:number,y:number):number{
        return x + y;
    };
    
    //匿名函数
    let add2 = function(x:number,y:number):number{
        return x + y;
    };
    

    通过代码可以看出我们可以给每一个参数添加类型,也可以给返回值添加类型。

  • 必需参数、可选参数、默认参数和不定参数

    • 必需参数

      在TypeScript中,赋予函数的参数数量必须与函数期望传入的参数数量相匹配,否则就回报出相应的错误

      function Person(name:string,age:number){
          return name + '有' + age + '岁'
      }
      
      let person1 = Person('jack')//会提示预期参数有两个,但函数只得到了一个
      let person2 = Person('jack',18,'打篮球')//会提示预期参数有两个个,但函数得到了三个
      let Person3 = Person('jack',18)//right
      
    • 可选参数

      在JavaScript中,函数中的每一个参数都是可选,用户可以根据需要将其保留。在TypeScript中我们如果需要一个可选参数的时候,我们可以在我们希望该参数成为可选参数的后面加一个**?,代表该参数是一个可选参数**。

      function Person(name:string,age:number,hobby?:string){
          return name + '有' + age + '岁'+'喜欢' + hobby; 
      }
      let result1 = Person("Bob");//会提示预期有2-3参数,但是函数只得到一个
      let result2 = Person("Bob", 18, "打篮球");//right
      let result3 = Person("Bob", 18);//right
      
    • 默认参数

      在TypeScript中,我们可以为函数参数设置一个值,如果用户不提供参数或用户将undefined其传递给它时,则将为该参数分配一个值,我们把这种参数称之为默认参数

      function Person(name:string,age:number,hobby='打篮球'){
          return name + '有' + age + '岁'+'喜欢' + hobby; 
      }
      let result1 = Person("Bob");//会提示预期有2-3参数,但是函数只得到一个
      let result2 = Person("Bob", 18, "打篮球");//right
      let result3 = Person("Bob", 18);//right
      let result4 = Person("Bob", 18, "踢足球");//right
      let result5 = Person("Bob", 18,undefined);//right
      
    • 不定参数

      必需参数可选参数默认参数都是一次只讨论一个参数,与它们不同的是不定参数是由多个参数作为一个组来组成的。在JavaScript中的不定参数我们可以使用arguments来处理,而在TypeScript中我们可以使用一个省略号(...)来标示该参数是一个不定参数。

      function Person(name:string,...hobby:string[]){
          return name +'喜欢' + hobby.join(''); 
      }
      let result2 = Person("Bob", "打篮球",'踢足球','打羽毛球');//right
      let result3 = Person("Bob", 18,"打篮球",'踢足球','打羽毛球');
      //“number”类型的参数不能赋值给“string”类型的参数
      

this

学习如何在JavaScript中使用this是一个很重要的技能,由于TypeScript是JavaScript的一个超集,因此TypeScript开发人员还需要去学习在TypeScript中如何去正确的使用this。

  • this和箭头函数

    在JavaScript中,由于this是一个在调用函数时设置的变量,这使其成为一个非常强大且灵活的功能,但是必须以执行的上下文作为代价,这使其在返回函数或者将函数作为参数传递时会导致this的指向出现问题,例如下面的例子:

    let deck = {
      suits: ["红心", "黑桃", "草花", "方块"],
      cards: Array(52),
      createCardPicker: function () {
        return function () {
          let pickedCard = Math.floor(Math.random() * 52);
          let pickedSuit = Math.floor(pickedCard / 13);
          console.log(this);//this是一个undefined
          return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
        };
      },
    };
    
    let cardPicker = deck.createCardPicker();
    let pickedCard = cardPicker();//运行报错,‘this’具有隐式类型的属性,所以该批注没有any类型
    console.log("card: " + pickedCard.card + " of " + pickedCard.suit);
    

    我们可以通过使用ES6的箭头语法来确保this能够绑定到正确的位置来解决该this的指向问题,代码如下:

    let deck = {
      suits: ["红心", "黑桃", "草花", "方块"],
      cards: Array(52),
      createCardPicker: function () {
        return () => {
          let pickedCard = Math.floor(Math.random() * 52);
          let pickedSuit = Math.floor(pickedCard / 13);
    
          return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
        };
      },
    };
    
    let cardPicker = deck.createCardPicker();
    let pickedCard = cardPicker();
    
    console.log("card: " + pickedCard.card + " of " + pickedCard.suit);
    
  • this参数

    上面的例子通过箭头语法解决该this的指向问题,但是该this类型仍然是一个any类型,我们可以提供一个显式的this参数来解决该问题,代码如下:

    interface Card {
      suit: string;
      card: number;
    }
    
    interface Deck {
      suits: string[];
      cards: number[];
      createCardPicker(this: Deck): () => Card;
    }
    
    let deck: Deck = {
      suits: ["红心", "黑桃", "草花", "方块"],
      cards: Array(52),
      createCardPicker: function (this: Deck) {
        return () => {
          let pickedCard = Math.floor(Math.random() * 52);
          let pickedSuit = Math.floor(pickedCard / 13);
    
          return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
        };
      },
    };
    
    let cardPicker = deck.createCardPicker();
    let pickedCard = cardPicker();
    
    console.log("card: " + pickedCard.card + " of " + pickedCard.suit);
    

    通过提供一个显式的this参数,我们现在可以得知this现在是一个Deck类型,而不是一个any类型。

重载

描述一个函数在传入不同参数产生不同的结果,以便于类型检查,代码如下:

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x: any): any {
  if (typeof x == "object") {
    let pickedCard = Math.floor(Math.random() * x.length);
    return pickedCard;
  }
  // Otherwise just let them pick the card
  else if (typeof x == "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13 };
  }
}

let myDeck = [
  { suit: "diamonds", card: 2 },
  { suit: "spades", card: 10 },
  { suit: "hearts", card: 4 },
];

let pickedCard1 = myDeck[pickCard(myDeck)];
console.log("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(12);
console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit);

该例子只有两个重载:一个重载一个对象,一个重载一个数字,如果函数pickCard()传入的参数是其他的类型进行调用会导致编译出错。

类的定义

类是面向对象程序设计实现信息封装的基础,类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数,类的实例称为对象。

类的属性和方法

一个类一般可以包含了静态属性、成员属性、构造函数、静态方法和成员方法,代码如下所示:

class Person{
  //静态属性
  static names:string = 'jack';
  //成员属性
  age:string;
  //构造函数
  constructor(message:string){
    this.age = message;
  };
  // 静态函数
  static getName(){
    return this.names
  }
  // 成员函数
  getAge(){
    return `${Person.names}现在有${this.age}岁`
  }
}
let jackAge = new Person('16');
console.log(jackAge.getAge());
console.log(Person.getName());

我们在上面代码中可以了解到静态成员是在类的本身,在实例上是不可见的,也就是说当我们需要访问该静态成员时,我们需要在类的本身调用该静态成员。

继承

继承就是子类继承父类的特征和行为,使得子类拥有父类的属性和方法,是一种扩展父类最基本的方法之一,代码如下:

class Animal {
  name:string;
  constructor(newName:string){
    this.name = newName;
  }
  move(distance: number = 0) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog('Dog');
dog.move(10);
dog.bark();
console.log(dog.name);

ECMAScript私有字段

在Typescript3.8的版本中,Typescript支持ECMAScript私有字段的语法,代码如下所示:

class Person {
  #name: string;
  constructor(theName: string) {
    this.#name = theName;
  }
}

new Person("jack").#name;
//Property '#name' is not accessible outside class 'Person' because it has a private identifier.

如代码中的报错可以知道我们私有字段与常规属性是不同的,虽然声明了#name,但它的值从不会被读取,私有字段有下面几个特点:

  • 私有字段使用一个"#"字符作为开头
  • 私有字段只作用于其包含的类
  • 私有字段不能不能再其包含的类之外访问,也不会被检测到

Typescript私有字段

Typescript中也有其自己的方法(private)将成员标记成私有字段,也是一个不能从其包含类的外部进行访问,代码如下:

class Person {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Jack extends Person {
  private department: string;
  constructor(name: string, department: string) {
  super(name);
    this.department = department;
  }

  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }//Property 'name' is private and only accessible within class 'Person'(属性“name”是私有的,只能在类“Person”中访问)
}

let howard = new Jack("Jack", "技术部");
console.log(howard.getElevatorPitch());
console.log(Jack.name);
console.log(Person.name)
console.log(howard.name);
//Property 'name' is private and only accessible within class 'Person'(属性“name”是私有的,只能在类“Person”中访问)

在Typescript中有一个protected的行为和private的行为很像,不同的是用protected声明成员可以用派生类来访问到该成员,代码如下:

class Person {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Jack extends Person {
  private department: string;
  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }

  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

let howard = new Jack("Jack", "技术部");
console.log(howard.getElevatorPitch());
console.log(Jack.name);
console.log(Person.name)
console.log(howard.name);//Property 'name' is protected and only accessible within class 'Person' and its subclasses.(属性“name”是受保护的,只能在类“Person”及其子类中访问。)

protected在标记一个构造函数的时候,该类不能被实例化,但是可以扩展(继承),代码如下:

class Person {
  protected name: string;
  protected constructor(theName: string) {
    this.name = theName;
  }
}
//能够被继承
class Jack extends Person {
  private department: string;
  constructor(name: string, department: string) {
    super(name);
    this.department = department;
  }

  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

let howard = new Jack("Jack", "技术部");
let john = new Person("John");//Constructor of class 'Person' is protected and only accessible within the class declaration.(类Person的构造函数是受保护的,只能在类声明中访问。)

只读修饰符

在Typescrip中,我们可以使用readonly关键字将属性设置为只读,只读属性必须在其声明或构造函数中进行初始化,代码如下:

class Person {
  readonly name: string;
  readonly numberOfLegs: number = 8;

  constructor(theName: string) {
    this.name = theName;
  }
}

let dad = new Person("jack");
console.log(dad.name);
dad.name = "jack";//name是一个只读属性,不能被赋值

访问器

Typescript中支持getters/setters方法,通过对拦截对象成员的访问,便于更好地控制如何访问每个对象上的成员,这样有利于防止出现异常数据,代码如下:

let nameMaxLength = 8;
class Person {
  private _name: string;
  constructor(newName:string){
    this._name = newName;
  }
  get name(): string {
    return this._name;
  };
  set name(newName: string) {
    if (newName && newName.length > nameMaxLength) {
      console.log('Names cannot be longer than '+ nameMaxLength);
      return;
    }
    this._name = newName;
  }
}

let Jack = new Person('Jack');
Jack.name = 'JackWangs';
if(Jack.name){
console.log(Jack.name);
}

抽象类

抽象类是一种可以派生到其他类的基类,抽象类无法被实例化。与接口不同的是,抽象类可能包含了其他成员的实现详细信息,一般我们使用abstract关键字声明定义抽象类,代码如下:

abstract class Animal{
  name:string;
  constructor(newName:string){
    this.name = newName;
  }
  abstract move(distance:number):void
}
let dog = new Animal();//error:Cannot create an instance of an abstract class

由于抽象类不能直接实例化,我们可以通过实例化该抽象类的子类来调用该抽象类的属性和方法,代码如下:

abstract class Animal{
  name:string;
  constructor(newName:string){
    this.name = newName;
  }
  abstract move(distance:number):void
}
class Dogs extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distance: number): void {
    console.log(`${this.name} moved ${distance}m`);
  }
}
let dog = new Dogs('dog');
dog.move(10);

类方法重载

类方法重载和函数重载的方式大同小异,代码如下:

class Animal{
	getID():void;
    getID(id: number):void;
    getID(id?: number){
        if(typeof id==="number"){
          console.log(`获取id为${id}的动物种类`);
        }else{
          console.log('获取全部动物种类');
        }
    }
}
let dog = new Animal();
dog.getID(10);//"获取id为10的动物种类" 
dog.getID()//"获取全部动物种类" 

使用类作为接口

类声明创建了两件事:代表类实例的类型和构造函数,由于类创建类型,所以可以在可以使用接口的位置使用它们,代码如下:

class Person{
    name:string;
    age:number
}

interface Person2 extends Person{
  hobby:string
}
let jack:Person2={name:'jack',age:19,hobby:'打篮球'};

function person(msg:Person2){
  console.log(msg);
}
person(jack);//{"name": "jack","age": 19,"hobby": "打篮球"}