在 JavaScript 中,函数(Functions)是组织代码的基本构建块,也是实现模块化、复用性和抽象的关键。它们是一段可执行的代码块,设计用于执行特定任务。无论是处理数据、响应事件、封装复杂逻辑,还是实现高级编程模式,函数都扮演着核心角色。理解函数的声明方式、行为、参数传递以及它们在不同上下文中的表现,是掌握 JavaScript 这门语言的基石。

本教程将全面深入地探讨 JavaScript 中各种类型的函数。我们将从传统的函数声明 (Function Declaration)函数表达式 (Function Expression) 开始,深入理解它们的差异、变量提升(Hoisting)行为。随后,我们将重点介绍现代 JavaScript (ES6+) 中极具表现力的箭头函数 (Arrow Functions),并剖析其独特的 this 绑定机制。此外,我们还将覆盖函数参数的多样性、return 语句的作用、以及回调函数、高阶函数等核心概念。特别地,我们将深入探讨 this 关键字在不同函数调用方式下的表现,并揭示闭包(Closures)这一强大特性如何借由函数作用域实现。通过掌握这些核心概念,您将能够更有效地组织代码,编写出更清晰、更灵活、更具表现力的 JavaScript 应用程序。


1. 函数:代码的组织者与执行者

函数是一组执行特定任务的语句。在 JavaScript 中,函数是一等公民 (first-class citizens),这意味着它们可以像任何其他值(如数字、字符串)一样被赋值给变量、作为参数传递给其他函数、或者作为其他函数的返回值。

为什么使用函数?

  • 代码复用: 避免重复编写相同的代码。
  • 模块化: 将大型程序拆分为更小、更易管理的模块。
  • 抽象: 隐藏实现细节,让调用者只需关注函数的功能,而不必了解其内部工作方式。
  • 提高可读性: 通过命名函数,使代码意图更清晰。

2. 函数的声明方式

JavaScript 提供了多种定义函数的方式,每种方式都有其特点。

2.1 函数声明 (Function Declaration)

这是最传统和常见的方式。它使用 function 关键字后跟函数名、参数列表和函数体。

  • 语法:
    function functionName(parameter1, parameter2, ...) {
        // 函数体
        // 要执行的代码
        return value; // 可选,返回一个值
    }
    
  • 特性:
    • 变量提升 (Hoisting): 函数声明会像 var 变量一样被提升到其作用域的顶部。这意味着你可以在函数声明之前调用它,而不会报错。
    • 命名: 必须有函数名。
    sayHello(); // 可以正常调用,因为 sayHello 被提升了
    function sayHello() {
        console.log("Hello from function declaration!");
    }
    sayHello(); // 再次调用
    
2.2 函数表达式 (Function Expression)

函数表达式是将一个匿名函数(或可选的具名函数)赋值给一个变量。

  • 语法:
    const functionName = function(parameter1, parameter2, ...) {
        // 函数体
        return value;
    };
    
  • 特性:
    • 无提升: 函数表达式不会被提升。你必须在赋值之后才能调用它,否则会引发 ReferenceError
    • 具名函数表达式 (Named Function Expression): 虽然不常用,但函数表达式也可以有名字。这个名字只在函数内部可用,用于递归或调试。
    // greet(); // ReferenceError: Cannot access 'greet' before initialization
    const greet = function(name) {
        console.log(`Hello, ${name} from function expression!`);
    };
    greet("Alice"); // 正常调用
    
    const factorial = function fact(n) { // 具名函数表达式
        if (n <= 1) return 1;
        return n * fact(n - 1); // 内部可以使用 'fact' 进行递归
    };
    console.log(factorial(5)); // 120
    // console.log(fact(5)); // ReferenceError: fact is not defined (fact 只在函数内部可见)
    
  • 使用场景: 作为回调函数、IIFE (Immediately Invoked Function Expression) 等。
2.3 箭头函数 (Arrow Functions) (ES6+)

