详解如何通过JavaScript实现函数重载

说明

JavaScript 中没有真正意义上的函数重载。

函数重载

函数名相同,函数的参数列表不同(包括参数个数和参数类型),根据参数的不同去执行不同的操作。

 

有的同学在开发中可能遇到过一个困扰,但是很少有人去解决这个问题,我这用一个例子展现出来

复制代码
const searcher = {};
searcher.findAll = () => {
  console.log("查询所有用户");
};
searcher.findByName = (name) => {
  console.log("按照用户名称查询");
};
searcher.findByFirstNameAndLastName = (firstName, lastName) => {
  console.log("按照姓和名用户查询");
};
复制代码

可以看到上方的searcher对象有三个方法,但是他的查询逻辑是不一样。 findAll查询所有用户;findByName按照用户名查;findByFirstNameAndLastName是按照姓和名查询用户,结构上没任何问题,那问题在哪?,恶心因为都是在做查询,但又不得不给每一个函数取不同的名字,因为一旦重名就覆盖了,如果说他们能取一样的名字那该有多好

复制代码
searcher.find = () => {
  console.log("查询所有用户");
};
searcher.find = (name) => {
  console.log("按照用户名称查询");
};
searcher.find = (firstName, lastName) => {
  console.log("按照姓和名用户查询");
};
复制代码

不传参数就是查所有用户

searcher.find()

给一个参数就是按照用户名来查询

searcher.find("aa")

给两个参数就是按照用户的姓跟名来查询

searcher.find("aa", "bb")

到用得时候也是非常的简单,也符合逻逻辑,这个就叫做函数重载

函数重载:就是给函数取同一样的一个函数名,根据你传递不同的参数数量,分别去调用不同的函数

好处:就在于使用不同的逻辑产生的函数,给他们取一个相同的名字,这样在使用上我只需要记住这个一样的名字就可以了,而不用去记这三个不同的名字,他有效的降低调用函数时产生的心智负担。

 但是JS不支持函数重载,所以说如果你写成已上的形式,无论你怎么调用,一定是调用最后一个函数,因为最后一个函数把前面的全部覆盖了
不传参数试一下,你看永远都是走最后一个函数的log。

    searcher.find() // 按照姓和名用户查询

那有什么办法呢?在很早的时候jQuery之父John Resig 他就已经提出了一个想法,我们能不能实现一个方法来帮助我们在js中完成函数重载?

于是他想出了一个addMethod方法,就是当我要重载的时,我不直接去定义函数,因为直接定义函数就会出现一个永远只调用最后一个函数的问题,而调用addMethod来定义函数,把对象传进去,把函数的名字传进去,把函数的实现传进去,后面依次同理。

复制代码
addMethod(searcher, 'find', () => {
    console.log('查询所有用户')
})
addMethod(searcher, 'find', (name) => {
    console.log('按照用户名称查询')
})
addMethod(searcher, 'find', (firstName, lastName) => {
    console.log('按照姓和名用户查询')
})
复制代码

写成这种格式之后,后面调用searcher.find()就能够完成函数重载,问题是这个方法我们得要自己实现,那么如何来实现? 这个方法他接受三个参数,如果你不加处理的话你可能写成下面函数一样,给对象加一个属性,这个属性等于一个函数,调用这个函数实际上就是在运行定义的实现函数fn,把this绑定带过去 然后把传递过来的arguments带过去。p.s arguments 对象,是函数内部的一个类数组对象,它里面保存着调用函数时,传递给函数的所有参数

function addMethod(object, name, fn) {
  object[name] = function () {
      fn.apply(this, arguments)
  }
}

仅仅这么些还是搞不定,因为每次调用addMethod最后一次调用一定会把object里面同一个属性给覆盖掉,那么如何处理?看下方代码

复制代码

//第一个参数是要绑定函数的对象,第二个函数是函数的名字,第三个是具体函数
function addMethod(Object, name, fn) { // 具体实现
  // 因为方法都是重名的,所以先取出原本的方法
  const old = Object[name];
  // ...args是形参表示接受所有参数
  Object[name] = function (...args) {
    // 判断形参个数,如果和传入换上的形参个数一样就执行传入的这个函数
    if (args.length === fn.length){

      return fn.apply(this, args);

    }

    else if (typeof old === 'function'){

      return old.apply(this, args);

    }
  }
}

复制代码

首先我不传参数走查询所有用户,传一个参数走按照用户名查询,传两个参数按照姓和名查询

完整的示例:

复制代码
const searcher = {};
//第一个参数是要绑定函数的对象,第二个函数是函数的名字,第三个是具体函数
function addMethod(Object, name, fn) { // 具体实现
  // 因为方法都是重名的,所以先取出原本的方法
  const old = Object[name];
  // ...args是形参表示接受所有参数
  Object[name] = function (...args) {
    // 判断形参个数,如果和传入换上的形参个数一样就执行传入的这个函数
    if (args.length === fn.length){

      return fn.apply(this, args);

    }else if (typeof old === 'function'){

      return old.apply(this, args);

    }
  }
}

function find0() {
    console.log('查询所有用户')
}
function find1(name) {
    console.log('按照用户名称查询')
}
function find2(firstName, lastName) {
    console.log('按照姓和名用户查询')
}
// 依次将函数注册
addMethod(searcher,'find',find0)
addMethod(searcher,'find',find1)
addMethod(searcher,'find',find2)

