函数基础

把重复的代码装进"盒子",随时调用

🎯 学习目标

学完本章,你将能够:

  • 能够定义和调用函数,正确使用参数和返回值
  • 能够区分函数声明、函数表达式和箭头函数,并根据场景选择合适写法
  • 能够使用默认参数和剩余参数(...args)处理可变参数
  • 能够解释作用域链的概念,理解变量查找规则
  • 能够使用闭包实现数据封装和高阶函数(如函数工厂)

📌 有编程基础?函数声明、参数、返回值你都会。但本章末尾的闭包是 JS 的核心特性——Java 没有闭包、C 的函数指针也不一样。如果你没接触过函数式语言,建议从 5.7 闭包部分开始精读,并衔接第 8 章的彻底解析。其余可跳过。

你有没有写过

同样的代码复制粘贴了五六遍,改一个地方就要改五六处?

比如,你写了一段计算折扣价的逻辑。购物车里每件商品都要用它,订单汇总也要用它,会员折扣还要用它。

如果你把这段逻辑复制了 10 次,有一天规则改了——你要改 10 个地方。漏掉一个就是 Bug。

函数就是解决这个问题的工具。你把逻辑写一次,给它起个名字,以后随时调用。

什么是函数

定义 → 白话 → 代码

定义:函数是一段可以被命名、接收参数、返回结果的代码块。

白话:函数就像一个自动售货机。你投币(输入参数),按按钮(调用函数),它吐出饮料(返回结果)。你不需要知道里面的机械怎么转,你只需要知道投什么币、出什么饮料。

这个比喻只是为了帮你建立直观感受。在计算机内部,函数实际上是一段被编译器记录了入口地址的指令序列,调用时 CPU 跳转到该地址执行,执行完再跳回来。

// 最简单的函数
function sayHi() {
    console.log("你好!");
}

sayHi(); // 调用:输出 "你好!"

声明与调用

先"造机器",再"按按钮"

声明函数用 function 关键字,调用时写函数名加括号。

// 声明函数
function greet(name) {
    console.log(`你好, ${name}!`);
}

// 调用函数
greet("张三"); // "你好, 张三!"
greet("李四"); // "你好, 李四!"

函数提升:用 function 声明的函数会被"提升"到作用域顶部。也就是说,你可以先调用,再声明。但表达式声明的函数不会提升,后面会讲到。

参数与返回值

输入原料,输出产品

参数是函数的输入,返回值是函数的输出。用 return 关键字把结果交还给调用者。

function add(a, b) {
    return a + b; // 返回 a + b 的结果
}

let result = add(3, 5);
console.log(result); // 8

没有 return 的函数默认返回 undefined。return 一执行,函数立刻结束,后面的代码不会运行。

默认与剩余参数

让参数更灵活、更智能

默认参数:调用时不传值,就使用默认值。

function greet(name = "世界") {
    console.log(`你好, ${name}!`);
}
greet();       // "你好, 世界!"
greet("张三"); // "你好, 张三!"

剩余参数:把多出来的参数收集成一个数组。必须放在最后。

function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

回调函数

把函数当作参数传给别人

JavaScript 中,函数是一等公民——你可以把函数当作变量一样传来传去。当一个函数被当作参数传给另一个函数时,它就叫回调函数(Callback)。

// setTimeout 就是使用回调的经典例子
setTimeout(function() {
    console.log("1秒后执行");
}, 1000);

// 用箭头函数更简洁
setTimeout(() => console.log("1秒后执行"), 1000);

// 自定义:让函数接收一个回调
function processArray(arr, callback) {
    for (let item of arr) {
        callback(item);
    }
}
processArray([1, 2, 3], (n) => console.log(n * 2));
// 输出: 2, 4, 6

回调函数是 JavaScript 异步编程的基础。后面学习 Promise 和 async/await 时,你会频繁遇到它。现在只需要理解"函数可以当参数传"就行。

检查点练习

练习一

任务:写一个函数 multiply,接收两个参数,返回它们的乘积。如果只传一个参数,第二个参数默认为 1。

// 你的代码:

参考答案

function multiply(a, b = 1) {
    return a * b;
}
console.log(multiply(3, 4)); // 12
console.log(multiply(5));    // 5

Part 2: 函数的多种写法

JavaScript 不止一种写函数的方式,掌握常见的几种看别人的代码才不会懵

