网络知识 娱乐 JS中的重载——如何实现一个类似这样的功能,我也想玩玩

JS中的重载——如何实现一个类似这样的功能,我也想玩玩

函数重载

在其他语言中,我们一般都听说过重载的概念,对于一个方法,如果我们期望传入不同的参数类型,或者传入不同的参数个数,甚至传入参数的顺序不一样,而去执行不同的处理代码,以返回相应的结果,那么他们的方法名是可以相同的,而且它们不会产生冲突,这就是所谓的重载。

这是因为像java等这样的强类型语言,他们具有类型约束和方法签名,这样就使得他们能够根据调用时传入参数的情况来决定使用哪一个同名方法来处理需要的操作。

是在说我吗?

但是js属于弱类型语言,也就是说它不会规定参数必须传入哪种类型,定义的方法也不具有签名的功能,而且在定义多个同名方法时,类似于css里面的层叠样式表的效果,后定义的同名方法会覆盖前面的方法,还有一个就是函数具有提升的特性,这也就使得它无法实现重载的功能。

怎么办呢?

其实js中也确实不需要重载的功能,因为没有类型的约束,在一个方法里面就可以做很多自由的发挥,不但能满足需要重载的需求,而且还能玩一些花样。

不过话又说回来,没有了约束,就容易犯错,都用一个方法体来处理所有情况,就会容易出乱子,使得我们需要使用一堆的类似if-else的语句来做到这一点。

那么我们能不能在现有js运行方式的基础上,借鉴其他语言对于重载的运用,来绕个弯子变量来实现一下属于js自己的重载方式呢?

试一试

我们今天只讨论在js中变相实现重载的方式,而不讨论它的意义与实际应用场景,我们通过一个简单的例子,来支撑这个讨论,其他的交由你们来自由发挥。

让我们开始吧

js重载的方式

在js中主要有以下几种实现重载的方式:

  1. 使用剩余参数的形式来接收可变的参数,并通过一些判断手段来处理不同情况的逻辑。
  2. 使用arguments的形式,来动态判断需要执行的操作
  3. 使用proxy的形式来拦截函数的行为,以达到控制不同的逻辑分支。

前两种方式比较相似,思路一样,只是实现手段有所不同,用户需要自己写判断逻辑。第三种方式结合proxy来隐藏实现细节,让用户只关注各自的分工。但它们都是在参数动态判断方面做文章。

前两种方式的优缺点:

优点:可以直接定义函数或使用表达式

缺点:函数名不能相同,需要写较多的判断分支

第三种方式的优缺点:

优点:可以不用写各种参数形式的分支

缺点:只能使用表达式定义函数

由于前两种网上已经有很多的实现思路与方案,因此这里不再进行探讨,其中有很多奇妙的实现,可以做到在js中使用重载的思想。

所以在此我们只讨论第三种方案,我们下面来看一下它的思路是什么,是否满足重载的需求,它是如何实现的,以及它能满足我们什么样的需求?

这是什么呢?

需求假设

我们现在有这样一个场景和需求:

自己开了一家服装店,由于生意火爆,我们想要答谢新老顾客,现在推出了一个活动,全场无论任何服装,只要买一件就直接打95折,只要买两件就全部打9折,只要买三件就全部打85折,只要买四件及以上,就全部打8折。

如果用代码来实现,其实就是给方法中传入一个两个三个四个参数的问题,因此我们自然而然的就想到了使用重载来实现这个需求。

接下来我们就试着自己实现一个这样的功能,看看可不可以创建一个赋能方法来使某个业务处理函数具有重载的能力。

思路分析

要生成这样一个赋能方法,我们需要有对函数改造的能力,在创建业务处理函数的时候,最好能够改变它的默认行为,在执行的时候也能够对它进行拦截以做一些额外的操作。

那么我们很自然而然的就想到了使用Proxy,先生成一个Proxy函数,然后在给它设置属性的时候,我们进行拦截,把赋值操作中的value缓存起来,以备将来调用的时候做分支处理,根据参数的个数与类型来控制需要执行的业务逻辑代码。它真的能做到吗?我们来看一下下面的一步步代码实现。

实现需求

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

可以看到,我们实现了一个Overload函数,用来返回一个Proxy,通过它去load不同的方法来实现对同名方法的重载,调用的时候只需要一个方法名即可,Proxy中我们对set(即设置该Proxy的值的操作)和apply(即执行该Proxy的操作)两种操作进行了拦截,用到了一个叫做overloadCached的属性来缓存我们的处理函数,在调用函数的时候,我们使用从后往前遍历的方式,来达到后定义优先生效的原则。

通过打印结果我们知道,它已经满足了我们的需求假设。

默认处理

从上面的代码中我们发现,Overload函数可以传入一个叫做defaultCall的参数,它是用来处理默认操作的,也就是说如果后面定义的所有方法都不能够处理的时候,将使用该默认函数进行处理,如果没有定义该函数,那么调用sum时如果没有满足的执行函数,将会返回undefined。

现在我们给它传入一个默认的处理函数,那么上面的需求将可以写成这样:

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函数的变化,现在依然满足上面的需求。

处理兼容

由于我们把四个参数即以上的处理函数改为通过传入默认函数的方式来实现,因此我们修改了Overload方法,这显然是不合理的,因为这样不设置默认函数的时候会出问题,因此我们做一个兼容处理,修改之后就变成了这样:

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

现在使用这个Overload函数就已经能够处理上面的这两种情况了。我们设定了一个rest属性来给方法打上了一个标识。

需求延伸

如果我们现在在上面的需求基础上,又想要对金额做一些处理,比如希望能够加上$、¥、€等前缀,来区分不同的币种。

这个时候我们需要增加新的重载函数,而加了币种之后的函数可能与现有的函数参数个数相同(比如sum('$', 220, 240)和sum(270, 260, 310)),这就造成了误判,那么我们能不能再做一个类型区分呢?

应该是可以的,但是我们必须约定一种格式,比如下面这种形式,我们需要在获取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

我们现在已经加上了类型判断,通过传入的参数类型与个数的不同,能够相应的去执行对应的函数,其实参数的顺序一个道理,也是支持的。

类型扩展

上面的类型约定我们可能看起来怪怪的,而且比较难以理解,因此我们可以扩展一下类型约定的表示方式,改造后的Overload函数如下:

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

到此为止,我们已经能够支持参数的个数、类型、顺序的不同,会执行不同的处理函数,满足了重载的基本需求,完成了我们在最开始的需求假设的实现。

结语

目前这种方式只能支持函数表达式的方式来进行重载,这里只是给大家提供一个自定义实现重载的方式,结合自己的业务场景,小伙伴们可以自由发挥,其实目前js的既有方式能满足我们需要重载的场景,而不需要额外设计重载的代码。

具体这种方式的优劣,大家可以自行判断,并且可以根据这种思路重新设计一下实现的手段。

谢谢