1. 程式人生 > 其它 >TypeScript學習(二)函式過載

TypeScript學習(二)函式過載

函式過載

這個概念是在一些強型別語言中才有的,在JS中依據不同引數型別或引數個數執行一些不同函式體的實現很常見,依託於TypeScript,就會有需要用到這種宣告的地方。

函式過載定義:函式名相同,函式的引數列表不同(包括引數個數和引數型別),根據引數的不同去執行不同的操作。

關於函式過載,必須要把精確的定義放在前面,最後函式實現時,需要使用|操作符或者?操作符,把所有可能的輸入型別全部包含進去

js函式過載

js 因為是動態型別,本身不需要支援過載,直接對引數進行型別判斷即可,因此js沒有真正意義上的函式過載

我們舉個例子看看

function overload(a){
    console.log(
'一個引數') } function overload(a,b){ console.log('兩個引數') } // 在支援過載的程式語言中,比如 java overload(1); //一個引數 overload(1,2); //兩個引數 // 在 JavaScript 中 overload(1); //兩個引數 overload(1,2); //兩個引數

在JavaScript中,同一個作用域,出現兩個名字一樣的函式,後面的會覆蓋前面的,所以 JavaScript 沒有真正意義的過載。

但是有各種辦法,能在 JavaScript 中模擬實現過載的效果。

先看第一種辦法,通過arguments 物件來實現

arguments 物件,是函式內部的一個類陣列物件,它裡面儲存著呼叫函式時,傳遞給函式的所有引數。

function overload () {
  if (arguments.length === 1) {
    console.log('一個引數')
  }
  if (arguments.length === 2) {
    console.log('兩個引數')
  }
}

overload(1);      //一個引數
overload(1, 2);  //兩個引數

這個例子非常簡單,就是通過判斷 arguments 物件的 length 屬性來確定有幾個引數,然後執行什麼操作。

但是引數少的情況下,還好,如果引數多一些,if 判斷就需要寫好多,就麻煩了。

所以,我們再來看一個經典的例子 在看這個例子之前,我們先來看一個需求,我們有一個 users 物件,users 物件的values 屬性中存著一些名字。 一個名字由兩部分組成,空格左邊的是 first-name ,空格右邊的是 last-name,像下面這樣。

var users = {
  values: ["Dean Edwards", "Alex Russell", "Dean Tom"]
};

我們要在 users 物件 中新增一個 find 方法,

當不傳任何引數時, 返回整個users .values

當傳一個引數時,就把 first-name 跟這個引數匹配的元素返回;

當傳兩個引數時,則把 first-name 和 last-name 都匹配的返回。

這個需求中 find方法 需要根據引數的個數不同而執行不同的操作,下來我們通過一個 addMethod 函式,來在 users 物件中新增這個 find 方法。

function addMethod (object, name, fn) {
  // 先把原來的object[name] 方法,儲存在old中
  var old = object[name];

  // 重新定義 object[name] 方法
  object[name] = function () {
    // 如果函式需要的引數 和 實際傳入的引數 的個數相同,就直接呼叫fn
    if (fn.length === arguments.length) {
      return fn.apply(this, arguments);

      // 如果不相同,判斷old 是不是函式,
      // 如果是就呼叫old,也就是剛才儲存的 object[name] 方法
    } else if (typeof old === "function") {
      return old.apply(this, arguments);
    }
  }
}

addMethod 函式,它接收3個引數
第一個:要繫結方法的物件,

第二個:繫結的方法名稱,

第三個:需要繫結的方法

這個 addMethod 函式在判斷引數個數的時候,除了用 arguments 物件,還用了函式的 length 屬性。

函式的 length 屬性,返回的是函式定義時形參的個數。

簡單說 函式的 length 是,函式需要幾個引數,而arguments.length是呼叫函式時,真的給了函式幾個引數

function fn (a, b) {
  console.log(arguments.length)
}
console.log(fn.length);  // 2
fn('a');    // 1

下來我們來使用這個 addMethod 函式

// 不傳引數時,返回整個values陣列
function find0 () {
  return this.values;
}
// 傳一個引數時,返回firstName匹配的陣列元素
function find1 (firstName) {
  var ret = [];
  for (var i = 0; i < this.values.length; i++) {
    if (this.values[i].indexOf(firstName) === 0) {
      ret.push(this.values[i
      ]);
    }
  }
  return ret;
}
// 傳兩個引數時,返回firstName和lastName都匹配的陣列元素
function find2 (firstName, lastName) {
  var ret = [];
  for (var i = 0; i < this.values.length; i++) {
    if (this.values[i
    ] === (firstName + " " + lastName)) {
      ret.push(this.values[i
      ]);
    }
  }
  return ret;
}
// 給 users 物件新增處理 沒有引數 的方法
addMethod(users, "find", find0);

// 給 users 物件新增處理 一個引數 的方法
addMethod(users, "find", find1);

// 給 users 物件新增處理 兩個引數 的方法
addMethod(users, "find", find2);

// 測試:
console.log(users.find()); //["Dean Edwards", "Alex Russell", "Dean Tom"]
console.log(users.find("Dean")); //["Dean Edwards", "Dean Tom"]
console.log(users.find("Dean","Edwards")); //["Dean Edwards"]

addMethod 函式是利用了閉包的特性,通過變數 old 將每個函式連線了起來,讓所有的函式都留在記憶體中。

