MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript函数实参与形参的代码优化

2023-07-307.6k 阅读

理解 JavaScript 函数的形参与实参

在 JavaScript 编程中,函数是至关重要的组成部分。函数允许我们封装可重用的代码块,提高代码的模块化和可维护性。而函数的形参(Formal Parameters)和实参(Actual Parameters)则是函数与调用者之间传递数据的关键机制。

形参的基础概念

形参是在函数定义时声明的变量,它们作为占位符,代表在函数调用时将传入的值。例如,考虑以下简单的函数定义:

function addNumbers(a, b) {
    return a + b;
}

在这个 addNumbers 函数中,ab 就是形参。它们是函数内部用于操作数据的变量,但在函数调用之前,它们并没有实际的值。

实参的基础概念

实参是在函数调用时实际传递给函数的值。继续以上面的 addNumbers 函数为例,当我们调用这个函数时:

var result = addNumbers(3, 5);
console.log(result); // 输出 8

这里的 35 就是实参。它们被传递给 addNumbers 函数,并分别赋值给形参 ab

形参与实参的匹配规则

精确匹配

当函数调用时传递的实参数量与函数定义时的形参数量相等时,实参将按照顺序精确匹配形参。例如:

function greet(name, message) {
    console.log(`Hello, ${name}! ${message}`);
}
greet('John', 'How are you?');

在这个例子中,'John' 作为第一个实参匹配第一个形参 name'How are you?' 作为第二个实参匹配第二个形参 message

实参多于形参

如果函数调用时传递的实参数量多于形参数量,多余的实参将被忽略。例如:

function multiply(a, b) {
    return a * b;
}
var result = multiply(2, 3, 4);
console.log(result); // 输出 6

这里虽然传递了三个实参 234,但函数只定义了两个形参 ab,所以第三个实参 4 被忽略,函数仅对前两个实参进行乘法运算。

实参少于形参

当传递的实参数量少于形参数量时,缺少的形参将被赋值为 undefined。例如:

function divide(a, b) {
    if (b === undefined) {
        return 'Division by zero or missing divisor';
    }
    return a / b;
}
var result1 = divide(10);
console.log(result1); // 输出 'Division by zero or missing divisor'
var result2 = divide(10, 2);
console.log(result2); // 输出 5

在第一次调用 divide(10) 时,由于只传递了一个实参,形参 b 被赋值为 undefined,函数通过检查 b 是否为 undefined 来处理这种情况。而在第二次调用 divide(10, 2) 时,实参数量与形参数量匹配,函数正常执行除法运算。

优化函数形参与实参的传递

使用默认参数值

在 ES6 之前,处理形参缺少值的情况通常需要在函数内部手动进行判断和赋值。例如:

function greet(name, message) {
    if (name === undefined) {
        name = 'Guest';
    }
    if (message === undefined) {
        message = 'Welcome!';
    }
    console.log(`Hello, ${name}! ${message}`);
}
greet(); // 输出 'Hello, Guest! Welcome!'

ES6 引入了默认参数值的特性,使代码更加简洁:

function greet(name = 'Guest', message = 'Welcome!') {
    console.log(`Hello, ${name}! ${message}`);
}
greet(); // 输出 'Hello, Guest! Welcome!'

通过在形参定义时直接指定默认值,我们可以避免在函数内部进行繁琐的检查和赋值操作,使代码更加清晰易读。

剩余参数

在某些情况下,我们可能需要函数接受任意数量的参数。在 ES6 之前,我们通常使用 arguments 对象来实现这一点。例如:

function sum() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}
var result = sum(1, 2, 3, 4, 5);
console.log(result); // 输出 15

然而,arguments 对象并不是一个真正的数组,它缺少数组的一些方法,如 mapfilter 等。ES6 引入了剩余参数(Rest Parameters)来解决这个问题。剩余参数允许我们将函数的多个实参收集到一个真正的数组中。例如:

function sum(...numbers) {
    return numbers.reduce((total, number) => total + number, 0);
}
var result = sum(1, 2, 3, 4, 5);
console.log(result); // 输出 15

在这个例子中,...numbers 就是剩余参数,它将所有传递给 sum 函数的实参收集到一个数组 numbers 中。我们可以使用数组的方法,如 reduce 来对这些参数进行操作,使代码更加简洁和强大。