箭头函数是一种更简洁的函数表达式语法,它在 ES6 中引入,提供了更短的语法和不同的 this 绑定行为。

  • 语法:
    // 基础语法
    const functionName = (parameter1, parameter2, ...) => {
        // 函数体
        return value;
    };
    
    // 单个参数可以省略括号
    const singleParam = param => { /* ... */ };
    
    // 无参数需要空括号
    const noParam = () => { /* ... */ };
    
    // 函数体只有一行表达式时,可以省略花括号和 return 关键字(隐式返回)
    const add = (a, b) => a + b;
    console.log(add(2, 3)); // 5
    
    // 返回对象字面量时,需要用括号包裹
    const getObject = () => ({ id: 1, name: "Test" });
    console.log(getObject()); // { id: 1, name: "Test" }
    
  • 特性:
    • 简洁: 更短的语法,尤其适用于简单的回调函数。
    • 没有自己的 this 箭头函数没有自己的 this 绑定。它会捕获其外层作用域的 this。这一点是箭头函数与普通函数最大的区别,解决了许多 this 指向问题。
    • 没有 arguments 对象: 箭头函数没有自己的 arguments 对象,但可以通过 rest 参数 (...args) 来获取所有参数。
    • 不能作为构造函数: 不能使用 new 关键字调用箭头函数。
    • 没有 prototype 属性: 它们是匿名的(除非赋值给变量),并且不适合作为类方法。
    // 箭头函数的 this 示例
    function Person(name) {
        this.name = name;
        this.sayHiRegular = function() {
            setTimeout(function() {
                // 这里的 this 指向 window/global (或 undefined 在严格模式下)
                console.log(`Hi, my name is ${this.name} (regular function).`);
            }, 1000);
        };
        this.sayHiArrow = function() {
            setTimeout(() => {
                // 这里的 this 捕获了 Person 实例的 this
                console.log(`Hi, my name is ${this.name} (arrow function).`);
            }, 1000);
        };
    }
    
    const p = new Person("Bob");
    p.sayHiRegular(); // Hi, my name is undefined (或报错,取决于环境)
    p.sayHiArrow();   // Hi, my name is Bob (arrow function).
    
  • 使用场景: 适用于回调函数、数组方法(map, filter, reduce)等,需要简洁语法且希望 this 行为更可预测的场景。

3. 函数参数

函数可以接受零个或多个参数,用于在调用时向函数传递数据。

3.1 默认参数 (Default Parameters) (ES6+)

允许在函数定义时为参数指定默认值。如果调用时没有提供该参数,或者传入 undefined,则使用默认值。

function greet(name = "Guest", greeting = "Hello") {
    console.log(`${greeting}, ${name}!`);
}

greet("Alice", "Hi"); // Hi, Alice!
greet("Bob");         // Hello, Bob!
greet();              // Hello, Guest!
greet(undefined, "Howdy"); // Howdy, Guest!
3.2 Rest 参数 (Rest Parameters) (ES6+)

允许将一个不定数量的参数表示为一个数组。它使用 ... 语法,并且必须是函数定义的最后一个参数。

function sumAll(...numbers) {
    console.log(numbers); // [1, 2, 3, 4, 5]
    return numbers.reduce((total, num) => total + num, 0);
}
console.log(sumAll(1, 2, 3, 4, 5)); // 15

function logArguments(firstArg, ...remainingArgs) {
    console.log("First argument:", firstArg);
    console.log("Remaining arguments:", remainingArgs);
}
logArguments("a", "b", "c", "d");
// Output:
// First argument: a
// Remaining arguments: ["b", "c", "d"]
3.3 arguments 对象 (Legacy)

在非箭头函数中,arguments 是一个类数组对象,包含了函数被调用时传入的所有参数。它不是一个真正的数组,但可以通过索引访问参数和获取长度。

  • 缺点: arguments 对象的行为有些奇怪,且不支持所有数组方法。Rest 参数是现代 JavaScript 中更推荐的替代方案。
  • 在箭头函数中不可用。
function showArgs() {
    console.log(arguments); // 类数组对象
    console.log(arguments[0]); // 第一个参数
    console.log(arguments.length); // 参数数量
}
showArgs(1, "hello", true);
3.4 解构参数 (Destructuring Parameters) (ES6+)

允许从传入的对象或数组中解构出参数。

function displayUser({ name, age, city = "Unknown" }) {
    console.log(`Name: ${name}, Age: ${age}, City: ${city}`);
}

const userProfile = { name: "Eve", age: 28, city: "Paris" };
displayUser(userProfile); // Name: Eve, Age: 28, City: Paris

displayUser({ name: "Frank", age: 40 }); // Name: Frank, Age: 40, City: Unknown

4. return 语句

return 语句用于终止函数的执行,并返回一个值给函数调用者。

  • 返回一个值:
    function multiply(a, b) {
        return a * b; // 返回乘积
        console.log("这行代码不会执行"); // return 后的代码不会被执行
    }
    const result = multiply(4, 5);
    console.log(result); // 20
    
  • 没有 return 语句或 return; 如果函数没有 return 语句,或者 return 后面没有指定值,函数会隐式返回 undefined
    function doNothing() {
        // 没有 return
    }
    console.log(doNothing()); // undefined
    
    function explicitUndefined() {
        return; // 返回 undefined
    }
    console.log(explicitUndefined()); // undefined
    

5. 回调函数 (Callback Functions)

回调函数是指作为参数传递给另一个函数,并在外部函数内部某个特定时刻被调用的函数。它们是 JavaScript 异步编程和事件处理的基石。

