MST

星途 面试题库

面试题:JavaScript 函数表达式与命名函数在复杂异步场景中的应用与陷阱

在使用 Promise、async/await 等异步处理机制的复杂场景下,函数表达式与命名函数的使用有哪些注意事项和容易出现的陷阱?请结合实际项目场景举例说明,并提供解决方案。
38.0万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

注意事项和陷阱

  1. 作用域问题
    • 函数表达式:在函数表达式内部,this 的指向可能因调用方式而异。例如在事件回调等场景下,如果不注意,this 可能指向全局对象(在浏览器环境中是 window)而不是预期的对象。
    • 命名函数:在严格模式下,命名函数内部 this 默认是 undefined,但在非严格模式下也可能指向全局对象,同样会导致与预期不符的行为。
  2. 函数提升
    • 命名函数:存在函数提升,即在代码执行前,命名函数就已经被声明提升到当前作用域的顶部。这可能导致在函数定义之前就可以调用函数,容易引起逻辑混淆。例如:
func(); // 可以正常调用,输出 'I am a named function'
function func() {
    console.log('I am a named function');
}
  • 函数表达式:不存在函数提升,只有在执行到定义它的那一行代码时,函数才被创建。如果在定义之前调用,会抛出 ReferenceError。例如:
func(); // 抛出 ReferenceError: func is not defined
const func = function() {
    console.log('I am a function expression');
};
  1. 内存管理

    • 命名函数:由于函数提升,可能在不需要使用函数的地方也占用了内存空间,尤其是在大型项目中,如果有大量命名函数被提升但未被充分使用,可能会影响性能。
    • 函数表达式:只有在执行到定义语句时才会创建函数,相对更节省内存,但如果频繁定义函数表达式,也可能导致内存开销增加。
  2. 调试困难

    • 函数表达式:由于没有名称,在调试栈中显示的是 anonymous,不利于定位问题。例如在捕获到一个函数内部的错误时,调试信息可能只显示 anonymous function,难以直接确定具体的函数位置。
    • 命名函数:在调试栈中会显示函数名,方便定位错误发生的具体函数。

实际项目场景举例及解决方案

  1. 场景一:事件处理
    • 问题描述:在一个 Web 应用中,有一个按钮,点击按钮时需要发起一个异步请求。如果使用函数表达式作为点击事件的回调,可能会遇到 this 指向问题。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <button id="myButton">Click me</button>
    <script>
        const myButton = document.getElementById('myButton');
        myButton.addEventListener('click', function() {
            // 这里的 this 指向 window(非严格模式下)
            console.log(this); 
            async function fetchData() {
                try {
                    const response = await fetch('/api/data');
                    const data = await response.json();
                    console.log(data);
                } catch (error) {
                    console.error('Error fetching data:', error);
                }
            }
            fetchData();
        });
    </script>
</body>
</html>
  • 解决方案:可以使用箭头函数来避免 this 指向问题,因为箭头函数没有自己的 this,它的 this 继承自外层作用域。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <button id="myButton">Click me</button>
    <script>
        const myButton = document.getElementById('myButton');
        myButton.addEventListener('click', () => {
            async function fetchData() {
                try {
                    const response = await fetch('/api/data');
                    const data = await response.json();
                    console.log(data);
                } catch (error) {
                    console.error('Error fetching data:', error);
                }
            }
            fetchData();
        });
    </script>
</body>
</html>
  1. 场景二:模块封装
    • 问题描述:在一个模块中,有多个异步函数相互调用。如果使用命名函数,由于函数提升,可能会导致函数调用顺序混乱,难以维护。
// module.js
function func1() {
    func2();
    console.log('Function 1 executed');
}
function func2() {
    console.log('Function 2 executed');
}
export {func1};
  • 解决方案:可以将函数定义放在 export 对象中,这样不会产生函数提升,使代码逻辑更清晰。
// module.js
const func1 = function() {
    const func2 = function() {
        console.log('Function 2 executed');
    };
    func2();
    console.log('Function 1 executed');
};
export {func1};
  1. 场景三:调试困难
    • 问题描述:在一个复杂的异步操作链中,使用了函数表达式,当出现错误时,调试栈显示 anonymous function,难以定位问题。
async function main() {
    const result = await new Promise((resolve, reject) => {
        setTimeout(() => {
            // 假设这里出现错误
            throw new Error('Something went wrong');
        }, 1000);
    });
    console.log(result);
}
main().catch(console.error);
  • 解决方案:给函数表达式命名,这样在调试栈中就会显示有意义的函数名,方便定位问题。
async function main() {
    const result = await new Promise((resolve, reject) => {
        const asyncTask = function() {
            setTimeout(() => {
                throw new Error('Something went wrong');
            }, 1000);
        };
        asyncTask();
    });
    console.log(result);
}
main().catch(console.error);