深度掌握TypeScript中的過載【函數過載、方法過載】

2023-12-06 12:02:15

深度掌握TypeScript中的過載【函數過載、方法過載】

1. 函數過載,方法過載的重要性

著名前端流行框架底層都用到函數過載,例如:Vue3 底層原始碼就多處使用到帶泛型的函數過載。很多前端面試更是拿函數過載作為考核求職者 TS 技能是否紮實的標準之一,如果你不掌握函數過載,等於你的 TS 技能有缺失,技能不過關。

函數過載或方法過載適用於完成專案種某種相同功能但細節又不同的應用場景,我們舉一個生活中的例子讓同學們先有個印象,比如:吃飯是一個函數,表示一個吃飯功能,但西方人用叉子,中國人用筷子,這就是細節不同,那如果我們可以用函數過載來解決。

不管現階段你公司的專案中是否用到了函數過載和方法過載【如果沒有用,多半是公司不少人用的並不熟練才不用的緣故】。

函數過載或方法過載有以下幾個優勢:

優勢1: 結構分明

讓 程式碼可讀性,可維護性提升許多,而且程式碼更漂亮。

優勢2: 各司其職,自動提示方法和屬性

每個過載簽名函數完成各自功能,輸出取值時不用強制轉換就能出現自動提示,從而提高開發效率

優勢3:更利於功能擴充套件

2.函數過載的定義

TS 的函數過載比較特殊,和很多其他後端語言的方法過載相比,多了不少規則。學習函數過載,先要了解什麼是函數簽名,定義如下:

函數簽名 [ function signature ]:函數簽名=函數名稱+函數引數+函數引數型別+返回值型別四者合成。在 TS 函數過載中,包含了實現簽名和過載簽名,實現簽名是一種函數簽名,過載簽名也是一種函數簽名。

關於函數過載的定義,我們先來看一個很多其他資料提供的不完整且模糊的TS函數過載定義:

不完整模糊的 TS 函數過載定義:一組具有相同名字,不同參數列的和返回值無關的函數 。

完整的函數過載定義:包含了以下規則的一組函數就是TS函數過載 :

規則1 由一個實現簽名+ 一個或多個過載簽名合成。

規則2 但外部呼叫函數過載定義的函數時,只能呼叫過載簽名,不能呼叫實現簽名,這看似矛盾的規則,其實 是TS 的規定:實現簽名下的函數體是給過載簽名編寫的,實現簽名只是在定義時起到了統領所有過載簽名的作用,在執行呼叫時就看不到實現簽名了。

規則3 呼叫過載函數時,會根據傳遞的引數來判斷你呼叫的是哪一個函數

規則4 只有一個函數體,只有實現簽名配備了函數體,所有的過載簽名都只有簽名,沒有配備函數體。

規則5 關於引數型別規則完整總結如下:

實現簽名引數個數可以少於過載簽名的引數個數,但實現簽名如果準備包含過載簽名的某個位置的引數 ,那實現簽名就必須相容所有過載簽名該位置的引數型別【聯合型別或 any 或 unknown 型別的一種】。

規則6 關於過載簽名和實現簽名的返回值型別規則完整總結如下:

必須給過載簽名提供返回值型別,TS 無法預設推導。

提供給過載簽名的返回值型別不一定為其執行時的真實返回值型別,可以為過載簽名提供真實返回值型別,也可以提供 void 或 unknown 或 any 型別,如果過載簽名的返回值型別是 void 或 unknown 或 any 型別,那麼將由實現簽名來決定過載簽名執行時的真實返回值型別。 當然為了呼叫時能有自動提示+可讀性更好+避免可能出現了型別強制轉換,強烈建議為過載簽名提供真實返回值型別。

不管過載簽名返回值型別是何種型別,實現簽名都可以返回 any 型別 或 unknown型別,當然一般我們兩者都不選擇,讓 TS 預設為實現簽名自動推導返回值型別。

3.方法過載

方法:方法是一種特定場景下的函數,由物件變數【範例變數】直接呼叫的函數都是方法。

比如:

  1. 函數內部用 this 定義的函數是方法;

  2. TS 類中定義的函數是方法【 TS 類中定義的方法就是編譯後 JS 底層 prototype 的一個函數】;

  3. 介面內部定義的函數是方法【注意:不是介面函數】;

  4. type 內部定義的函數是方法【注意:不是 type 函數】。

方法簽名和函數簽名一樣,方法簽名 = 方法名稱 + 方法引數 + 方法引數型別 + 返回值型別四者合成。

4.構造器 【建構函式】過載

(1)再次強化this

this 其實是一個物件變數,當 new 出來一個物件時,構造器會隱式返回 this 給 new 物件等號左邊的物件變數,this 和等號左邊的物件變數都指向當前正建立的物件。