function fetchData(url, callback) {
    // 模拟异步数据获取
    setTimeout(() => {
        const data = `${url} 获取的数据`;
        callback(data); // 数据获取完毕后,调用回调函数
    }, 2000);
}

function displayData(data) {
    console.log("接收到的数据:", data);
}

console.log("开始获取数据...");
fetchData("https://api.example.com/users", displayData); // displayData 是回调函数
console.log("数据获取请求已发送,程序继续执行其他任务...");

// 匿名回调函数
setTimeout(function() {
    console.log("2 秒后执行。");
}, 2000);

// 箭头函数作为回调
document.getElementById('myButton').addEventListener('click', () => {
    console.log("按钮被点击了!");
});

6. 高阶函数 (Higher-Order Functions)

高阶函数是满足以下两个条件之一或全部的函数:

  1. 接受一个或多个函数作为参数。
  2. 返回一个函数。

它们是函数式编程的核心概念,极大地提高了代码的抽象和复用能力。

// 1. 接受函数作为参数的例子
function operateOnNumbers(arr, operation) {
    const results = [];
    for (let i = 0; i < arr.length; i++) {
        results.push(operation(arr[i]));
    }
    return results;
}

function double(num) {
    return num * 2;
}

function square(num) {
    return num * num;
}

const numbers = [1, 2, 3, 4];
console.log(operateOnNumbers(numbers, double)); // [2, 4, 6, 8]
console.log(operateOnNumbers(numbers, square)); // [1, 4, 9, 16]

// JavaScript 内置的高阶函数:map, filter, reduce 等
const doubledNumbers = numbers.map(double); // map 接受一个回调函数
console.log(doubledNumbers); // [2, 4, 6, 8]


// 2. 返回函数的例子 (函数工厂)
function createMultiplier(factor) {
    return function(number) { // 返回一个函数
        return number * factor;
    };
}

const multiplyBy5 = createMultiplier(5); // multiplyBy5 现在是一个新函数
const multiplyBy10 = createMultiplier(10); // multiplyBy10 也是一个新函数

console.log(multiplyBy5(3));  // 15
console.log(multiplyBy10(3)); // 30

高阶函数与闭包紧密相关,createMultiplier 函数返回的内部函数就是闭包,它“记住”了外部 factor 变量。


7. 闭包 (Closures)

(此部分为简要回顾,详细内容可参考前一篇作用域教程)
闭包是指函数可以记住并访问其词法作用域(即定义它时的外部函数作用域)的变量,即使外部函数已经执行完毕并返回。这是因为 JavaScript 的词法作用域特性。

