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

JavaScript函数表达式与命名函数的比较

2022-11-111.9k 阅读

函数定义方式概述

在JavaScript中,函数是一等公民,它既可以像普通变量一样被使用,也能够作为参数传递给其他函数,或者从其他函数返回。函数有多种定义方式,其中函数表达式和命名函数是两种常见且重要的形式。理解它们之间的区别,对于编写高质量、可维护的JavaScript代码至关重要。

函数表达式

函数表达式是在表达式上下文中定义函数的一种方式。它将函数定义作为一个值赋给变量、作为数组元素、对象属性,或者在其他表达式中使用。

基本语法

// 匿名函数表达式
let funcExpression = function() {
    console.log('This is a function expression');
};
funcExpression();

在上述代码中,function()是一个匿名函数,它没有自己独立的名称,而是被赋值给变量funcExpression。通过调用funcExpression(),我们可以执行这个函数。

立即调用的函数表达式(IIFE)

IIFE是函数表达式的一种特殊形式,它允许函数在定义后立即执行。

// 立即调用的函数表达式
let result = (function() {
    let a = 5;
    let b = 3;
    return a + b;
})();
console.log(result);

在这段代码中,(function() { /* code */ })()定义了一个IIFE。括号将函数定义包裹起来,使其成为一个表达式,最后的括号用于立即调用这个函数。函数内部的代码被执行,ab相加的结果被返回并赋值给result变量。

作为参数传递

函数表达式常常作为参数传递给其他函数。例如,在数组的map方法中:

let numbers = [1, 2, 3, 4];
let squaredNumbers = numbers.map(function(number) {
    return number * number;
});
console.log(squaredNumbers);

这里,map方法期望一个函数作为参数,我们通过函数表达式定义了这个函数,它将数组中的每个元素平方,并返回一个新的数组。

命名函数

命名函数,也称为函数声明,是在JavaScript中定义函数的传统方式。它具有一个明确的函数名,该函数名在函数的作用域内以及包含该函数的外部作用域中都可以访问。

基本语法

function namedFunction() {
    console.log('This is a named function');
}
namedFunction();

上述代码定义了一个名为namedFunction的函数,通过函数名可以直接调用该函数。

函数提升

命名函数具有函数提升的特性。这意味着在JavaScript代码执行之前,解析器会将函数声明提升到其所在作用域的顶部。

// 先调用函数,后定义函数
namedFunction();
function namedFunction() {
    console.log('Function is hoisted');
}

尽管namedFunction的调用在其定义之前,但代码仍然能够正常运行。这是因为函数声明在编译阶段被提升到了作用域的顶部,所以在调用时函数已经存在。

函数表达式与命名函数的比较

函数提升

  1. 命名函数的提升 命名函数会被提升到其所在作用域的顶部,这使得在函数定义之前就可以调用它。这在一些复杂的逻辑结构中,例如在条件判断语句之前调用函数时非常有用。
if (true) {
    callFunction();
    function callFunction() {
        console.log('Function called from inside if block');
    }
}

在这个例子中,callFunction的调用在其定义之前,但由于函数提升,代码能够顺利执行。

  1. 函数表达式的非提升性 函数表达式不会被提升。如果在定义之前尝试调用函数表达式,会导致ReferenceError
// 这里会报错
funcExpression();
let funcExpression = function() {
    console.log('This is a function expression');
};

在上述代码中,funcExpression()的调用在变量声明之前,由于函数表达式不会被提升,此时funcExpression还未定义,从而引发错误。

作用域

  1. 命名函数的作用域 命名函数的作用域取决于其定义的位置。如果在全局作用域中定义,它在整个全局作用域内都可访问;如果在函数内部定义,则在该函数的作用域内可访问。
function outerFunction() {
    function innerNamedFunction() {
        console.log('Inner named function');
    }
    innerNamedFunction();
}
outerFunction();
// 以下调用会报错,因为innerNamedFunction只在outerFunction作用域内有效
// innerNamedFunction(); 

innerNamedFunction定义在outerFunction内部,只能在outerFunction内部调用,在外部调用会导致错误。

  1. 函数表达式的作用域 函数表达式的作用域也遵循JavaScript的作用域规则。但需要注意的是,当函数表达式作为变量定义时,其作用域与变量的作用域相同。