解构赋值

解构赋值是 ES6 中一个非常强大的特性,它可以用于函数的形参和实参传递,使代码更加清晰和易于维护。

数组解构

考虑一个函数,它接受一个包含两个元素的数组,并返回这两个元素的乘积:

function multiplyArray(arr) {
    return arr[0] * arr[1];
}
var result = multiplyArray([3, 5]);
console.log(result); // 输出 15

使用数组解构,我们可以使函数的形参更加直观:

function multiply([a, b]) {
    return a * b;
}
var result = multiply([3, 5]);
console.log(result); // 输出 15

这里,[a, b] 是一个数组解构模式,它将传递给 multiply 函数的数组的第一个元素赋值给 a,第二个元素赋值给 b

对象解构

对象解构在处理具有多个属性的对象作为参数时非常有用。例如,假设我们有一个函数,用于格式化用户信息:

function formatUser(user) {
    return `Name: ${user.name}, Age: ${user.age}`;
}
var user = { name: 'John', age: 30 };
var result = formatUser(user);
console.log(result); // 输出 'Name: John, Age: 30'

使用对象解构,我们可以使代码更加简洁:

function formatUser({ name, age }) {
    return `Name: ${name}, Age: ${age}`;
}
var user = { name: 'John', age: 30 };
var result = formatUser(user);
console.log(result); // 输出 'Name: John, Age: 30'

在这个例子中,{ name, age } 是一个对象解构模式,它从传递给 formatUser 函数的对象中提取 nameage 属性,并将它们分别赋值给同名的变量。我们还可以为对象解构的属性指定默认值,例如:

function formatUser({ name, age = 'Unknown' }) {
    return `Name: ${name}, Age: ${age}`;
}
var user1 = { name: 'John' };
var result1 = formatUser(user1);
console.log(result1); // 输出 'Name: John, Age: Unknown'
var user2 = { name: 'Jane', age: 25 };
var result2 = formatUser(user2);
console.log(result2); // 输出 'Name: Jane, Age: 25'

这里,当 age 属性不存在于传递的对象中时,将使用默认值 'Unknown'

形参与实参传递中的性能考量

基本类型与引用类型的传递

在 JavaScript 中,基本类型(如 numberstringboolean 等)是按值传递的,而引用类型(如 objectarrayfunction 等)是按引用传递的。这意味着在函数调用时,基本类型的实参传递的是值的副本,而引用类型传递的是对象或数组的引用。

基本类型按值传递

function increment(num) {
    num++;
    return num;
}
var a = 5;
var result = increment(a);
console.log(a); // 输出 5
console.log(result); // 输出 6

在这个例子中,a 是一个基本类型 number,当它作为实参传递给 increment 函数时,传递的是 a 的值的副本。所以在函数内部对 num 的修改不会影响到外部的 a

引用类型按引用传递

function addProperty(obj) {
    obj.newProperty = 'Added';
    return obj;
}
var myObj = { name: 'Original' };
var resultObj = addProperty(myObj);
console.log(myObj.newProperty); // 输出 'Added'
console.log(resultObj.newProperty); // 输出 'Added'

这里,myObj 是一个对象,属于引用类型。当它作为实参传递给 addProperty 函数时,传递的是 myObj 的引用。所以在函数内部对 obj 的修改会直接影响到外部的 myObj

了解这种传递方式对于性能优化很重要。例如,如果我们需要在函数内部修改一个大型数组或对象,但又不希望影响外部的原始数据,可以在函数内部创建一个副本。对于数组,可以使用 slice() 方法创建副本,对于对象,可以使用 Object.assign() 或展开运算符(...)来创建副本。

避免不必要的参数传递

在编写函数时,应尽量避免传递不必要的参数。过多的参数会使函数的调用变得复杂,并且可能导致代码难以维护。例如,考虑以下函数:

function calculateTotalPrice(price, taxRate, discount, shippingCost) {
    var subtotal = price * (1 - discount);
    var tax = subtotal * taxRate;
    return subtotal + tax + shippingCost;
}
var total = calculateTotalPrice(100, 0.1, 0.05, 10);
console.log(total); // 输出 104.5

