TypeScript學習(一)函式過載
函式過載
這個概念是在一些強型別語言中才有的,在JS中依據不同引數型別或引數個數執行一些不同函式體的實現很常見,依託於TypeScript,就會有需要用到這種宣告的地方。
函式過載定義:函式名相同,函式的引數列表不同(包括引數個數和引數型別),根據引數的不同去執行不同的操作。
關於函式過載,必須要把精確的定義放在前面,最後函式實現時,需要使用|
操作符或者?
操作符,把所有可能的輸入型別全部包含進去
js函式過載
JavaScript 中沒有真正意義上的函式過載。
我們舉個例子看看
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函式過載
例子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);
過載的好處
過載其實是把多個功能相近的函式合併為一個函式,重複利用了函式名。 假如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( )方法,傳入一個引數,就判斷是用十六進位制解析,還是用十進位制解析,如果傳入兩個引數,就用第二個引數作為數字的基數,來進行解析。
文中提到的實現過載效果的方法,本質都是對引數進行判斷,不管是判斷引數個數,還是判斷引數型別,都是根據引數的不同,來決定執行什麼操作的。
雖然,過載能為我們帶來許多的便利,但是也不能濫用,不要把一些根本不相關的函式合為一個函式,那樣並沒有什麼意義。