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( )方法,傳入一個引數,就判斷是用十六進位制解析,還是用十進位制解析,如果傳入兩個引數,就用第二個引數作為數字的基數,來進行解析。
文中提到的實現過載效果的方法,本質都是對引數進行判斷,不管是判斷引數個數,還是判斷引數型別,都是根據引數的不同,來決定執行什麼操作的。
雖然,過載能為我們帶來許多的便利,但是也不能濫用,不要把一些根本不相關的函式合為一個函式,那樣並沒有什麼意義。