以後,哪一個物件呼叫 TS 類的方法,那麼這個方法中的 this 都指向當前正使用的物件【 this 和當前的物件變數中都儲存著當前物件的首地址】

(2)TS構造器沒有返回值

儘管TS類構造器會隱式返回 this,如果我們非要返回一個值,TS 類構造器只允許返回 this,但構造器不需要返回值也能通過編譯,更沒有返回值型別之說,從這個意義上,TS 構造器可以說成是沒有返回值這一說的建構函式。【注意:TS 構造器和 JS 建構函式關於返回值的說法不完全相同

(3)構造器不是方法

我們說物件呼叫的才是方法,但是 TS 構造器是在物件空間地址賦值給物件變數之前被呼叫,而不是用來被物件變數呼叫的,所以構造器( constructor )可以說成建構函式,但不能被看成是一個方法。

(4)構造器【建構函式】過載的意義

構造器過載和函數過載使基本相同,主要區別是:TS 類構造器過載簽名和實現簽名都不需要管理返回值,TS 構造器是在物件建立出來之後,但是還沒有賦值給物件變數之前被執行,一般用來給物件屬性賦值。

我們知道在 TS 類中只能定義一個構造器,但實際應用時,TS 類在建立物件時經常需要用到有多個構造器的場景,比如:我們計算一個正方形面積,建立正方形物件,可以給構造器傳遞寬和高,也可以給構造器傳遞一個包含了寬和高的形狀引數物件,這樣需要用構造器過載來解決。而面試中也多次出現過關於TS構造器過載的考察,主要考察求職者對過載+構造器的綜合運用能力。

5.函數過載實戰

場景:聊天應用中,傳送的訊息分為很多型別,例如:圖片、文字、語音等等,現在需要封裝一個訊息查詢函數,根據傳入的引數從陣列中查詢資料,如果入參為數位, 就認為訊息 id,然後從從後端資料來源中找對應 id 的資料並返回,否則當成型別,返回這一型別的全部訊息。

程式碼1,不使用函數過載的特性:

type MessageType = "image" | "audio" | string;//訊息型別
//type xtype=string
//boolean true false
type Message = {
  id: number;
  type: MessageType;
  sendmessage: string;
};
//let msgobj:Message={id:23,type:"df",sendmessage:"abc"}
//let obj={username:"wangwu",age:23}
let messages: Message[] = [
  //let messages: Array<Message> = [
  {
    id: 1, type: 'image', sendmessage: "你好啊,今晚咱們一起去三里屯吧",
  },
  {
    id: 2, type: 'audio', sendmessage: "朝辭白帝彩雲間,千里江陵一日還"
  },
  {
    id: 3, type: 'audio', sendmessage: "你好!張無忌"
  },
  {
    id: 4, type: 'image', sendmessage: "劉老根苦練舞臺絕技!"
  },
  {
    id: 5, type: 'image', sendmessage: "今晚王牌對王牌節目咋樣?"
  }]

//不用函數過載來實現
// 1.函數結構不分明,可讀性,可維護性變差
function getMessage(value: number | MessageType):
  Message | undefined | Array<Message> {
  if (typeof value === "number") {
    return messages.find((msg) => { return value === msg.id })
  } else {
    //return messages.filter((msg) => { return value === msg.type })
    return messages.filter((msg) => value === msg.type)
  }
}
// 自定義守衛
console.log(getMessage("audio"));
// TS沒有辦法執行之前根據傳遞的值來推導方法最終返回的資料的資料型別
// 只可以根據方法定義的型別展現
//let msg=getMessage(1) 
//console.log(msg.sendMessage)//錯誤 型別「Message | Message[]」上不存在屬性「sendMessage」。
//  型別「Message」上不存在屬性「sendMessage」,聯合型別使用方法,必須是交集(公共方法)!
let msg = (<Message>getMessage(1)).sendmessage // 型別轉換成Message或型別斷言
console.log("msg:", msg)// msg: 你好啊,今晚咱們一起去三里屯吧

export { }

程式碼2,使用函數過載實現:

type MessageType = "image" | "audio" | string;//訊息型別

type Message = {
  id: number;
  type: MessageType;
  sendmessage: string;
};

let messages: Message[] = [
  //let messages: Array<Message> = [
  {
    id: 1, type: 'image', sendmessage: "你好啊,今晚咱們一起去三里屯吧",
  },
  {
    id: 2, type: 'audio', sendmessage: "朝辭白帝彩雲間,千里江陵一日還"
  },
  {
    id: 3, type: 'audio', sendmessage: "你好!張無忌"
  },
  {
    id: 4, type: 'image', sendmessage: "劉老根苦練舞臺絕技!"
  },
  {
    id: 5, type: 'image', sendmessage: "今晚王牌對王牌節目咋樣?"
  }]

