JavaScript 函数终极教程

函数是 JavaScript 的核心构建块,它们是可重复使用的代码块,用于执行特定的任务,理解函数是掌握 JavaScript 的关键一步。

javascript 函数教程
(图片来源网络,侵删)

目录

  1. 什么是函数?
  2. 函数的基本语法
  3. 函数参数
  4. 函数返回值
  5. 函数表达式 vs 函数声明
  6. 箭头函数 (ES6)
  7. 作用域 和提升
  8. 高阶函数
  9. 闭包
  10. 立即调用函数表达式
  11. 最佳实践与总结

什么是函数?

想象一下函数就像一个“工具箱”,你给它一些原材料(参数),它按照一套固定的指令(函数体)进行加工,最后可能会给你一个成品(返回值)。

在编程中,函数的主要作用是:

  • 代码复用:避免重复编写相同的代码。
  • 模块化:将复杂的任务分解成更小、更易管理的部分。
  • 可读性:让代码结构更清晰,更容易理解和维护。

函数的基本语法

定义函数最基本的方式是使用 function 关键字,这被称为函数声明

// 语法
function functionName(参数1, 参数2, ...) {
  // 函数体:这里是要执行的代码
  // ...
  // 可选:返回一个值
  return 返回值;
}
// 示例:一个简单的打招呼函数
function sayHello() {
  console.log("你好,世界!");
}
// 调用(执行)函数
sayHello(); // 输出: 你好,世界!

组成部分解析:

javascript 函数教程
(图片来源网络,侵删)
  • function: 关键字,告诉 JavaScript 你正在定义一个函数。
  • functionName: 函数的名称,用于在后续调用它,命名应具有描述性(calculateSumdoSomething 更好)。
  • (parameters): 参数列表,用逗号分隔,这是你传递给函数的“原材料”,如果没有参数,括号可以为空。
  • 函数体,包含函数要执行的所有语句。
  • return: 关键字,用于从函数中“返回”一个值,执行到 return 语句后,函数会立即停止执行。

函数参数

参数是函数与外部世界交互的入口,你可以向函数传递数据,让函数基于这些数据执行操作。

1 传递参数

// 定义一个带参数的函数
function greet(name) {
  console.log("你好, " + name + "!");
}
// 调用时传入参数
greet("Alice"); // 输出: 你好, Alice!
greet("Bob");   // 输出: 你好, Bob!

2 默认参数 (ES6)

如果调用函数时没有提供某个参数,你可以为其设置一个默认值。

function power(base, exponent = 2) {
  return base ** exponent;
}
console.log(power(4));      // 输出: 16 (因为 exponent 默认为 2)
console.log(power(4, 3));   // 输出: 64 (覆盖了默认值)

3 剩余参数

当你不确定会传入多少个参数时,可以使用剩余参数 将它们收集到一个数组中。

function sum(...numbers) {
  // numbers 是一个数组,[1, 2, 3]
  let total = 0;
  for (const num of numbers) {
    total += num;
  }
  return total;
}
console.log(sum(1, 2));          // 输出: 3
console.log(sum(1, 2, 3, 4, 5)); // 输出: 15

函数返回值

函数可以通过 return 语句将一个值“返回”给调用者,如果没有 return 语句,函数会返回 undefined

javascript 函数教程
(图片来源网络,侵删)
// 返回一个值的函数
function add(a, b) {
  return a + b;
}
let result = add(5, 3);
console.log(result); // 输出: 8
// 没有返回值的函数
function logMessage(message) {
  console.log(message);
}
let returnValue = logMessage("这是一个日志消息。");
console.log(returnValue); // 输出: undefined

重要提示: return 语句会立即终止函数的执行。

function checkAge(age) {
  if (age < 18) {
    return "未成年";
  }
  return "已成年"; // 只有当 age >= 18 时才会执行到这里
}
console.log(checkAge(15)); // 输出: "未成年"
console.log(checkAge(20)); // 输出: "已成年"

函数表达式 vs 函数声明