searcher.find() // 查询所有用户
searcher.find(1) // 按照用户名称查询
searcher.find(1,2) // 按照姓和名用户查询
复制代码

每调用一次 addMethod 函数,就会产生一个 old,形成一个闭包。 我们可以通过

console.dir(searcher.find) 把 find 方法打印到控制台看看。

 

 个人理解:

 第一次 使用addMethod函数注册函数时 old =object[name] 为undefined

然后给object[name]赋值一个匿名函数(在函数内部声明的函数即闭包)

(该闭包持有所需的执行环境 包含old=undefined,也包含该次注册的函数 find0


 第二次 使用addMethod函数注册函数时 old=object[name] 为第一次使用addMethod函数内的闭包 (修正理解: 注意这里old持有的匿名函数引用会被下面赋值的匿名函数给捕获到,赋值完成之后,object[name] 的值已经被覆盖更新了

然后给 object[name] 赋值一个匿名函数(由于name一样,所以进行重新赋值)
(该闭包持有所需的执行环境 包含old= object[name] 为第一次使用addMethod函数内的闭包,也包含该次注册的函数 find1


 p.s 在这里也许有人会有疑问?为什么 object[name] 都重新赋值了 old的值不应该也跟着改变吗?

old 保存了 上一个闭包的引用地址,object[name] 重新指向了一个新的引用地址。


 第三次 使用addMethod函数注册函数时 old=object[name] 为第二次使用addMethod函数内的闭包 (修正理解: 注意这里old持有的匿名函数引用会被下面赋值的匿名函数给捕获到,赋值完成之后,object[name] 的值已经被覆盖更新了

然后给 object[name] 赋值一个匿名函数(由于name一样,所以进行重新赋值)
(该闭包持有所需的执行环境 包含old=object[name] 为第二次使用addMethod函数内的闭包,也包含该次注册的函数 find2


此时再去调用 object[name]() 在本文中是searcher.find() 由于在addMethod函数中
 匿名函数返回fn.apply(this,args),find函数持有匿名函数的词法作用域(即当前代码定义位置所在的作用域),匿名函数又捕获了自身作用域以及上层作用域等相关作用域。

,这一系列环环相扣的持有,形成了作用域链。在调用searcher.find()时,由于find2是最后注册即最后入栈,秉承后进先出,根据object[name]所赋值的匿名函数逻辑找到持有上一个匿名函数old,沿着作用域链,直到找到find0函数。

 

AI修正理解

在这个例子中,const old = Object[name]; 这行代码的目的是捕获当前要覆盖方法的状态。但是,关键在于 Object[name] = function(...args) {...}; 这行代码是如何执行的。

每次 addMethod 被调用时,它实际上并没有直接修改原始对象 Object 上的 name 属性,而是创建了一个新的函数,并把这个新函数赋值给了 Object[name]。这个新函数会覆盖掉之前 Object[name] 上的值。这里的 old 变量是在赋值之前捕获的,所以它保存了覆盖前的函数引用。

这里是详细的步骤:

  1. 调用 addMethod(searcher, 'find', find0); 时,old 被设置为 undefined,因为此时 searcher.find 尚未定义。然后 searcher.find 被设置为 find0 函数。

  2. 接下来调用 addMethod(searcher, 'find', find1); 时,old 现在引用的是 find0 函数,因为这是 searcher.find 当前的值。然后,searcher.find 被设置为一个新的函数,这个新函数内部有逻辑来决定是调用 find1 还是 find0

  3. 最后调用 addMethod(searcher, 'find', find2); 时,old 引用的是上一步创建的新函数(它能够调用 find1find0)。然后,searcher.find 再次被设置为一个新的函数,这个新函数内部有逻辑来决定是调用 find2find1 还是 find0

因此,每次调用 addMethod 时,你不是直接修改原始的 find0find1find2 函数,而是在 searcher 对象上创建一个新的函数,这个新函数内部根据参数长度来决定调用哪个旧的函数。

这就是为什么 old 变量能够引用上一个注册的函数,即使 Object[name] 被新的函数覆盖了。每次 addMethod 调用时捕获的 old 都是那一刻 Object[name] 的值,这个值在新函数创建过程中被保留。


 p.s 在js中作用域链的顶层是window,原型链的顶层是object。
在函数中寻找一个属性,会先在该作用域链上查找,没有再去原型链上找。
调用searcher.find()就是在find函数作用域上寻找find无参函数,由于最后注册的是find2所以先执行addMethod中的闭包逻辑,形参个数不同,则执行old(即find2)所持有的上一个闭包逻辑。此时来到了find1所在的闭包,还是
形参个数不同,则执行old(即find1),形参个数相同,执行find0()。

 

总结

其实就是用到了闭包,将闭包形成了闭包链把我们传递的方法依次注册。在调用的时候从最后一个有序的去找,用这种非常巧妙的方式,用极少的代码就实现了一个函数重载的效果,函数重载虽然说你不一定能用到,但是通过去解析这个addMethod,我们可以感受到有这么智慧的实现方案,肯定对你将来实现某一些东西的时候,有所启发。

posted @   当下是吾  阅读(810)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示