【JS 深入】Javascript 中函数重载的必要性讨论

有过 C、C++、Java 等编程语言经验的童鞋对“函数重载”一定不陌生,它是指“在同一个作用域下、函数名称相同但参数不同的一组函数”。但在 Javascript 中,却没有函数重载,这是为什么呢?今天我们就讨论讨论 Javascript 中函数重载是否有必要实现。

 函数重载

定义就是“在同一个作用域下、函数名称相同但参数不同的一组函数”,在 C、C++、Java 等编程语言中应用十分广泛。这是因为这些语言是强变量类型语言,一个变量一旦声明了类型,后期是不能修改的,正是因为如此,所以才出现了“函数重载”,它可以让同一个方法处理不同的变量类型,实现了让一个单独函数可以完成多项任务的能力。

而在 Javascript 中,是没有“函数重载”这一个概念的,比如下面的例子,后面声明的函数会覆盖前面的,同一作用域中始终只有一个同名函数:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>JS 模拟函数重载</title>
</head>
<body>
	<script>
		function add(a){
			alert(a + 10);
		}

		function add(b){
			alert(b + 1);
		}

		add(1);
	</script>
</body>
</html>

在 Javascript 中,变量是弱类型的,你在声明变量的时候无需声明它是什么类型,同一个变量,我可以存储 String,也可以存储 Number。正是因为弱变量类型这一特性,所以对一个函数,你可以传入任何类型参数;同时一个函数也不需要强调其参数类型,甚至可以不用显示声明参数变量,也可以做到一个函数处理多种变量类型。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>JS 模拟函数重载</title>
</head>
<body>
	<script>

		// 同一函数处理不同类型参数
		function add(a){
			if (typeof a !== "number") {
				return alert("非 Number 类型参数");
			}

			return alert(a + 10);
		}

		add(1); // 11
		add("1"); // 非 Number 类型参数


		// 隐藏函数参数
		function add2(){
			var a = arguments[0];
			if (typeof a !== "number") {
				return alert("非 Number 类型参数");
			}

			return alert(a + 10);
		}

		add(1); // 11
		add("1"); // 非 Number 类型参数
	</script>
</body>
</html>

JS 函数重载有其必要性吗?

因为 Javascript 的弱变量类型特性,我认为在 Javascript 中完全没有必要实现函数重载,因为你的函数能接收任何类型参数的调用!

因为在 Java 中有函数重载是因为 Java 函数的参数类型是确定的,传入错误的变量类型会报错;但在 Javascript 中,函数的参数类型是未知的,只有在被调用的时候才能知道传入的参数是什么类型的。

正是因为这样, Javascript 中完全没有必要实现函数重载!

虽然 Javascript 中没有必要实现函数重载,但是每次写函数的时候我们都要时刻小心处理变量类型,如果你没有处理,可能会因为 Javascript 中处处存在的隐式转换而带来出乎意料的结果。

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>JS 模拟函数重载</title>
</head>
<body>
	<script>

		// 不进行参数类型检查,直接运算
		function add(a){
			return alert(a + 10);
		}

		add(1); // 11

		// 因为隐式转换,"1" + 10 变成了 "1" + "10"
		add("1"); // "110"
	</script>
</body>
</html>

因为没有检查变量类型,当你传入一个字符串“1”的时候,结果就不是预期的 11 了,而是变成了 “110”。想要正确的输出结果,你就需要进行类型判断:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS 模拟函数重载</title>
</head>
<body>
    <script>

        // 进行参数类型判断并做相应的转换
        function add(a){
            if (typeof a === "string") {
                a = Number(a);
            }
            if (typeof a === "object") {
                // 一元操作符 + 可以调用对象的 valueOf 方法
                a = +a;
            }
            return alert(a + 10);
        }

        add(1); // 11

        add("1"); // 11

        add({
            valueOf: function(){
                return 1;
            }
        }) // 11
    </script>
</body>
</html>

但是,你可能发现了问题,如果我要再增加一个类型的判断,就需要更改原函数,可能会影响其他类型的判断,又或者我们就需要这个函数对每一个不同类型的参数进行完全独立的处理,上面的写法通过改写虽然也可以满足需求,但是就语法上已经不直观了。这时我们就需要将“参数类型判断”这一层抽象为一个独立的函数,可以更加直观的实现我们的需求。

Javascript 模拟“函数重载”(大雾)

我们想要实现的效果类似于

var printf = overload({
	"string": function(){
		alert("参数类型为 'string'");
	},
	"array": function(){
		alert("参数类型为 'array'");
	},
	"string,array,object": function(){
		alert("参数类型为 'string,array,object'");
	},
})

printf("a");
printf([1, 2, 4]);
printf("a", [1, 2, 4], {"a": "A"});

下面就是实现代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS 模拟函数重载</title>
</head>
<body>
    <script>
        /*
        Javascript 模拟函数重载
        obj 为对象类型,其中对象的属性为参数类型,属性值为对应的处理函数
        */
        function overload(obj){

            // 模式匹配函数
            var isMatch = function(t, c){
                if (t === c) return true;

                if (c.indexOf("*") === -1) return false;

                var t_list = t.split(","),
                    c_list = c.split(",");

                if (c_list.length === 0) return false;

                for(var i = 0; i < c_list.length; i++){
                    if (c_list[i] !== t_list[i] && c_list[i] !== "*") return false;
                }

                return true;
            }


            var types = Object.keys(obj);

            return function(){
                var args = [].slice.call(arguments),
                args_type = args.map(function(item){
                    if (typeof item === "object") {
                        // 对 Array 类型额外判断
                        if (Object.prototype.toString.call(item) === "[object Array]") {
                            return "array";
                        } else {
                            return "object";
                        }
                    }
                    return typeof item;
                }).join(",");

                for(var i = 0; i < types.length; i ++){
                    if (isMatch(args_type, types[i])) {
                        obj[types[i]].apply(this, arguments);
                    }
                }
            }
        }


        var printf = overload({
            "string": function(){
                alert("参数类型为 'string'");
            },
            "array": function(){
                alert("参数类型为 'array'");
            },
            "string,array,object": function(){
                alert("参数类型为 'string,array,object'");
            },
        })

        printf("a");
        printf([1, 2, 4]);
        printf("a", [1, 2, 4], {"a": "A"});
        printf("a", "a", "a");
    </script>
</body>
</html>

通过上面的代码,我们就实现了对不同的参数类型做不同的处理的抽象化函数。