这是 JavaScript 中一个非常重要的概念,它涉及到变量提升。

1 函数声明

我们之前看到的就是函数声明,它有一个关键特性:函数声明会被提升到其作用域的顶部,这意味着你可以在声明函数之前调用它。

sayHi(); // 可以正常执行,因为函数声明被提升了
function sayHi() {
  console.log("Hi!");
}

2 函数表达式

将一个函数赋值给一个变量。

// 错误的调用方式
// sayBye(); // 这里会报错,因为变量 sayBye 此时是 undefined
const sayBye = function() {
  console.log("Bye!");
};
sayBye(); // 正常执行

关键区别:

  • 函数声明:可以被提升,可以在声明前调用。
  • 函数表达式:不能被提升(变量会被提升,但赋值不会),只能在声明后调用。

命名函数表达式

你可以在函数表达式中给函数命名,这有助于调试。

const namedFunc = function myNamedFunction() {
  console.log("我是一个命名函数表达式。");
};
namedFunc(); // 可以调用
// myNamedFunction(); // 不能调用,这个名字只在函数内部可用

箭头函数 (ES6)

箭头函数是 ES6 引入的一种更简洁的函数书写方式,它不仅语法更短,还改变了一些 this 的行为(稍后会在闭包中提到)。

1 基本语法

// 1. 无参数,单行返回
const sayHello = () => "Hello!";
console.log(sayHello()); // 输出: Hello!
// 2. 单个参数,可以省略括号
const square = x => x * x;
console.log(square(4)); // 输出: 16
// 3. 多个参数,需要括号
const add = (a, b) => a + b;
console.log(add(2, 3)); // 输出: 5
// 4. 函数体有多行代码,需要花括号,并且必须使用 return
const complexCalc = (a, b) => {
  const sum = a + b;
  const product = a * b;
  return { sum, product };
};
console.log(complexCalc(3, 4)); // 输出: { sum: 7, product: 12 }

2 this 的区别

箭头函数没有自己的 this,它会从其外部(词法)作用域继承 this,这使得它在回调函数(如 setTimeout, map, filter)中非常有用。

function traditionalFunction() {
  console.log(this); // 在浏览器中,通常指向 window 对象
}
const arrowFunction = () => {
  console.log(this); // 继承外部作用域的 this
};
// 假设在一个对象中
const person = {
  name: "Alice",
  greetTraditional: function() {
    console.log(`你好,我是 ${this.name}`);
  },
  greetArrow: () => {
    console.log(`你好,我是 ${this.name}`); // 这里的 this 是全局的,不是 person
  }
};
person.greetTraditional(); // 输出: 你好,我是 Alice
person.greetArrow();       // 输出: 你好,我是 undefined (因为 this.name 是 undefined)

作用域 和提升

理解作用域和提升对于避免难以发现的 bug 至关重要。

1 作用域

作用域决定了变量和函数的可访问性。

  • 全局作用域:在任何函数外部定义的变量,在整个程序中都可以访问。
  • 函数作用域:在函数内部定义的变量,只能在函数内部访问。
let globalVar = "我是全局变量";
function myFunction() {
  let functionVar = "我是函数内部变量";
  console.log(globalVar); // 可以访问
  console.log(functionVar); // 可以访问
}
myFunction();
// console.log(functionVar); // 会报错,因为 functionVar 不在当前作用域

2 提升

JavaScript 引擎在代码执行前会进行“预编译”,在这个过程中,变量和函数声明会被移动到其作用域的顶部。

  • 变量提升var 声明的变量会被提升,但初始化不会,这会导致 var 存在一些问题(var x;var x = 10; 的区别)。
  • 函数提升:函数声明会被完整提升,因此你可以在声明前调用它。

let, const, var 的区别:

  • var: 函数作用域,有提升问题,可以被重复声明。
  • let: 块级作用域( 内),有提升(但不初始化),在声明前访问会报错(暂时性死区 TDZ),不能被重复声明。
  • const: 和 let 类似,但声明的变量是常量,不能被重新赋值。