// 闭包示例
function makeCounter() {
    let count = 0; // 这是一个局部变量

    return function() { // 返回的内部函数形成了闭包
        count++;        // 它可以访问并修改外部函数的 count 变量
        return count;
    };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

8. this 关键字

this 关键字是 JavaScript 中一个非常重要的概念,但它也很容易让人困惑。this 的值在函数调用时才确定,并且取决于函数的调用方式

  • 全局上下文: 在全局作用域中,this 指向全局对象(浏览器中是 window,Node.js 中是 global)。
  • 普通函数调用: 当函数作为独立函数被调用时(非方法),this 在非严格模式下指向全局对象,在严格模式下指向 undefined
    function showThis() {
        console.log(this);
    }
    showThis(); // window (浏览器非严格模式) / undefined (严格模式)
    
  • 方法调用: 当函数作为对象的方法被调用时,this 指向调用该方法的对象。
    const myObject = {
        name: "MyObject",
        logName: function() {
            console.log(this.name);
        }
    };
    myObject.logName(); // MyObject
    
  • 构造函数调用: 当函数作为构造函数使用 new 关键字调用时,this 指向新创建的实例对象。
    function Car(make) {
        this.make = make;
    }
    const myCar = new Car("Honda");
    console.log(myCar.make); // Honda
    
  • 显式绑定: 可以使用 call(), apply(), bind() 方法显式地设置 this 的值。
    • call(thisArg, arg1, arg2, ...):立即执行函数,thisArgthis 值,参数逐个传入。
    • apply(thisArg, [arg1, arg2, ...]):立即执行函数,thisArgthis 值,参数以数组形式传入。
    • bind(thisArg, arg1, arg2, ...):返回一个新函数,该新函数的 this 永远绑定为 thisArg,参数逐个传入。
    const anotherObject = { name: "AnotherObject" };
    myObject.logName.call(anotherObject); // AnotherObject
    
    const boundLogName = myObject.logName.bind(anotherObject);
    boundLogName(); // AnotherObject
    
  • 箭头函数: 箭头函数没有自己的 this。它会从其词法作用域(外层作用域)继承 this 的值。这使得它们在作为回调函数时特别有用,因为它能保持 this 的上下文。
    // 见 2.3 箭头函数示例
    

9. 纯函数 (Pure Functions)

纯函数是函数式编程中的一个重要概念。一个函数被称为纯函数,如果它满足以下两个条件:

  1. 相同的输入,总是产生相同的输出: 给定相同的参数,它总是返回相同的结果,不依赖于任何外部可变状态。
  2. 没有副作用: 它不会修改其外部的任何状态(如全局变量、参数对象、DOM),也不会执行 I/O 操作(如网络请求、打印到控制台)。
// 纯函数
function addPure(a, b) {
    return a + b;
}

// 非纯函数 (因为它依赖/修改了外部状态)
let total = 0;
function addImpure(a, b) {
    total = a + b; // 修改了外部变量 total
    return total;
}

优点: 易于测试、易于理解、易于并行处理、可缓存。


10. 立即执行函数表达式 (IIFE - Immediately Invoked Function Expression)

IIFE 是一种在定义后立即执行的函数表达式。它通常用于创建私有作用域,避免变量污染全局命名空间。

  • 语法: (function() { /* ... */ })();(() => { /* ... */ })();
    (function() {
        var secret = "我的秘密"; // secret 只在 IIFE 内部可见
        console.log("IIFE 执行了。");
        // console.log(secret);
    })();
    // console.log(secret); // ReferenceError: secret is not defined
    
    // 带参数的 IIFE
    (function(message) {
        console.log(message);
    })("你好,IIFE!");
    
  • 用途: 创建私有作用域、模块模式、避免全局变量污染。

11. 异步函数 (Async Functions) (ES2017+) (简要提及)

async 函数是处理异步操作的一种更简洁、更易读的方式。它们总是返回一个 Promise。

  • 语法: 使用 async 关键字修饰函数,内部可以使用 await 关键字暂停执行,直到一个 Promise 解决。
    async function fetchDataAsync(url) {
        try {
            const response = await fetch(url); // 等待 fetch Promise 解决
            const data = await response.json(); // 等待 json 解析 Promise 解决
            console.log("Async data:", data);
        } catch (error) {
            console.error("Async error:", error);
        }
    }
    // fetchDataAsync("https://api.example.com/data");
    
  • 更多细节: 异步函数与 Promise 紧密相关,是一个更深入的主题。

12. 最佳实践与常见陷阱

  1. 优先使用 const 声明函数: 对于函数表达式和箭头函数,使用 const 可以防止函数被意外重新赋值,增加代码的稳定性。
  2. 选择合适的函数类型:
    • 函数声明: 当你需要函数提升,并且函数是顶层代码块中的主要功能时。
    • 函数表达式: 作为回调函数、IIFE 或在需要将函数赋值给变量的场景。
    • 箭头函数: 作为简洁的回调函数,或者当你需要 this 自动绑定到外层作用域时。避免在需要动态 this(如对象方法,尤其是原型方法)或作为构造函数时使用。
  3. 理解 this 的行为: 这是 JavaScript 最常见的困惑点之一。牢记 this 取决于函数的调用方式。
  4. 清晰命名函数: 函数名应该清楚地表达其功能,遵循动词-名词的约定(如 getUserData, calculateTotal)。
  5. 避免副作用: 尽可能编写纯函数,它们更容易测试和理解。如果函数确实需要修改外部状态,请明确地在函数名或文档中体现。
  6. 合理使用参数:
    • 利用默认参数减少不必要的 if 条件检查。
    • 使用 Rest 参数处理不定数量的参数,取代老旧的 arguments 对象。
    • 使用解构参数提高代码可读性,特别是处理配置对象时。
  7. 函数应该做一件事 (Single Responsibility Principle): 尽量保持函数短小,只负责完成一个明确的任务。如果函数功能过于复杂,考虑拆分成多个小函数。
  8. 考虑错误处理: 如果函数可能失败,确保有适当的错误处理机制(如 try-catch,或返回 Promise 的 reject)。

13. 总结与展望

函数是 JavaScript 的灵魂,它们是构建任何复杂应用程序的基石。从最简单的函数声明到现代的箭头函数,从参数的灵活传递到 this 的复杂绑定,再到回调、高阶函数和闭包等高级概念,每一个环节都体现了 JavaScript 的强大和灵活性。

  • 函数声明提升,函数表达式箭头函数不提升(箭头函数行为更独特)。
  • this 的指向取决于函数的调用方式,而箭头函数则词法绑定 this
  • 回调函数高阶函数是实现异步和函数式编程的强大工具。
  • 闭包允许函数记住其外部环境,实现数据私有化和工厂模式。

深入掌握函数及其相关概念,将使您能够编写出更具表达力、更健壮、更易于维护的 JavaScript 代码。不断实践和探索,您将解锁 JavaScript 编程的无限可能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