JavaScript 函数

函数是 JS 最重要的概念之一——把一段代码封装起来,起个名字,需要时调用。JS 里函数是"一等公民":可以赋值给变量、当参数传递、当返回值,这种灵活性是 JS 强大的根源。这一章我们彻底搞清楚函数的所有用法。

1. 三种定义方式

// 1. 函数声明(有提升,可以在定义前调用)
function add(a, b) {
  return a + b;
}
console.log(add(1, 2));   // 3

// 2. 函数表达式(赋值给变量)
const subtract = function (a, b) {
  return a - b;
};
console.log(subtract(5, 2));  // 3

// 3. 箭头函数(最简洁,推荐)
const multiply = (a, b) => a * b;
console.log(multiply(3, 4));  // 12

// 函数没有 return 就返回 undefined
function greet() {
  console.log("hi");
}
console.log(greet());     // undefined

函数声明会被提升到作用域顶部,可以在定义之前调用;函数表达式不会提升——变量提升后值是 undefined,在赋值前调用会报错。所以函数表达式必须先定义后使用

2. 箭头函数

箭头函数是 ES6 引入的,语法极其简洁,是现代 JS 的主流写法:

// 箭头函数语法
const f1 = () => "hello";           // 无参数
const f2 = (x) => x * 2;            // 一个参数(括号可省)
const f3 = x => x * 2;              // 等价于上一行
const f4 = (a, b) => a + b;         // 多参数
const f5 = (a, b) => {              // 函数体多行,需要 {} 和 return
  const sum = a + b;
  return sum;
};

// 箭头函数 vs 普通函数:箭头函数没有自己的 this
const obj = {
  name: "小明",
  // ❌ 普通函数:this 取决于调用方式
  greet: function () {
    setTimeout(function () {
      console.log(this.name);   // undefined(this 是 window)
    }, 100);
  },
  // ✅ 箭头函数:继承外层的 this
  greetOk: function () {
    setTimeout(() => {
      console.log(this.name);   // "小明"
    }, 100);
  }
};

箭头函数 vs 普通函数最大的区别:箭头函数没有自己的 this——它的 this 继承自定义时的外层作用域(词法 this)。普通函数的 this 取决于调用方式。这意味着在回调里(尤其 setTimeout、事件处理),箭头函数能避免 this 丢失的经典坑。

另外箭头函数不能用作构造函数(不能 new),没有 arguments 对象。

3. 参数:默认值、剩余、解构

// 默认参数:调用时不传就用默认值
const greet = (name = "匿名", greeting = "你好") => {
  console.log(`${greeting},${name}!`);
};
greet();                 // 你好,匿名!
greet("小明");           // 你好,小明!
greet("小明", "嗨");     // 嗨,小明!

// 剩余参数(rest):把多余的实参收成数组
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
console.log(sum(1, 2, 3));     // 6
console.log(sum(1, 2, 3, 4));  // 10

// 普通参数 + 剩余参数(剩余必须在最后)
const log = (prefix, ...items) => {
  console.log(prefix, items);
};
log("fruits", "apple", "banana");   // fruits ["apple", "banana"]

// arguments 对象(老语法,普通函数才有)
function showArgs() {
  console.log(arguments);   // 类数组,不是真数组
}
showArgs(1, 2, 3);          // [1, 2, 3]

剩余参数(...args)是替代老式 arguments 的现代写法,得到的是真正的数组,可以调用数组方法。

4. 回调与高阶函数

这是 JS 函数式编程的基础。把函数当数据用:

// 函数是一等公民:可以当参数传,可以当返回值

// 回调函数:把函数作为参数传给另一个函数
function doSomething(callback) {
  console.log("开始干活...");
  callback();
}
doSomething(() => console.log("干完了"));

// 数组方法的回调
const nums = [1, 2, 3];
nums.forEach(n => console.log(n));
const doubled = nums.map(n => n * 2);
const evens = nums.filter(n => n % 2 === 0);

// 高阶函数:接收函数作为参数,或返回函数
const multiplier = (factor) => (x) => x * factor;
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5));   // 10
console.log(triple(5));   // 15

柯里化(返回函数的函数)是高阶函数的典型应用,可以"预填参数"生成专用函数,在函数式编程里大量使用。

5. 递归

递归是函数调用自己。必须要有终止条件,否则会无限调用导致栈溢出:

// 递归:函数调用自己
// 经典:阶乘 n!
function factorial(n) {
  if (n <= 1) return 1;       // 终止条件
  return n * factorial(n - 1);
}
console.log(factorial(5));    // 120(5*4*3*2*1)

// 斐波那契数列
function fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}
console.log(fib(10));         // 55

// 实战:深拷贝对象
function deepCopy(obj) {
  if (obj === null || typeof obj !== "object") return obj;
  const result = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepCopy(obj[key]);
    }
  }
  return result;
}

递归通常代码更简洁(树、图的遍历特别自然),但性能上不如循环(每次调用都压栈)。性能敏感场景可以改写成循环,或用尾递归优化(JS 严格模式支持,但浏览器支持不完善)。

6. 立即调用函数(IIFE)

IIFE(Immediately Invoked Function Expression)是定义后立刻执行的函数,主要用于创建独立作用域:

// IIFE:立即调用函数表达式(定义后立刻执行)
(function () {
  console.log("立刻执行");
})();

// 用途 1:避免污染全局(ES6 之前用 var 时常用)
(function () {
  var private = "secret";
  console.log(private);
})();
// console.log(private);   // 报错,外面访问不到

// 用途 2:创建独立作用域,避免循环变量泄漏
for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 100);
  })(i);
}
// 输出: 0 1 2

// ES6 之后,let/const + 模块基本替代了 IIFE

ES6 之后,有了 let/const 块级作用域和模块系统,IIFE 的使用大幅减少。但在老代码里仍很常见,需要看得懂。

7. this 简述

JS 的 this 是最让人困惑的概念之一,这里先做简单了解:

const obj = {
  name: "小明",
  sayHi() { console.log(this.name); },
  sayHiArrow: () => console.log(this.name)
};

obj.sayHi();           // "小明"(普通函数,this 是 obj)
obj.sayHiArrow();      // undefined(箭头函数,this 是外层,即 window)

const fn = obj.sayHi;
fn();                  // undefined(裸调用,this 是 window/undefined)

this 的坑会在"对象"一章进一步讲。

8. 闭包预览

// 函数可以"记住"它定义时所在的作用域
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

// count 从外部访问不到,只能通过 counter() 改 - 这就是"私有变量"
// console.log(count);    // ReferenceError

函数"记住"外层变量、即使外层函数已经返回——这就是闭包。下一章我们专门讲它。

小结

下一篇我们深入 JS 最有特色的概念——作用域与闭包

← 上一篇 JavaScript 条件与循环

下一篇 JavaScript 作用域与闭包

✈️💬