推荐:在现代 JavaScript 开发中,优先使用 const,如果变量需要被重新赋值,再使用 let,尽量避免使用 var


高阶函数

高阶函数是函数式编程的核心概念,它是指满足以下任一条件的函数:

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

常见的例子是数组的 map, filter, forEach 等方法。

1 接受函数作为参数

function callWithFive(callback) {
  // 调用传入的函数,并传入参数 5
  callback(5);
}
function printNumber(num) {
  console.log("打印的数字是:", num);
}
callWithFive(printNumber); // 输出: 打印的数字是: 5

2 返回一个函数

function createMultiplier(multiplier) {
  // 返回一个新的函数
  return function(number) {
    return number * multiplier;
  };
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(10)); // 输出: 20
console.log(triple(10)); // 输出: 30

闭包

闭包是一个非常重要且强大的概念,它指的是一个函数能够“并访问其词法作用域,即使该函数在其词法作用域之外执行。

就是内层函数可以访问外层函数的变量

function outerFunction(x) {
  // outerFunction 的作用域
  let message = "外层函数的变量: ";
  return function innerFunction(y) {
    // innerFunction 访问了外层函数的变量 x
    // 这就形成了一个闭包
    console.log(message + x + y);
  };
}
const myClosure = outerFunction(10); // myClosure 持有了 outerFunction 的作用域
myClosure(5); // 输出: 外层函数的变量: 105
myClosure(1); // 输出: 外层函数的变量: 101

闭包的应用:

  1. 数据私有化:创建只能通过特定方法访问的私有变量。
  2. 创建工厂函数:如上面高阶函数的例子,createMultiplier 就是一个利用闭包的工厂函数。
  3. 回调函数和事件处理:在异步操作中,回调函数需要访问其定义时的上下文。

立即调用函数表达式

IIFE 是一种在定义后立即执行的函数表达式,它主要用于创建一个新的作用域,以避免污染全局命名空间。

// 语法:(function() { /* 代码 */ })();
(function() {
  console.log("这个函数只会执行一次。");
  let privateVar = "我是私有变量";
  console.log(privateVar);
})(); // 立即执行
// console.log(privateVar); // 会报错,因为 privateVar 不在全局作用域内

为什么用 包裹? JavaScript 引擎看到 function 关键字时,默认会将其视为一个函数声明,函数声明不能立即执行,必须以分号结尾,通过用 或 等运算符将其包裹起来,JavaScript 引擎就会将其解析为一个函数表达式,从而可以立即执行。


最佳实践与总结

  1. 命名清晰:函数名应该用动词开头,并清晰描述其功能(fetchUserData, validateInput)。
  2. 保持简洁:一个函数应该只做一件事,这被称为“单一职责原则”。
  3. 参数尽量少:函数参数越多,调用就越复杂,超过 3-4 个参数时,考虑使用一个对象作为参数。
  4. 优先使用 constlet:避免使用 var
  5. 使用箭头函数:在需要回调函数或 this 不会造成困扰的地方,优先使用简洁的箭头函数。
  6. 添加注释:对于复杂的函数逻辑,添加注释解释其目的和工作方式。
  7. 理解闭包:闭包是 JavaScript 的精髓之一,务必深入理解。

JavaScript 函数是构建动态和交互式应用程序的基石,从基础的声明和调用,到高级的闭包和高阶函数,每一个概念都至关重要。

  • 函数声明:最基础的定义方式,有提升。
  • 函数表达式:将函数赋值给变量,更灵活,无提升。
  • 箭头函数:更简洁的语法,并改变了 this 的行为。
  • 参数和返回值:函数与外界交互的桥梁。
  • 作用域和提升:决定了变量的可见性,是理解代码执行顺序的关键。
  • 高阶函数和闭包:实现函数式编程和高级模式的强大工具。

希望这份教程能帮助你全面掌握 JavaScript 函数!多写代码,多实践,这些概念会变得越来越清晰。