你学会了用 function 声明函数,但打开别人的代码,怎么全是 => 和 const xxx = function()?别慌,它们做的事情一样,只是写法不同。这一节带你认识函数表达式、箭头函数和立即执行函数,搞清楚每种写法的细微差别——尤其是箭头函数里 this 的行为,这是面试高频题。

函数表达式

把函数存进变量,灵活但不会提升

// 函数表达式:把匿名函数赋给变量
const greet = function(name) {
    return `你好, ${name}!`;
};

console.log(greet("张三")); // "你好, 张三!"
常见错误:函数表达式不会提升。如果你在声明之前调用它,会得到 ReferenceError。而 function 声明的函数可以。
修复:养成先声明再调用的习惯,不管哪种写法都适用。

箭头函数

ES6 最受欢迎的语法糖

箭头函数用 => 代替 function,写法更短。

// 一个参数,一行返回
const double = x => x * 2;
console.log(double(5)); // 10

// 多个参数
const add = (a, b) => a + b;

// 多行函数体需要花括号 + return
const area = (w, h) => {
    const result = w * h;
    return result;
};

// 返回对象字面量要加括号
const user = (name, age) => ({ name, age });

箭头 vs 普通

this 指向不同,是最关键的区别

特性普通函数箭头函数
this取决于调用方式继承外层 this
arguments有没有
构造函数可以用 new不能用 new
提升声明会提升不会提升
const obj = {
    name: "张三",
    sayHi: function() {
        console.log(`我是${this.name}`);
    },
    sayHiArrow: () => {
        console.log(`我是${this?.name}`);
    }
};
obj.sayHi();       // "我是张三"
obj.sayHiArrow();  // "我是undefined"

立即执行函数

写完就跑,不留痕迹

IIFE(Immediately Invoked Function Expression):定义后立即调用的函数。常用于创建独立作用域,避免污染全局变量。

(function() {
    let secret = "这段代码立刻执行";
    console.log(secret);
})();

// 带返回值的 IIFE
const counter = (function() {
    let count = 0;
    return {
        next: () => ++count,
        reset: () => { count = 0; }
    };
})();
console.log(counter.next()); // 1
console.log(counter.next()); // 2

Part 3: 作用域与闭包

变量在哪里能被访问?函数能不能"记住"它出生时的环境?

函数会写了,但有一个经典面试题你可能答不上来:循环里用 var 声明的变量,为什么循环结束后的值和预期不一样?答案藏在"作用域"里。这一节从作用域的三层结构讲起,一路讲到闭包——函数能"记住"自己被创建时的环境变量。搞懂它,你的 JS 功力就上了一个台阶。

作用域三兄弟

全局、函数、块级——变量能活多大范围

作用域决定了变量在哪些位置可以被访问。

let global = "全局"; // 哪里都能访问

function outer() {
    let outerVar = "外层";
    function inner() {
        let innerVar = "内层";
        console.log(global);   // 可以
        console.log(outerVar); // 可以
    }
    inner();
    // console.log(innerVar); // ReferenceError!
}
outer();

let 和 const 有块级作用域({} 内有效),var 只有函数作用域。现在写代码基本用 let/const,忘掉 var。

作用域链

变量查找的"向上追溯"机制

当你在函数中访问一个变量时,JS 引擎先在当前作用域找,找不到就去外层找,一直找到全局。这条查找路径就叫作用域链。

图:作用域链的变量查找过程
let global = "全局";
function outer() {
    let name = "外层";
    function inner() {
        let age = 20;
        console.log(name);   // "外层"——从外层找到的
        console.log(global); // "全局"——从全局找到的
    }
    inner();
    // console.log(age); // ReferenceError! 外层访问不到内层
}
outer();

作用域链是单向的:内层可以访问外层,外层不能访问内层。就像你能看到窗外的风景,但窗外的人看不到你屋内。

闭包初体验

函数"记住"了它出生时的环境

闭包:函数能够记住并访问它定义时的作用域,即使在外部执行。

用人话说:就像你带着家乡的口音去了外地。虽然人已经不在家乡了,但口音(变量)还在。

这个比喻只是为了帮你建立直观感受。在计算机内部,闭包实际上是函数对象额外保存了一个指向其词法环境的引用,引擎通过这个引用找到外层变量。