每呼叫一次 addMethod 函式,就會產生一個 old,形成一個閉包。 我們可以通過console.dir(users.find),把 find 方法列印到控制檯看看。

上面這個例子是 jQuery 之父John Resig寫的,他在他的部落格和他寫的書《secrets of the JavaScript ninja》第一版中都有提到過,在書中的第4章中也有講解 Function overloading,文中的 addMethod 函式 就是書中的例子 4.15,感興趣的朋友可以去看看。

上面的例子,本質都是在判斷引數的個數,根據不同的個數,執行不同的操作,而下來舉的例子是通過判斷引數的型別,來執行不同的操作。

TS函式過載

為同一個函式提供多個函式型別定義來進行函式過載,目的是過載的pickCard函式在呼叫的時候會進行正確的型別檢查。

例子1:例如我們有一個add函式,它可以接收string型別的引數進行拼接,也可以接收number型別的引數進行相加。

// 上邊是宣告
function add (arg1: string, arg2: string): string
function add (arg1: number, arg2: number): number
// 因為我們在下邊有具體函式的實現,所以這裡並不需要新增 declare 關鍵字

// 下邊是實現
function add (arg1: string | number, arg2: string | number) {
  // 在實現上我們要注意嚴格判斷兩個引數的型別是否相等,而不能簡單的寫一個 arg1 + arg2
  if (typeof arg1 === 'string' && typeof arg2 === 'string') {
    return arg1 + arg2
  } else if (typeof arg1 === 'number' && typeof arg2 === 'number') {
    return arg1 + arg2
  }
}

TypeScript 中的函式過載也只是多個函式的宣告,具體的邏輯還需要自己去寫,他並不會真的將你的多個重名 function 的函式體進行合併

考慮如下例子2:

interface User {
  name: string;
  age: number;
}

declare function test(para: User | number, flag?: boolean): number;
在這個 test 函式裡,我們的本意可能是當傳入引數 para 是 User 時,不傳 flag,當傳入 para 是 number 時,傳入 flag。TypeScript 並不知道這些,當你傳入 para 為 User 時,flag 同樣允許你傳入:
const user = {
  name: 'Jack',
  age: 666
}

// 沒有報錯,但是與想法違背
const res = test(user, false);

使用函式過載能幫助我們實現:

interface User {
  name: string;
  age: number;
}

declare function test(para: User): number;
declare function test(para: number, flag: boolean): number;

const user = {
  name: 'Jack',
  age: 666
};

// bingo
// Error: 引數不匹配
const res = test(user, false);

實際專案中,你可能要多寫幾步,如在 class 中:

interface User {
  name: string;
  age: number;
}

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {

  /**
   * 註釋 1
   */
  public test(para: User): number;
  /**
   * 註釋 2
   */
  public test(para: number, flag: boolean): number;
  public test(para: User | number, flag?: boolean): number {
    // 具體實現
    return 11;
  }
}

const someClass = new SomeClass();


// ok
someClass.test(user);
someClass.test(123, false);

// Error
someClass.test(123);
someClass.test(user, false);

官網:

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 {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    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)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

過載的pickCard函式在呼叫的時候會進行正確的型別檢查。

為了讓編譯器能夠選擇正確的檢查型別,它與JavaScript裡的處理流程相似。 它查詢過載列表,嘗試使用第一個過載定義。 如果匹配的話就使用這個。 因此,在定義過載的時候,一定要把最精確的定義放在最前面。

注意,function pickCard(x): any並不是過載列表的一部分,因此這裡只有兩個過載:一個是接收物件另一個接收數字。 以其它引數呼叫pickCard會產生錯誤。

過載的好處

過載其實是把多個功能相近的函式合併為一個函式,重複利用了函式名。 假如jQuery中的css( )方法不使用 過載,那麼就要有5個不同的函式,來完成功能,那我們就需要記住5個不同的函式名,和各個函式相對應的引數的個數和型別,顯然就麻煩多了。

一些不需要函式過載的場景(並不絕對,如上例子2)

函式過載的意義在於能夠讓你知道傳入不同的引數得到不同的結果,如果傳入的引數不同,但是得到的結果(型別)卻相同,那麼這裡就不要使用函式過載(沒有意義)。

如果函式的返回值型別相同,那麼就不需要使用函式過載

function func (a: number): number
function func (a: number, b: number): number

// 像這樣的是引數個數的區別,我們可以使用可選引數來代替函式過載的定義

function func (a: number, b?: number): number

// 注意第二個引數在型別前邊多了一個`?`

// 亦或是一些引數型別的區別導致的
function func (a: number): number
function func (a: string): number

// 這時我們應該使用聯合型別來代替函式過載
function func (a: number | string): number

總結

雖然 JavaScript 並沒有真正意義上的過載,但是過載的效果在JavaScript中卻非常常見,比如 陣列的splice( )方法,一個引數可以刪除,兩個引數可以刪除一部分,三個引數可以刪除完了,再新增新元素。
再比如parseInt( )方法,傳入一個引數,就判斷是用十六進位制解析,還是用十進位制解析,如果傳入兩個引數,就用第二個引數作為數字的基數,來進行解析。

文中提到的實現過載效果的方法,本質都是對引數進行判斷,不管是判斷引數個數,還是判斷引數型別,都是根據引數的不同,來決定執行什麼操作的。

雖然,過載能為我們帶來許多的便利,但是也不能濫用,不要把一些根本不相關的函式合為一個函式,那樣並沒有什麼意義。