函數(shù)重載
在其他語言中,我們一般都聽說過重載的概念,對于一個方法,如果我們期望傳入不同的參數(shù)類型,或者傳入不同的參數(shù)個數(shù),甚至傳入?yún)?shù)的順序不一樣,而去執(zhí)行不同的處理代碼,以返回相應的結果,那么他們的方法名是可以相同的,而且它們不會產(chǎn)生沖突,這就是所謂的重載。
這是因為像JAVA等這樣的強類型語言,他們具有類型約束和方法簽名,這樣就使得他們能夠根據(jù)調(diào)用時傳入?yún)?shù)的情況來決定使用哪一個同名方法來處理需要的操作。
是在說我嗎?
但是js屬于弱類型語言,也就是說它不會規(guī)定參數(shù)必須傳入哪種類型,定義的方法也不具有簽名的功能,而且在定義多個同名方法時,類似于css里面的層疊樣式表的效果,后定義的同名方法會覆蓋前面的方法,還有一個就是函數(shù)具有提升的特性,這也就使得它無法實現(xiàn)重載的功能。
怎么辦呢?
其實js中也確實不需要重載的功能,因為沒有類型的約束,在一個方法里面就可以做很多自由的發(fā)揮,不但能滿足需要重載的需求,而且還能玩一些花樣。
不過話又說回來,沒有了約束,就容易犯錯,都用一個方法體來處理所有情況,就會容易出亂子,使得我們需要使用一堆的類似if-else的語句來做到這一點。
那么我們能不能在現(xiàn)有js運行方式的基礎上,借鑒其他語言對于重載的運用,來繞個彎子變量來實現(xiàn)一下屬于js自己的重載方式呢?
試一試
我們今天只討論在js中變相實現(xiàn)重載的方式,而不討論它的意義與實際應用場景,我們通過一個簡單的例子,來支撐這個討論,其他的交由你們來自由發(fā)揮。
讓我們開始吧
js重載的方式
在js中主要有以下幾種實現(xiàn)重載的方式:
- 使用剩余參數(shù)的形式來接收可變的參數(shù),并通過一些判斷手段來處理不同情況的邏輯。
- 使用arguments的形式,來動態(tài)判斷需要執(zhí)行的操作
- 使用proxy的形式來攔截函數(shù)的行為,以達到控制不同的邏輯分支。
前兩種方式比較相似,思路一樣,只是實現(xiàn)手段有所不同,用戶需要自己寫判斷邏輯。第三種方式結合proxy來隱藏實現(xiàn)細節(jié),讓用戶只關注各自的分工。但它們都是在參數(shù)動態(tài)判斷方面做文章。
前兩種方式的優(yōu)缺點:
優(yōu)點:可以直接定義函數(shù)或使用表達式
缺點:函數(shù)名不能相同,需要寫較多的判斷分支
第三種方式的優(yōu)缺點:
優(yōu)點:可以不用寫各種參數(shù)形式的分支
缺點:只能使用表達式定義函數(shù)
由于前兩種網(wǎng)上已經(jīng)有很多的實現(xiàn)思路與方案,因此這里不再進行探討,其中有很多奇妙的實現(xiàn),可以做到在js中使用重載的思想。
所以在此我們只討論第三種方案,我們下面來看一下它的思路是什么,是否滿足重載的需求,它是如何實現(xiàn)的,以及它能滿足我們什么樣的需求?
這是什么呢?
需求假設
我們現(xiàn)在有這樣一個場景和需求:
自己開了一家服裝店,由于生意火爆,我們想要答謝新老顧客,現(xiàn)在推出了一個活動,全場無論任何服裝,只要買一件就直接打95折,只要買兩件就全部打9折,只要買三件就全部打85折,只要買四件及以上,就全部打8折。
如果用代碼來實現(xiàn),其實就是給方法中傳入一個兩個三個四個參數(shù)的問題,因此我們自然而然的就想到了使用重載來實現(xiàn)這個需求。
接下來我們就試著自己實現(xiàn)一個這樣的功能,看看可不可以創(chuàng)建一個賦能方法來使某個業(yè)務處理函數(shù)具有重載的能力。
思路分析
要生成這樣一個賦能方法,我們需要有對函數(shù)改造的能力,在創(chuàng)建業(yè)務處理函數(shù)的時候,最好能夠改變它的默認行為,在執(zhí)行的時候也能夠對它進行攔截以做一些額外的操作。
那么我們很自然而然的就想到了使用Proxy,先生成一個Proxy函數(shù),然后在給它設置屬性的時候,我們進行攔截,把賦值操作中的value緩存起來,以備將來調(diào)用的時候做分支處理,根據(jù)參數(shù)的個數(shù)與類型來控制需要執(zhí)行的業(yè)務邏輯代碼。它真的能做到嗎?我們來看一下下面的一步步代碼實現(xiàn)。
實現(xiàn)需求
function Overload(defaultCall) { let func = defaultCall || new Function() func.overloadCached = [] return new Proxy(func, { set(target, prop, value) { if(prop === 'load') { target.overloadCached.push(value) } }, Apply(target, thisArg, argumentsList) { for(let i = target.overloadCached.length - 1; i > -1; i--) { if(argumentsList.length === target.overloadCached[i].length || (argumentsList.length > target.overloadCached[i].length)) { return target.overloadCached[i].apply(thisArg, argumentsList) } } return target.apply(thisArg, argumentsList) } })}let sum = Overload()sum.load = function (a) { return a * 0.95;}sum.load = function (a, b) { return (a + b) * 0.9;}sum.load = function (a, b, c) { return (a + b + c) * 0.85;}sum.load = function (a, b, c, d, ...arg) { return (arg.concat(a,b,c,d).reduce((total, cur) => {return total + cur},0)) * 0.8;}console.log(sum(200));console.log(sum(200, 300));console.log(sum(180, 280, 190));console.log(sum(270, 260, 310, 240));console.log(sum(180, 220, 240, 210, 190));//輸出:190,450,552.5,864,832
可以看到,我們實現(xiàn)了一個Overload函數(shù),用來返回一個Proxy,通過它去load不同的方法來實現(xiàn)對同名方法的重載,調(diào)用的時候只需要一個方法名即可,Proxy中我們對set(即設置該Proxy的值的操作)和apply(即執(zhí)行該Proxy的操作)兩種操作進行了攔截,用到了一個叫做overloadCached的屬性來緩存我們的處理函數(shù),在調(diào)用函數(shù)的時候,我們使用從后往前遍歷的方式,來達到后定義優(yōu)先生效的原則。
通過打印結果我們知道,它已經(jīng)滿足了我們的需求假設。
默認處理
從上面的代碼中我們發(fā)現(xiàn),Overload函數(shù)可以傳入一個叫做defaultCall的參數(shù),它是用來處理默認操作的,也就是說如果后面定義的所有方法都不能夠處理的時候,將使用該默認函數(shù)進行處理,如果沒有定義該函數(shù),那么調(diào)用sum時如果沒有滿足的執(zhí)行函數(shù),將會返回undefined。
現(xiàn)在我們給它傳入一個默認的處理函數(shù),那么上面的需求將可以寫成這樣:
function Overload(defaultCall) { let func = defaultCall || new Function() func.overloadCached = [] return new Proxy(func, { set(target, prop, value) { if(prop === 'load') { target.overloadCached.push(value) } }, apply(target, thisArg, argumentsList) { for(let i = target.overloadCached.length - 1; i > -1; i--) { //注意這里的變化 if(argumentsList.length === target.overloadCached[i].length) { return target.overloadCached[i].apply(thisArg, argumentsList) } } return target.apply(thisArg, argumentsList) } })}let sum = Overload(function () { return ([].__proto__.reduce.call(arguments, (total, cur) => {return total + cur},0)) * 0.8;})sum.load = function (a) { return a * 0.95;}sum.load = function (a, b) { return (a + b) * 0.9;}sum.load = function (a, b, c) { return (a + b + c) * 0.85;}console.log(sum(200));console.log(sum(200, 300));console.log(sum(180, 280, 190));console.log(sum(270, 260, 310, 240));console.log(sum(180, 220, 240, 210, 190));//輸出:190,450,552.5,864,832
我們注意Overload函數(shù)的變化,現(xiàn)在依然滿足上面的需求。
處理兼容
由于我們把四個參數(shù)即以上的處理函數(shù)改為通過傳入默認函數(shù)的方式來實現(xiàn),因此我們修改了Overload方法,這顯然是不合理的,因為這樣不設置默認函數(shù)的時候會出問題,因此我們做一個兼容處理,修改之后就變成了這樣:
function Overload(defaultCall) { let func = defaultCall || new Function() func.overloadCached = [] return new Proxy(func, { set(target, prop, value) { if(prop === 'load') { let str = value.toString() let m1 = str.match(/(.+?)/) if(m1 && m1[0].indexOf("...") != -1) { value.rest = true } target.overloadCached.push(value) } }, apply(target, thisArg, argumentsList) { for(let i = target.overloadCached.length - 1; i > -1; i--) { if((argumentsList.length === target.overloadCached[i].length) || (target.overloadCached[i].rest && argumentsList.length > target.overloadCached[i].length)) { return target.overloadCached[i].apply(thisArg, argumentsList) } } return target.apply(thisArg, argumentsList) } })}//輸出:190,450,552.5,864,832
現(xiàn)在使用這個Overload函數(shù)就已經(jīng)能夠處理上面的這兩種情況了。我們設定了一個rest屬性來給方法打上了一個標識。
需求延伸
如果我們現(xiàn)在在上面的需求基礎上,又想要對金額做一些處理,比如希望能夠加上$、¥、€等前綴,來區(qū)分不同的幣種。
這個時候我們需要增加新的重載函數(shù),而加了幣種之后的函數(shù)可能與現(xiàn)有的函數(shù)參數(shù)個數(shù)相同(比如sum('$', 220, 240)和sum(270, 260, 310)),這就造成了誤判,那么我們能不能再做一個類型區(qū)分呢?
應該是可以的,但是我們必須約定一種格式,比如下面這種形式,我們需要在獲取Proxy屬性的時候(這里就用到了攔截獲取Proxy屬性的操作),將類型進行緩存,以便將來時候的時候來做類型的判斷:
//我們約定了10種類型//n→number//s→string//b→boolean//o→object//a→array//d→date//S→Symbol//r→regexp//B→bigint//f→functionfunction Overload(defaultCall) { let func = defaultCall || new Function() func.overloadCached = [] func.modifier = [] return new Proxy(func, { get(target, property, receiver) { if(property !== 'load') { target.modifier.push(property) } return receiver }, set(target, prop, value) { if(['n','s','b','o','a','d','S','r','B','f'].includes(prop)) { target.modifier.push(prop) } if(prop === 'load' || target.modifier.length !== 0) { let str = value.toString() let m1 = str.match(/(.+?)/) if(m1 && m1[0].indexOf("...") != -1) { value.rest = true } value.modifier = target.modifier target.overloadCached.push(value) target.modifier = [] } }, apply(target, thisArg, argumentsList) { for(let i = target.overloadCached.length - 1; i > -1; i--) { if((argumentsList.length === target.overloadCached[i].length) || (target.overloadCached[i].rest && argumentsList.length > target.overloadCached[i].length)) { if(target.overloadCached[i].modifier.length !== 0){ let ty = { '[object Number]': ['n'], '[object String]': ['s'], '[object Boolean]': ['b'], '[object Object]': ['o'], '[object Array]': ['a'], '[object Date]': ['d'], '[object Symbol]': ['S'], '[object Regexp]': ['r'], '[object BigInt]': ['B'], '[object Function]': ['f'], } if(target.overloadCached[i].modifier.some((m, j) => { return !ty[({}).__proto__.toString.call(argumentsList[j])].includes(m) })) { continue } } return target.overloadCached[i].apply(thisArg, argumentsList) } } return target.apply(thisArg, argumentsList) } })}let sum = Overload()sum.load.n = function (a) { return a * 0.95;}sum.load.n.n = function (a, b) { return (a + b) * 0.9;}sum.load.n.n.n = function (a, b, c) { return (a + b + c) * 0.85;}sum.load.s.n.n = function (a, b, c) { return a + (b + c) * 0.85;}sum.load.n.n.n.n = function (a, b, c, d, ...arg) { return (arg.concat(a,b,c,d).reduce((total, cur) => {return total + cur},0)) * 0.8;}sum.load.s.n.n.n = function (a, b, c, d, ...arg) { return a + (arg.concat(b,c,d).reduce((total, cur) => {return total + cur},0)) * 0.8;}console.log(sum(200));console.log(sum(200, 300));console.log(sum(260, 310, 240));console.log(sum('€', 280, 190));console.log(sum(180, 220, 240, 210, 190));console.log(sum('$', 220, 240, 210, 190));//輸出:190,450,688.5,€399.5,832,$688
我們現(xiàn)在已經(jīng)加上了類型判斷,通過傳入的參數(shù)類型與個數(shù)的不同,能夠相應的去執(zhí)行對應的函數(shù),其實參數(shù)的順序一個道理,也是支持的。
類型擴展
上面的類型約定我們可能看起來怪怪的,而且比較難以理解,因此我們可以擴展一下類型約定的表示方式,改造后的Overload函數(shù)如下:
function Overload(defaultCall) { let func = defaultCall || new Function() func.overloadCached = [] func.modifier = [] return new Proxy(func, { get(target, property, receiver) { if(property !== 'load') { if(property.indexOf(',') !== -1) { property.split(',').map(item => { target.modifier.push(item) }) }else{ property.split('').map(item => { target.modifier.push(item) }) } } return receiver }, set(target, prop, value) { let modi = null if(prop.indexOf(',') !== -1) { modi = prop.split(',') }else{ modi = prop.split('') } if(modi.every(p => { return ['n','s','b','o','a','d','S','r','B','f','number','string','boolean','object','array','date','Symbol','regexp','bigint','function'].includes(p) })) { modi.map(item => { target.modifier.push(item) }) } if(prop === 'load' || target.modifier.length !== 0) { let str = value.toString() let m1 = str.match(/(.+?)/) if(m1 && m1[0].indexOf("...") != -1) { value.rest = true } value.modifier = target.modifier target.overloadCached.push(value) target.modifier = [] } }, apply(target, thisArg, argumentsList) { for(let i = target.overloadCached.length - 1; i > -1; i--) { if((argumentsList.length === target.overloadCached[i].length) || (target.overloadCached[i].rest && argumentsList.length > target.overloadCached[i].length)) { if(target.overloadCached[i].modifier.length !== 0){ let ty = { '[object Number]': ['n','number'], '[object String]': ['s','string'], '[object Boolean]': ['b','boolean'], '[object Object]': ['o','object'], '[object Array]': ['a','array'], '[object Date]': ['d','date'], '[object Symbol]': ['S','Symbol'], '[object Regexp]': ['r','regexp'], '[object BigInt]': ['B','bigint'], '[object Function]': ['f','function'], } if(target.overloadCached[i].modifier.some((m, j) => { return !ty[({}).__proto__.toString.call(argumentsList[j])].includes(m) })) { continue } } return target.overloadCached[i].apply(thisArg, argumentsList) } } return target.apply(thisArg, argumentsList) } })}
這樣我們就可以支持一下幾種類型約定的書寫形式:
sum.load.s.n.n = function (a, b, c) { return a + (b + c) * 0.85;}sum.load['snn'] = function (a, b, c) { return a + (b + c) * 0.85;}sum.load.snn = function (a, b, c) { return a + (b + c) * 0.85;}//對于全稱不能夠寫成.(點)的形式sum.load['string,number,number'] = function (a, b, c) { return a + (b + c) * 0.85;}//這四種形式的任意一種對于console.log(sum('$', 280, 190));//都會輸出:$399.5
到此為止,我們已經(jīng)能夠支持參數(shù)的個數(shù)、類型、順序的不同,會執(zhí)行不同的處理函數(shù),滿足了重載的基本需求,完成了我們在最開始的需求假設的實現(xiàn)。
結語
目前這種方式只能支持函數(shù)表達式的方式來進行重載,這里只是給大家提供一個自定義實現(xiàn)重載的方式,結合自己的業(yè)務場景,小伙伴們可以自由發(fā)揮,其實目前js的既有方式能滿足我們需要重載的場景,而不需要額外設計重載的代碼。
具體這種方式的優(yōu)劣,大家可以自行判斷,并且可以根據(jù)這種思路重新設計一下實現(xiàn)的手段。