function createCounter() {
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// count 变量"活着",不会被回收

闭包实战用法

工厂函数、私有变量——闭包的经典场景

// 工厂函数:批量生成"定制版"函数
function createMultiplier(factor) {
    return (number) => number * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15
// 私有变量:外部无法直接访问 age
function createPerson(name) {
    let age = 0; // 外部访问不到
    return {
        getName: () => name,
        getAge:  () => age,
        setAge: (n) => { if (n > 0) age = n; }
    };
}

const p = createPerson("张三");
p.setAge(25);
console.log(p.getAge()); // 25

高阶函数

接收函数或返回函数的函数

高阶函数是指接收函数作为参数,或者返回一个函数的函数。你其实已经在用它们了——createCounter 返回函数,processArray 接收函数。

// 接收函数作为参数
function operate(a, b, fn) {
    return fn(a, b);
}
console.log(operate(3, 5, (x, y) => x + y)); // 8
console.log(operate(3, 5, (x, y) => x * y)); // 15

// 返回函数——前面学的闭包就是典型的高阶函数
function createValidator(min, max) {
    return (value) => value >= min && value <= max;
}
const isAge = createValidator(0, 150);
console.log(isAge(25));  // true
console.log(isAge(-1));  // false

// 数组的高阶方法(后面章节会详细学)
[1, 2, 3].map(n => n * 2);        // [2, 4, 6]
[1, 2, 3].filter(n => n > 1);     // [2, 3]

高阶函数是函数式编程的核心概念。掌握它,你会发现代码可以写得更灵活、更简洁。

检查点练习

练习二

任务:用闭包写一个 createGreeter 函数。它接收一个问候语(如"你好"),返回一个新函数。新函数接收名字,输出"你好, 张三!"这样的结果。

// 你的代码:

参考答案

function createGreeter(greeting) {
    return (name) => `${greeting}, ${name}!`;
}

const sayHello = createGreeter("你好");
console.log(sayHello("张三")); // "你好, 张三!"

综合练习

练习三:配置生成器

任务:用闭包写一个 createConfig 函数。它接收一个默认配置对象,返回一个新函数。新函数接收用户配置,合并后返回最终配置(用户配置覆盖默认值)。

const getConfig = createConfig({ theme: "light", lang: "zh" });
console.log(getConfig({ theme: "dark" }));
// { theme: "dark", lang: "zh" }
console.log(getConfig({ lang: "en", debug: true }));
// { theme: "light", lang: "en", debug: true }

提示:用展开运算符 { ...a, ...b } 合并对象,b 的属性会覆盖 a 的同名属性。

参考答案

function createConfig(defaults) {
    return (userConfig) => ({ ...defaults, ...userConfig });
}
const getConfig = createConfig({ theme: "light", lang: "zh" });
console.log(getConfig({ theme: "dark" }));
// { theme: "dark", lang: "zh" }

练习四:函数管道

任务:写一个 pipe 函数,接收多个函数作为参数,返回一个新函数。新函数会依次调用所有传入的函数,前一个的输出作为后一个的输入。

const transform = pipe(
    (s) => s.trim(),
    (s) => s.toLowerCase(),
    (s) => s.replace(/\s+/g, "-")
);
console.log(transform("  Hello  World  "));
// "hello-world"

提示:用 for...of 循环依次调用每个函数,把上一次的结果传给下一次。

参考答案

function pipe(...fns) {
    return function(input) {
        let result = input;
        for (let fn of fns) {
            result = fn(result);
        }
        return result;
    };
}
const transform = pipe(
    (s) => s.trim(),
    (s) => s.toLowerCase(),
    (s) => s.replace(/\s+/g, "-")
);
console.log(transform("  Hello  World  ")); // "hello-world"

常见错误诊断

症状一:调用函数时返回 undefined,但代码里明明有逻辑。
原因:函数没有 return,或者 return 写在了 if 分支里但没覆盖所有情况。
修复:检查每个分支是否都有 return,或在函数末尾加一个兜底的 return。
症状二:TypeError: xxx is not a function
原因:用函数表达式声明,但在声明之前调用了它。
修复:把调用移到声明之后,或者改用 function 声明。
症状三:箭头函数里的 this 是 undefined
原因:箭头函数没有自己的 this,它继承外层的 this。如果外层是模块或全局,this 就是 undefined。
修复:需要用 this 的地方,改用普通函数。

📝 要点总结

本章核心知识点

函数定义

声明、表达式、箭头函数三种方式

参数处理

默认参数、剩余参数、解构参数

作用域链

变量从内向外查找,形成作用域链

闭包

函数记住并访问外部变量的能力

高阶函数

函数作为参数或返回值,实现抽象和复用

下一章

函数学会了,接下来该学对象——用键值对组织你的数据,让信息不再散乱。

1 / X