您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > javascript

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

时间:2022-08-07 12:17:22  来源:  作者:lnsstyp

函数重载

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

这是因为像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的既有方式能满足我们需要重载的场景,而不需要额外设计重载的代码。

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



Tags:JS   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
又出新JS运行时了!JS运行时大盘点
Node.js是基于Google V8引擎的JavaScript运行时,以非阻塞I/O和事件驱动架构为特色,实现全栈开发。它跨平台且拥有丰富的生态系统,但也面临安全性、TypeScript支持和性能等挑战...【详细内容】
2024-03-21  Search: JS  点击:(29)  评论:(0)  加入收藏
GitHub顶流"Web OS"——运行于浏览器的桌面操作系统、用户超100万、原生jQuery和JS编写
Puter 是近日在 GitHub 上最受欢迎的一款开源项目,正式开源还没到一周 ——star 数就已接近 7k。作者表示这个项目已开发 3 年,并获得了超过 100 万用户。根据介绍,P...【详细内容】
2024-03-10  Search: JS  点击:(32)  评论:(0)  加入收藏
前端开始“锈化”?Vue团队开源JS打包工具:基于Rust、速度极快、尤雨溪主导
Vue 团队已正式开源Rolldown —— 基于 Rust 的 JavaScrip 打包工具。Rolldown 是使用 Rust 开发的 Rollup 替代品,它提供与 Rollup 兼容的应用程序接口和插件接口...【详细内容】
2024-03-09  Search: JS  点击:(23)  评论:(0)  加入收藏
如何在Rust中操作JSON,你学会了吗?
sonic-rs ​还具有一些额外的方法来进行惰性评估和提高速度。例如,如果我们想要一个 JSON​ 字符串文字,我们可以在反序列化时使用 LazyValue​ 类型将其转换为一个仍然带有斜...【详细内容】
2024-02-27  Search: JS  点击:(51)  评论:(0)  加入收藏
JS小知识,使用这6个小技巧,避免过多的使用 if 语句
最近在重构我的代码时,我注意到早期的代码使用了太多的 if 语句,达到了我以前从未见过的程度。这就是为什么我认为分享这些可以帮助我们避免使用过多 if 语句的简单技巧很重要...【详细内容】
2024-01-30  Search: JS  点击:(64)  评论:(0)  加入收藏
花 15 分钟把 Express.js 搞明白,全栈没有那么难
Express 是老牌的 Node.js 框架,以简单和轻量著称,几行代码就可以启动一个 HTTP 服务器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都与 Express 息息相关。Express 框...【详细内容】
2024-01-16  Search: JS  点击:(91)  评论:(0)  加入收藏
JS 中如何克隆对象?你学会了吗?
大家好,这里是大家的林语冰。JS 中如何克隆对象?此问题看似简单,实际十分复杂。假设我们需要获取下述对象的拷贝。const cat = { name: '薛定谔', girlFriends: { na...【详细内容】
2024-01-05  Search: JS  点击:(123)  评论:(0)  加入收藏
理解 Node.js 中的事件循环
你已经使用 Node.js 一段时间了,构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但是有些事情一直在困扰着你——事件循环(Event Loop)。如果你像我...【详细内容】
2024-01-05  Search: JS  点击:(124)  评论:(0)  加入收藏
彻底搞懂 JS 类型转换
1. 什么是类型转换?Javascript 是一种弱类型语言,这意味着变量是没有明确类型的,而是由 JavaScript 引擎在编译时隐式完成。类型转换就是将一种数据类型转换为另一种数据类型,例...【详细内容】
2024-01-03  Search: JS  点击:(116)  评论:(0)  加入收藏
.NET配置文件大揭秘:轻松读取JSON、XML、INI和环境变量
概述:.NET中的IConfiguration接口提供了一种多源读取配置信息的灵活机制,包括JSON、XML、INI文件和环境变量。通过示例,清晰演示了从这些不同源中读取配置的方法,使配置获取变得...【详细内容】
2023-12-28  Search: JS  点击:(94)  评论:(0)  加入收藏
▌简易百科推荐
JavaScript的异步编程常见模式
在JavaScript中,异步编程是一种处理长时间运行操作(如网络请求或I/O操作)的常见方式。它允许程序在等待这些操作完成时继续执行其他任务,从而提高应用程序的响应性和性能。JavaS...【详细内容】
2024-04-12  靳国梁    Tags:JavaScript   点击:(11)  评论:(0)  加入收藏
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  前端新世界  微信公众号  Tags:JavaScript   点击:(7)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践——如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  前端新世界  微信公众号  Tags:JavaScript   点击:(29)  评论:(0)  加入收藏
又出新JS运行时了!JS运行时大盘点
Node.js是基于Google V8引擎的JavaScript运行时,以非阻塞I/O和事件驱动架构为特色,实现全栈开发。它跨平台且拥有丰富的生态系统,但也面临安全性、TypeScript支持和性能等挑战...【详细内容】
2024-03-21  前端充电宝  微信公众号  Tags:JS   点击:(29)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  前端历险记  微信公众号  Tags:JavaScript   点击:(23)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  WangLiwen    Tags:JavaScript   点击:(13)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  京东云开发者    Tags:JavaScript   点击:(13)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06    51CTO  Tags:JavaScript   点击:(60)  评论:(0)  加入收藏
JS小知识,使用这6个小技巧,避免过多的使用 if 语句
最近在重构我的代码时,我注意到早期的代码使用了太多的 if 语句,达到了我以前从未见过的程度。这就是为什么我认为分享这些可以帮助我们避免使用过多 if 语句的简单技巧很重要...【详细内容】
2024-01-30  前端达人  今日头条  Tags:JS   点击:(64)  评论:(0)  加入收藏
18个JavaScript技巧:编写简洁高效的代码
本文翻译自 18 JavaScript Tips : You Should Know for Clean and Efficient Code,作者:Shefali, 略有删改。在这篇文章中,我将分享18个JavaScript技巧,以及一些你应该知道的示例...【详细内容】
2024-01-30  南城大前端  微信公众号  Tags:JavaScript   点击:(80)  评论:(0)  加入收藏
站内最新
站内热门
站内头条