// function getMessage(value: number | MessageType):
//   Message | undefined | Array<Message> {
//   if (typeof value === "number") {
//     return messages.find((msg) => { return value === msg.id })
//   } else {
//     //return messages.filter((msg) => { return value === msg.type })
//     return messages.filter((msg) => value === msg.type)
//   }
// }
// 過載簽名可以有多個
function getMessage(value: number, myname: string): Message//第一個根據數位id來查詢單個訊息的 過載簽名
function getMessage(value: MessageType, readRecordCount: number): Message[]//第二個根據訊息型別來查詢訊息陣列的 過載簽名

// 實現簽名函數,只有實現簽名才有函數體,實現簽名只有一個
function getMessage(value: any, value2: any = 1) {
  //console.log(myname)
  if (typeof value === "number") {
    return messages.find((msg) => { return 6 === msg.id })//undefined
  } else {
    //return messages.filter((msg) => { return value === msg.type })
    return messages.filter((msg) => value === msg.type).splice(0, value2)
  }
}
getMessage(1, "df") // 這裡呼叫的是第一個過載簽名
// let x: number = 3;
// let y: any = x;

// let z: any = 3;
// let k: number = z;

// function go(value:number,readRecordCount:number=1){

// }
// go(1);


//console.log(getMessage(6).sendmessage); 根據傳參,判斷為第一個過載簽名,所以這裡的sendmessage可以正常呼叫,不需要型別轉換
getMessage("image", 2).forEach((msg) => {
  console.log(msg);
})

export { }

6.方法過載實戰

場景: Java 簡易版 ArrayList 中的經典應用實現【 ArrayList 可彌補 Set 取值不方便短板,比 Set 刪除功能更方便】

程式碼:

//  1.對現有的陣列進行封裝,讓陣列增刪改變得更加好用
//  2.提供get方法 remove方法 顯示方法【add方法】
// 其中需求中的remove方法有兩個,我們用方法過載來實現

class ArrayList {
  //第一步:定義一個參照屬性【陣列】
  constructor(public element: Array<object>) {

  }
  // 第二步:根據索引來查詢陣列中指定元素
  get(index: number) {
    return this.element[index]
  }

  // 第三步: 顯示方法
  show() {
    this.element.forEach((ele) => {
      console.log(ele);
    })
  }

  remove(value: number): number
  remove(value: object): object
  //remove(value: number | object): number | object {
  remove(value: any): any {
    this.element = this.element.filter((ele, index) => {
      //如果是根據數位【元素索引】去刪除元素,remove方法返回的是一個數位
      if (typeof value === "number") {
        return value !== index
      } else {
        // 如果是根據物件去刪除元素,remove方法返回的是一個物件
        return value !== ele
      }
    })
    return value;
  }

}

let stuOne = { stuname: "wnagwu", age: 23 }
let stuTwo = { stuname: "lisi", age: 39 }
let stuThree = { stuname: "liuqi", age: 31 }

let arrayList = new ArrayList([stuOne, stuTwo, stuThree]);
arrayList.show();

console.log("刪除第一個學生");
// let value = arrayList.remove(0)
// console.log("刪除的元素為第:", value, "學生")
// arrayList.show();
let value = arrayList.remove(stuTwo)
console.log("刪除的學生物件為:", value)
arrayList.show();
// 如果是根據數位【元素索引】去刪除元素,remove方法返回的是一個數位
// 如果是根據物件去刪除元素,remove方法返回的是一個物件
//let value=arr.remove(1)

7.構造器過載實戰

場景:計算正方形面積

// 計算正方形面積
// 計算建立正方形物件,可以給構造器傳遞寬和高
// 也可以給構造器傳遞一個包含了寬和高的形狀引數物件,這樣需要用構造器過載
type type_ChartParam = { // 各個圖形求面積引數
  width?: number,
  height?: number,
  radius?: number
}

class Square {
  public width: number;
  public height: number;

  constructor(width_: number, height_: number) // 過載簽名
  constructor(value_: type_ChartParam) // 過載簽名
  // constructor(value_: number | type_ChartParam) { // 實現簽名
  constructor(paramObjOrWidth_: any, height_: number = 0) { // 實現簽名
    if (typeof paramObjOrWidth_ === 'object') {
      this.width = paramObjOrWidth_.width;
      this.height = paramObjOrWidth_.height;
    } else {
      this.width = paramObjOrWidth_;
      this.height = height_;
    }

  }

  public getArea(): number {
    return this.height * this.width
  }
}

let square1 = new Square(40, 50);
let chartParamObj: type_ChartParam = {width: 50, height: 90}
let square2 = new Square(chartParamObj);
console.log(square1.getArea()); // => 2000
console.log(square2.getArea()); // => 4500

腳踏實地行,海闊天空飛!