在某些情况下,如果 taxRateshippingCost 是固定值,我们可以将它们设置为默认参数,或者将相关的计算逻辑封装到其他函数中,以减少传递的参数数量。例如:

const DEFAULT_TAX_RATE = 0.1;
const DEFAULT_SHIPPING_COST = 10;
function calculateSubtotal(price, discount) {
    return price * (1 - discount);
}
function calculateTotalPrice(price, discount) {
    var subtotal = calculateSubtotal(price, discount);
    var tax = subtotal * DEFAULT_TAX_RATE;
    return subtotal + tax + DEFAULT_SHIPPING_COST;
}
var total = calculateTotalPrice(100, 0.05);
console.log(total); // 输出 104.5

这样,calculateTotalPrice 函数只需要传递 pricediscount 两个参数,使函数的调用更加简洁,同时也提高了代码的可读性和可维护性。

函数重载的模拟与优化

在一些编程语言中,函数重载(Function Overloading)允许我们定义多个同名但参数列表不同的函数。然而,JavaScript 本身并不支持传统意义上的函数重载。不过,我们可以通过一些技巧来模拟函数重载的行为。

通过检查参数类型和数量模拟重载

function greet() {
    if (arguments.length === 0) {
        console.log('Hello, world!');
    } else if (arguments.length === 1 && typeof arguments[0] ==='string') {
        console.log(`Hello, ${arguments[0]}!`);
    } else if (arguments.length === 2 && typeof arguments[0] ==='string' && typeof arguments[1] ==='string') {
        console.log(`Hello, ${arguments[0]} and ${arguments[1]}!`);
    }
}
greet(); // 输出 'Hello, world!'
greet('John'); // 输出 'Hello, John!'
greet('John', 'Jane'); // 输出 'Hello, John and Jane!'

在这个例子中,我们通过检查 arguments 对象的长度和参数的类型来模拟不同的函数重载情况。虽然这种方法可以实现类似函数重载的功能,但代码可能会变得冗长和难以维护,尤其是当重载情况较多时。

使用函数柯里化优化模拟重载

函数柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。通过柯里化,我们可以更优雅地模拟函数重载。例如:

function greetCurried(name) {
    if (arguments.length === 0) {
        return 'Hello, world!';
    }
    return function (secondName) {
        if (secondName === undefined) {
            return `Hello, ${name}!`;
        }
        return `Hello, ${name} and ${secondName}!`;
    };
}
var greet = greetCurried();
console.log(greet()); // 输出 'Hello, world!'
greet = greetCurried('John');
console.log(greet()); // 输出 'Hello, John!'
console.log(greet('Jane')); // 输出 'Hello, John and Jane!'

这里,greetCurried 函数返回一个新的函数。通过逐步传递参数,我们可以实现不同的功能,就像函数重载一样。这种方式使代码更加简洁和灵活,同时也提高了代码的可读性和可维护性。

总结形参与实参优化的最佳实践

保持函数参数的简洁性

尽量减少函数的参数数量,避免传递不必要的参数。如果参数较多,可以考虑将相关参数封装成对象,并使用对象解构来获取参数值。这样可以使函数的调用更加清晰,并且易于维护。

使用默认参数值

为函数的形参设置合理的默认参数值,以处理实参缺失的情况。这不仅可以减少函数内部的逻辑判断,还可以使函数的调用更加灵活。

利用剩余参数和数组/对象解构

剩余参数可以方便地处理不定数量的参数,而数组和对象解构可以使函数的形参更加直观和易于理解。合理使用这些特性可以提高代码的可读性和简洁性。

注意参数传递的性能影响

了解基本类型和引用类型的传递方式,对于引用类型,如果需要在函数内部修改数据但不影响外部原始数据,要注意创建副本。同时,避免传递大型的不必要的数据,以提高性能。

谨慎模拟函数重载

如果需要模拟函数重载,尽量使用函数柯里化等技术来使代码更加优雅和易于维护,避免过度依赖对 arguments 对象的检查,以免代码变得冗长和难以理解。

通过对 JavaScript 函数形参与实参的优化,我们可以编写更高效、更易读和更可维护的代码。在实际编程中,应根据具体的需求和场景,灵活运用这些优化技巧,以提升代码的质量和性能。