JS / TS 中的重载 (Overload)中文

lang
中文
date
Dec 20, 2021
Property
slug
overload
status
Published
tags
JavaScript
summary
重载,就是函数或者方法有相同的名称,但是参数列表不相同的情形。一直觉得 JavaScript 是没有重载的,直到 TypeScript 的出现,所以我一直觉得 JavaScript 没有重载,TypeScript 才有,但是现在看来我是错的。
type
Post

什么是重载

重载,从简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者重载方法。
在 Java 中同一个类中的2个或2个以上的方法可以有同一个名字,只要它们的参数声明不同即可。这种情况下,该方法就被称为重载,这个过程称为方法重载。在 Java 中实现重载的例子如下:
public class OverloadDemo { // 1. test()方法第一次重载,没有参数 void test() { System.out.println("No parameters"); } // 2. test()方法第二次重载,一个整型参数 void test(int a) { System.out.println("a: " + a); } // 3. test()方法第三次重载,两个整型参数 void test(int a, int b) { System.out.println("a and b: " + a + " " + b); } // 4. test()方法第四次重载,一个双精度型参数 double test(double a) { System.out.println("double a: " + a); return a * a; // 返回a*a的值 } } public class Overload { public static void main(String args[]){ OverloadDemo ob = new OverloadDemo(); double result; ob.test(); // test() -> No parameters ob.test(10); // test(int a) -> a: 10 ob.test(10, 20); // test(int a,int b) -> a and b: 10 20 result = ob.test(123.23); // test(double a) -> double a: 123.23 System.out.println("result of ob.test(123.23): " + result); // result of ob.test(123.23): 15185.6329 } }

TypeScript 中的重载

JavaScript 函数可以接收多个不同类型的参数,如创建一个创建日期的函数,它可以接收时间戳作为参数,也可以接收年月日三个参数。
在 TypeScript 中可以通过创建 “overload signatures” 来标记函数重载,例子如下:
function makeDate(timestamp: number): Date; function makeDate(m: number, d: number, y: number): Date; function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { if (d !== undefined && y !== undefined) { return new Date(y, mOrTimestamp, d); } else { return new Date(mOrTimestamp); } } const d1 = makeDate(12345678); const d2 = makeDate(5, 5, 5); const d3 = makeDate(1, 3); // No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
其中我们创建了两个函数重载,一个接收一个时间戳参数,另一个接收年月日三个参数。同时创建了一个函数实现。注意,有重载标记的函数不能被直接调用,因此如果只传入两个参数则编译器会抛出错误。
可能你也发现了,TypeScript 中的重载与上面提到的重载不太一样,TS 的重载更像只是重载函数声明。
如果按照重载的概念,我们的代码应该写成这样:
function makeDate(timestamp: number): Date { return new Date(mOrTimestamp); } function makeDate(m: number, d: number, y: number): Date { return new Date(y, mOrTimestamp, d); }
但是这样第二次定义的函数就覆盖了第一个,失去了我们想要达到的效果。
其实 TypeScript 的『Overload』只是允许给这样的函数标注多个类型,所以 TS 中的 Function Overloads 并不是真正意义上的重载,具体可以见下面知乎的回答。
如何看待Typescript中的重载(Overload)?
几点。 第一,常见的静态类型语言中的overload是发生在编译时的,编译器可以清楚的将每一处同名函数调用对应到你写的不同的函数实现。也就是,同名只是一个(让程序员看到的)假象。如你写了fun(x),有两个实现fun(x: string)、fun(x: int),真正编译后的程序里实际会有两个函数,假设记做fun_string和fun_int,而每个fun(x)调用会被自动替换成fun_string(x)或fun_int(x)。有没有可能编译器无法确定替换成哪一个?当然有可能,这个时候编译器就报错了嘛,意思是你代码写错啦! 第二,JavaScript是动态类型,所以是没有上面这种意义上的overload的。但JS程序员可以在运行时判断类型,也就是 function fun(x) { if (typeof x === 'string') ... else/* assume x is int */ ... } 。TypeScript 的『overload』只是允许给这样的函数标注多个类型。某轮说这是『绕过编译器类型检查』,是有问题的。这不是绕过,把函数参数标记为 (x: any) 才叫『绕过』。不过因为函数的具体实现只有一个,代码本身会比上面那种overload要麻烦一些,比如说为了检测类型偶尔你需要自己实现一些 type guard。至于说『下标函数也不能自己写,这个很傻逼』,我估计某轮指的是 operator overload,然而很多语言都不允许(比如 java)。所以单单骂 TS/JS 有点扯。 第三,TS理论上当然是可以实现传统的 overload 的,比如直接生成两个函数,fun1、fun2。问题是从TS与JS的互操作性上来说,这事情就比较麻烦,比如一个js项目用了ts的库,我不能直接写fun,而得写fun1、fun2。本来 overload 就是希望给程序员提供便利,但现在就并没有什么卵用。其实像java之类有『真』重载的语言编译到js,或直接和js互操作,都有类似的问题。早在二十年前rhino里就有这问题--你在js里要指定到底调用的是哪一个java的重载方法是非常烦人的。特别是构造器,一般函数你说编译成fun_string、fun_int也就算了,但构造器呢?相当棘手。 那TS能不能自动生成一个把多个实现合并起来的fun呢? 不好办。因为runtime的类型检查和编译时类型检查是很不一样的(可以上网去查下override和overload的差异,前者通常就是runtime的),而且TS编译后并没有保留类型信息,所以复杂一点的类型根本没法在runtime检查。并且不带有runtime类型检查是TS的设计目标确定的,所谓by design是也。(其他答案也都提到了这一点。) 最后,这种只有『函数签名重载』而函数实现却还是只有一个,看上去只是因为TS要迁就JS而导致的限制。但从另外一方面说,『真』重载本身也并非只有优点没有缺点,如维基词条里写的: CaveatsIf a method is designed with an excessive number of overloads, it may be difficult for developers to discern which overload is being called simply by reading the code.
如何看待Typescript中的重载(Overload)?
那我们如何在 TS 中实现传统的 Overload 呢,请看下面在 JS 中实现重载的方法

在 JavaScript 中实现重载

我们都知道 JavaScript 是没有重载这个概念的,那如何实现理想的 重载 效果呢?最简单的办法就是写一个 fn 函数,并在这个函数中判断 arguments 类数组的长度,执行不同的代码,就可以完成 重载 的效果。
function fn() { switch (arguments.length) { case 1: var [name] = arguments console.log(`I'm ${name}`) break; case 2: var [name, age] = arguments console.log(`I'm ${name}, I'm ${age} years old`) break; case 3: var [name, age, sport] = arguments console.log(`I'm ${name}, I'm ${age} years old, I like ${sport}`) break; default: throw Error(`No overload expects ${arguments.length} arguments`) } } fn('John') // I'm John fn('John', 21) // I'm John,I'm 21 years old fn('John', 21, 'basketball') // I'm John,I'm 21 years old,I like basketball fn('John', 21, 'basketball', 'games') // No overload expects 4 arguments
或者还可以利用 闭包 来实现 重载 的效果。这个方法在 JQuery 之父 John Resig 写的《Secrets of the JavaScript ninja》中,这种方法充分的利用了 闭包 的特性!
function addMethod(object, name, fn) { var old = object[name]; //把前一次添加的方法存在一个临时变量old里面 object[name] = function () { // 如果调用object[name]方法时,传入的参数个数跟预期的一致,则直接调用 if (fn.length === arguments.length) { return fn.apply(this, arguments); // 否则,判断old是否是函数,如果是,就调用old } else if (typeof old === "function") { return old.apply(this, arguments); } else { throw Error(`No overload expects ${arguments.length} arguments`) } } } addMethod(this, 'fn', (name) => console.log(`I'm ${name}`)) addMethod(this, 'fn', (name, age) => console.log(`I'm ${name}, I'm ${age} years old`)) addMethod(this, 'fn', (name, age, sport) => console.log(`I'm ${name}, I'm ${age} years old, I like ${sport}`)) this.fn('John') // I'm John this.fn('John', 21) // I'm John,I'm 21 years old this.fn('John', 21, 'basketball') // I'm John,I'm 21 years old,I like basketball this.fn('John', 21, 'basketball', 'games') // No overload expects 4 arguments
需要注意的是,使用 fn.length 判断函数参数长度时,函数的参数不能有默认值,因为 fn.length 获取的是函数有几个不含默认值的参数数量,如:
// length = 1 (name, age = 20, sport) => console.log(`I'm ${name}, I'm ${age} years old, I like ${sport}`)) // length = 0 (name = "John", age, sport) => console.log(`I'm ${name}, I'm ${age} years old, I like ${sport}`))

© Matoz 2021 - 2024