function outerFunction() {
    let funcExpression = function() {
        console.log('Inner function expression');
    };
    funcExpression();
}
outerFunction();
// 以下调用会报错,因为funcExpression只在outerFunction作用域内有效
// funcExpression(); 

这里funcExpression作为函数表达式定义在outerFunction内部,其作用域也局限于outerFunction,在外部调用同样会出错。

递归调用

  1. 命名函数的递归 命名函数由于有明确的函数名,在递归调用时非常直观和方便。例如计算阶乘:
function factorial(n) {
    if (n === 0 || n === 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}
let result = factorial(5);
console.log(result);

factorial函数内部,通过函数名factorial进行递归调用,实现了阶乘的计算。

  1. 函数表达式的递归 对于函数表达式,由于匿名函数本身没有名称,递归调用相对复杂一些。可以通过将函数表达式赋值给一个变量,然后在函数内部使用该变量进行递归。
let factorial = function f(n) {
    if (n === 0 || n === 1) {
        return 1;
    } else {
        return n * f(n - 1);
    }
};
let result = factorial(5);
console.log(result);

这里在函数表达式内部使用了一个内部名称f来进行递归调用。如果不使用这个内部名称,直接使用factorial进行递归,在严格模式下可能会导致错误,因为在函数完全初始化之前factorial可能是未定义的。

可读性与可维护性

  1. 命名函数的优势 命名函数具有明确的函数名,在代码阅读和调试时,从函数名就可以大致了解函数的功能。例如:
function calculateTotalPrice(products) {
    let total = 0;
    for (let product of products) {
        total += product.price * product.quantity;
    }
    return total;
}

calculateTotalPrice这个函数名清晰地表明了函数的功能是计算产品的总价格,代码的可读性非常高。

  1. 函数表达式的情况 函数表达式,尤其是匿名函数表达式,在简单场景下简洁明了,如在数组的mapfilter等方法中使用时。但在复杂逻辑或较长的函数体中,匿名函数表达式可能会降低代码的可读性。
let numbers = [1, 2, 3, 4];
let newNumbers = numbers.map(function(number) {
    let temp = number * 2;
    if (temp > 5) {
        return temp;
    } else {
        return number;
    }
});

这段代码中,map方法中的匿名函数逻辑相对复杂,没有明确的函数名,对于阅读代码的人来说,理解其功能需要花费更多时间。

内存管理

  1. 命名函数 命名函数在其作用域内一直存在,直到作用域被销毁。这意味着如果命名函数在全局作用域中定义,它会一直占用内存,直到页面卸载。
function globalNamedFunction() {
    // 函数逻辑
}
// 只要页面不卸载,globalNamedFunction就会一直占用内存
  1. 函数表达式 函数表达式在其所赋值的变量不再被引用时,相关的函数对象可以被垃圾回收机制回收。
function outerFunction() {
    let funcExpression = function() {
        // 函数逻辑
    };
    return funcExpression;
}
let resultFunction = outerFunction();
// 如果之后不再使用resultFunction,funcExpression所代表的函数对象可能会被垃圾回收
resultFunction = null; 

resultFunction被赋值为null后,它不再引用函数表达式所创建的函数对象,该对象就有可能被垃圾回收机制回收,从而释放内存。

事件处理

  1. 命名函数在事件处理中的应用 在事件处理中,使用命名函数可以使代码结构更清晰,特别是在多个元素绑定相同事件处理逻辑时。
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Named Function in Event Handling</title>
</head>

<body>
    <button id="btn1">Click me</button>
    <button id="btn2">Click me too</button>
    <script>
        function handleClick() {
            console.log('Button clicked');
        }
        document.getElementById('btn1').addEventListener('click', handleClick);
        document.getElementById('btn2').addEventListener('click', handleClick);
    </script>
</body>

</html>

这里handleClick作为命名函数,被多个按钮的点击事件所引用,代码简洁且易于维护。

  1. 函数表达式在事件处理中的应用 函数表达式在事件处理中也很常见,特别是对于只在特定元素上使用一次的事件处理逻辑。
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Function Expression in Event Handling</title>
</head>

<body>
    <button id="btn">Click me</button>
    <script>
        document.getElementById('btn').addEventListener('click', function() {
            console.log('Button clicked');
        });
    </script>
</body>

</html>

这种方式直接在addEventListener中使用函数表达式定义事件处理逻辑,无需额外定义命名函数,代码更加紧凑。

实际应用场景

函数表达式的应用场景

  1. 回调函数 在异步操作中,如setTimeoutsetInterval以及各种AJAX请求库中,函数表达式经常作为回调函数使用。
setTimeout(function() {
    console.log('This is a callback function');
}, 2000);

这里函数表达式作为setTimeout的回调函数,在2秒后执行。

  1. 函数式编程 在函数式编程中,函数表达式用于创建高阶函数,如mapfilterreduce等数组方法。
let numbers = [1, 2, 3, 4];
let evenNumbers = numbers.filter(function(number) {
    return number % 2 === 0;
});
console.log(evenNumbers);

filter方法接收一个函数表达式作为参数,用于筛选出数组中的偶数。

命名函数的应用场景

  1. 复杂业务逻辑 当函数包含复杂的业务逻辑,需要进行多次调用,并且逻辑需要清晰表达时,命名函数是更好的选择。
function calculateShippingCost(order) {
    let baseCost = 5;
    if (order.totalPrice < 50) {
        baseCost += 10;
    }
    if (order.distance > 100) {
        baseCost += 5;
    }
    return baseCost;
}

calculateShippingCost函数处理了复杂的运费计算逻辑,通过命名函数,其功能一目了然,且可以在不同地方重复调用。

  1. 模块设计 在模块化编程中,命名函数可以作为模块的对外接口,提供清晰的功能定义。
// mathUtils.js
function add(a, b) {
    return a + b;
}
function subtract(a, b) {
    return a - b;
}
export { add, subtract };

在这个模块中,addsubtract作为命名函数,清晰地定义了模块提供的数学运算功能。

性能考虑

在大多数情况下,函数表达式和命名函数在性能上的差异可以忽略不计。现代JavaScript引擎都对函数的定义和调用进行了高度优化。然而,在极端情况下,如在一个循环中频繁定义函数,可能会有一些性能差异。

频繁定义命名函数

for (let i = 0; i < 10000; i++) {
    function innerFunction() {
        // 简单逻辑
        return i * 2;
    }
    let result = innerFunction();
}

在这个循环中,每次迭代都定义一个新的命名函数innerFunction。虽然JavaScript引擎可能会对此进行优化,但理论上这种频繁定义命名函数的方式可能会带来一些性能开销,因为函数提升和作用域管理需要一定的资源。

频繁定义函数表达式

for (let i = 0; i < 10000; i++) {
    let innerFunction = function() {
        // 简单逻辑
        return i * 2;
    };
    let result = innerFunction();
}

同样在循环中频繁定义函数表达式,相比命名函数,它不会有函数提升带来的额外开销,但每次创建新的函数对象也会占用一定的内存和计算资源。不过,实际性能差异在现代引擎下很难察觉,除非是极其复杂和高频的操作场景。

最佳实践建议

  1. 根据功能和复杂度选择 对于简单的、一次性使用的功能,如在数组方法中作为回调函数,或者在事件处理中只使用一次的逻辑,优先选择函数表达式,因为它简洁明了。而对于复杂的、需要多次调用的业务逻辑,或者作为模块的公共接口,使用命名函数可以提高代码的可读性和可维护性。

  2. 注意作用域和提升 了解函数提升和作用域规则,避免在函数定义之前调用函数表达式导致错误。在使用命名函数时,要注意其作用域范围,确保函数在合适的地方被调用。

  3. 内存管理意识 在可能出现内存泄漏的场景下,如长时间运行的应用程序或频繁创建和销毁函数的场景,要注意函数表达式和命名函数对内存的影响。合理使用变量引用,确保不再使用的函数对象能够被垃圾回收。

  4. 代码风格一致性 在团队开发中,保持一致的代码风格非常重要。如果团队已经习惯使用某种方式定义函数,尽量遵循团队的约定,以提高代码的整体可维护性和可读性。

通过深入理解JavaScript中函数表达式和命名函数的区别,并根据实际场景合理选择使用,开发者能够编写出更加高效、可读和可维护的JavaScript代码。无论是在小型项目还是大型企业级应用中,正确运用这两种函数定义方式都是至关重要的。