JavaScript函数完全指南:从基础语法到闭包原理与应用实战

## 📚 前言

函数是 JavaScript 的核心,也是前端开发中最常用的概念之一。掌握函数不仅能让你的代码更加模块化、可维护,更是理解闭包、异步编程、高阶函数等进阶概念的基础。

本文将从函数的基本概念开始,深入讲解函数声明、函数表达式、箭头函数、参数与返回值、内置函数、闭包等内容,并结合大量代码示例和实际应用场景,帮助你彻底理解 JavaScript 函数。

一、什么是函数?

1.1 函数的概念

**函数(Function)** 是一段封装好的、可重复使用的代码块,用于执行特定的任务或计算。

你可以把函数想象成一个"加工厂":

```

┌─────────────────────────────────────────┐

│ 函数(加工厂) │

├─────────────────────────────────────────┤

│ 输入(参数) → 处理(函数体) → 输出(返回值)│

│ a, b a + b a+b │

└─────────────────────────────────────────┘

```

1.2函数的四大特性

🔄 代码复用:避免重复编写相同代码。例如,计算圆面积的函数可以多次调用,无需重复编写计算逻辑。

📦 模块化:将复杂任务分解为小模块。例如,将用户注册功能拆分为验证、提交等多个独立函数。

🔒 封装性:隐藏内部实现细节,只暴露必要接口。外部调用者无需了解函数内部如何实现,只需知道如何使用。

🎮 可维护性:便于测试和维护。函数独立性强,修改一个函数不会影响其他代码。

1.3 函数基本结构

复制代码
// 函数声明
function functionName(parameter1, parameter2) {
// 函数体:要执行的代码
return result; // 返回值
}
// 函数调用
let result = functionName(arg1, arg2);

组成部分解析:

部分 说明
function 关键字,声明函数
functionName 函数名称,用于调用
parameter1, parameter2 参数,接收外部传入的值
{ ... } 函数体,执行具体逻辑
return 返回结果,可选

1.4 为什么需要函数?

想象一下,如果没有函数,你的代码会变成这样:

复制代码
// 计算3个圆的面积 - 重复代码!
let radius1 = 5;
let area1 = Math.PI * radius1 * radius1;
console.log('圆1面积:', area1);
let radius2 = 10;
let area2 = Math.PI * radius2 * radius2;
console.log('圆2面积:', area2);
let radius3 = 7;
let area3 = Math.PI * radius3 * radius3;
console.log('圆3面积:', area3);

使用函数后:

复制代码
function calculateArea(radius) {
    return Math.PI * radius * radius;
}

console.log('圆1面积:', calculateArea(5));
console.log('圆2面积:', calculateArea(10));
console.log('圆3面积:', calculateArea(7));

**差异一目了然!** 代码量减少,逻辑更清晰。

二、函数声明

2.1 语法格式

复制代码
function calculateArea(radius) {
    let area = Math.PI * radius * radius;
    return area.toFixed(2);
}

组成部分解析:

1. function 关键字:用于声明函数,告诉JavaScript这是一个函数定义。

2. functionName(函数名):函数的名称,用于后续调用该函数。

3. parameter1, parameter2(参数):函数接收外部传入值的变量,可以有零个或多个参数。

4. { ... }(函数体):包含函数要执行的具体逻辑代码,用花括号包裹。

5. return(返回值):可选的返回语句,用于将函数执行结果返回给调用者。

2.2 函数提升(Hoisting)

被提升到当前作用域的顶部**,这意味着你可以在声明之前调用函数:

复制代码
// ✅ 可以在声明之前调用
console.log(sayHello()); // 输出: Hello!

function sayHello() {
return 'Hello!';
}

**原理:** JavaScript 引擎在执行代码前,会先扫描所有函数声明,并将它们提升到作用域顶部。

2.3 完整示例:BMI计算器

复制代码
// 计算BMI指数
function calculateBMI(weight, height) {
// 参数验证
if (weight <= 0 || height <= 0) {
return '请输入有效的体重和身高';
}

// BMI公式:体重(kg) / 身高(m)²
let bmi = weight / (height * height);

// 返回保留两位小数的结果
return bmi.toFixed(2);
}

// 使用函数
console.log('BMI(65kg, 1.75m):', calculateBMI(65, 1.75)); // 21.22
console.log('BMI(无效值):', calculateBMI(-50, 0)); // 请输入有效的体重和身高

三、函数表达式

3.1 匿名函数表达式

复制代码
// 将匿名函数赋值给变量
let add = function(a, b) {
return a + b;
};

// 通过变量名调用函数
console.log(add(3, 5)); // 输出: 8
```

3.2 命名函数表达式

复制代码
// 函数有内部名称,但外部通过变量名调用
let multiply = function calc(a, b) {
console.log('calc函数被调用');
return a * b;
};

console.log(multiply(4, 6)); // 输出: 24
// console.log(calc(4, 6)); // ❌ 错误:calc在外部不可访问

命名函数表达式的优点:

  • 函数内部可以通过名称递归调用

  • 调试时栈跟踪显示函数名,更容易定位问题

3.3 函数声明 vs 函数表达式

特性 函数声明 (Function Declaration) 函数表达式 (Function Expression)
语法结构 function fn() { ... } let fn = function() { ... }
提升 (Hoisting) 可以提升 整个函数声明会被提升到当前作用域顶部 不会提升 仅变量声明被提升,赋值操作保留在原位
调用时机 可在代码中定义之前调用 必须在赋值之后才能调用
命名要求 必须有名称 可匿名(也可命名,常用于调试)
典型用途 定义通用的、核心的业务逻辑函数 常用于回调函数、IIFE 或临时函数
作用域 块级作用域(ES6 严格模式/块内) 块级作用域(受 let/const 限制)
核心差异总结

提升机制:函数声明具有完整的提升特性,而函数表达式受限于变量声明的提升规则(仅提升声明,不提升赋值)。
执行上下文:函数声明在代码执行前就已完成初始化,函数表达式在代码执行到赋值语句时才创建函数对象。
调试友好性:命名函数表达式在调用栈中会显示函数名,便于调试;匿名函数则显示为 anonymous。

示例对比:

复制代码
// 函数声明 - 可以提前调用
console.log(funcDeclaration()); // ✅ 输出: declaration

function funcDeclaration() {
return 'declaration';
}

// 函数表达式 - 不能提前调用
console.log(funcExpression()); // ❌ 报错: funcExpression is not defined

let funcExpression = function() {
return 'expression';
};

3.4 函数表达式的应用场景

场景1:回调函数
复制代码
// 代码A 同步代码,从上至下顺序执行
console.log("代码最先执行我");

// 代码B 定时器异步 | ⚠️易错点1:此处书写function(){}仅传递【函数本体】,不会立即执行
setTimeout(function () {
  console.log('1秒后执行');
}, 1000);

// 代码D 同步代码,不受定时器阻塞,优先走完所有同步逻辑
console.log("我排在定时器后面,但比回调先运行");

/*
=====新手高频踩坑提醒=====
❌ 错误写法:给回调函数加(),会当场执行函数,定时器彻底失效
setTimeout(function(){
  console.log('1秒后执行');
}(), 1000);

✅ 原理:()是函数执行符号,只要带上括号,代码运行到本行就立刻调用函数;
定时器需要接收一个函数,等待时间结束后再调用,只传函数名/匿名函数体即可。
*/
场景2:事件处理
复制代码
// 按钮点击事件的回调函数
document.getElementById('btn').addEventListener('click', function() {
console.log('按钮被点击');
});
场景3:条件定义
复制代码
let operation;
let type = 'add';

if (type === 'add') {
operation = function(a, b) { return a + b; };
} else {
operation = function(a, b) { return a - b; };
}

console.log(operation(5, 3)); // 输出: 8

四、箭头函数(ES6)

4.1 语法格式

箭头函数是 ES6 引入的简洁语法,使用 `=>` 定义函数。

复制代码
// 传统函数
let sum1 = function(a, b) {
return a + b;
};

// 箭头函数 - 完整写法
let sum2 = (a, b) => {
return a + b;
};

// 箭头函数 - 简洁写法(单条返回语句可省略{}和return)
let sum3 = (a, b) => a + b;

// 单个参数可省略括号
let double = x => x * 2;

// 无参数需保留括号
let random = () => Math.random();

// 返回对象时需加括号
let createUser = (name, age) => ({ name: name, age: age });

4.2 箭头函数的特点

特点 说明
语法更简洁 省略了 function 关键字;当函数体为单条 return 语句时,可进一步省略 {}return
词法 this 箭头函数没有自己的 this,它的 this 始终指向定义时外层作用域的 this
arguments 对象 函数内部不绑定 arguments 对象(如需获取参数,可使用剩余参数 ...args
super 绑定 箭头函数内部没有自己的 super,同样继承自外层作用域
不可作为构造函数 不能使用 new 关键字实例化,否则会抛出 TypeError
不可用作 Generator 函数体内不能使用 yield 关键字,无法作为生成器函数使用

补充提示

  • 适用场景 :非常适合用于回调函数(如数组的 mapfilterforEach)以及需要绑定外层 this 的场景。
  • 不适用场景 :需要动态 this 的对象方法、原型方法、构造函数以及需要 arguments 对象的函数,仍建议使用传统函数声明。

4.3 箭头函数与传统函数的核心区别:`this` 指向

传统函数的 `this`:

复制代码
const person = {
name: '小明',
greet: function() {
console.log('Hello, ' + this.name); // this指向person对象
}
};
person.greet(); // 输出: Hello, 小明

箭头函数的 `this`:

复制代码
const person = {
name: '小明',
greet: () => {
console.log('Hello, ' + this.name); // this继承外层作用域(可能是全局对象)
}
};
person.greet(); // 输出: Hello, undefined(因为this不指向person)

4.4 箭头函数的适用场景

✅ 适合的场景:

场景1:简短的回调函数
复制代码
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
场景2:数组方法回调
复制代码
const sum = numbers.reduce((acc, cur) => acc + cur, 0); // 6
场景3:处理this绑定问题
复制代码
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // this正确指向Timer实例
console.log(this.seconds);
}, 1000);
}
❌ 不适合的场景:
复制代码
// 场景1:对象方法(this指向错误)
const obj = {
name: 'test',
getName: () => this.name // ❌ this不指向obj
};

// 场景2:构造函数(不能使用new)
const Person = (name) => { // ❌ 不能作为构造函数
this.name = name;
};

// 场景3:需要arguments的函数
const fn = () => { // ❌ 没有arguments
console.log(arguments);
};

五、参数与返回值

5.1 参数的概念

参数(Parameters)** 是函数定义时声明的变量,用于接收外部传入的值。

复制代码
function greet(name) { // name是参数
console.log('Hello, ' + name);
}

greet('小明'); // '小明'是实参

5.2 默认参数(ES6)

复制代码
// 设置默认参数
function greet(name = '朋友', greeting = 'Hello') {
return `${greeting}, ${name}!`;
}

console.log(greet('小明')); // 输出: Hello, 小明!
console.log(greet()); // 输出: Hello, 朋友!   
console.log(greet('小红', 'Hi')); // 输出: Hi, 小红!
//不传参或者传入undefined,才会触发默认值
//传入null,空字符串,0,false都会判断有效传参,覆盖默认值

**默认参数的注意事项:**

复制代码
// 默认参数可以是表达式
function calculateTax(price, taxRate = 0.08) {
return price * taxRate;
}

// 默认参数可以引用前面的参数
function createUrl(protocol = 'https', domain = 'example.com') {
return `${protocol}://${domain}`;
}

5.3 剩余参数(ES6)

剩余参数允许函数接收任意数量的参数,将它们打包成一个数组。

复制代码
function sumAll(...numbers) {
console.log(numbers); // [1, 2, 3, 4]
return numbers.reduce((a, b) => a + b, 0);
}

console.log(sumAll(1, 2, 3, 4)); // 输出: 10

剩余参数 vs arguments:

特性 剩余参数 (...args) arguments 对象
数据类型 真正的 Array 数组实例 类数组对象 (Array-like Object)
数组方法 可直接使用 支持 mapfilterreduce 等所有数组方法 不可直接使用 需先通过 Array.from()slice() 转换为真数组
箭头函数 完全可用 不可用 箭头函数内部没有绑定 arguments 对象
语法声明 显式声明:function fn(...args) {} 隐式存在:函数内部直接访问 arguments
参数范围 仅包含剩余的、未命名的额外参数 包含传递给函数的所有参数
同步机制 始终独立,不受具名参数重新赋值的影响 在非严格模式下,会与具名参数双向同步
复制代码
// 剩余参数(推荐)
function fn1(...args) {
return args.filter(x => x > 0);
}

// arguments(不推荐)
function fn2() {
return Array.from(arguments).filter(x => x > 0);
}

5.4 返回值

返回值(Return Value)** 是函数执行后的输出结果,通过 `return` 语句返回。

复制代码
function add(a, b) {
return a + b; // 返回结果
console.log('这句不会执行'); // return后函数立即结束
}

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

无返回值的函数:

复制代码
function sayHello() {
console.log('Hello!');
// 没有return,默认返回undefined
}

let result = sayHello();
console.log(result); // 输出: undefined

返回多个值:

复制代码
// 通过数组返回多个值
function calculate(a, b) {
return [a + b, a - b, a * b, a / b];
}

let [sum, diff, product, quotient] = calculate(10, 5);
console.log(sum, diff, product, quotient); // 15 5 50 2

六、内置函数

6.1 什么是内置函数

JavaScript 提供了大量 **内置函数(Built-in Functions)**,无需自己定义即可使用。

6.2 常用内置函数分类

6.2.1 全局函数
函数 / 方法 功能说明 代码示例
parseInt() 字符串转整数 解析字符串并返回整数,遇到非数字字符会停止解析 parseInt('123')123 parseInt('10px')10
parseFloat() 字符串转浮点数 解析字符串并返回带小数的浮点数 parseFloat('3.14')3.14 parseFloat('3.14em')3.14
isNaN() 判断是否为 NaN 检测传入的值是否为"非数值"(Not a Number) isNaN(NaN)true isNaN('hello')true
isFinite() 判断是否为有限数 检测传入的值是否为有限的数字 isFinite(10)true isFinite(Infinity)false
prompt() 弹出输入框 在浏览器中弹出一个包含文本框的对话框,等待用户输入 prompt('请输入姓名') (返回用户输入的字符串或 null)
alert() 弹出提示框 在浏览器中弹出一个包含指定消息的警告框 alert('操作成功') (无返回值)
confirm() 弹出确认框 在浏览器中弹出一个包含"确定"和"取消"按钮的对话框 confirm('确定删除吗?') (点击确定返回 true,取消返回 false)
console.log() 控制台输出 将信息输出到浏览器的开发者控制台,常用于代码调试 console.log('调试信息') console.log({name: 'Tom'})
核心开发建议
  • 类型转换 :在进行数值计算前,强烈建议使用 parseInt()parseFloat() 对用户输入或接口返回的字符串进行显式转换,避免隐式转换带来的意外结果。
  • 浏览器 API 慎用alert()prompt()confirm() 属于阻塞式的浏览器原生弹窗,会暂停页面所有交互。在现代前端开发中,通常仅用于简单的本地测试,实际业务中建议使用 UI 组件库(如 Element UI、Ant Design)提供的 Modal 组件来替代。
6.2.2 Math 对象方法
方法 功能说明 代码示例
Math.PI 圆周率常量 返回圆的周长与直径的比值(约等于 3.14159) Math.PI3.141592653589793
Math.abs() 绝对值 返回一个数的绝对值(即非负值) Math.abs(-5)5
Math.max() 最大值 返回一组数中的最大值 Math.max(1, 2, 3)3
Math.min() 最小值 返回一组数中的最小值 Math.min(1, 2, 3)1
Math.pow() 幂运算 返回底数的指定次幂 Math.pow(2, 10)1024
Math.sqrt() 平方根 返回一个数的非负平方根 Math.sqrt(16)4
Math.random() 随机数 返回一个大于等于 0 且小于 1 的伪随机浮点数 Math.random()0.12345...
Math.round() 四舍五入 返回最接近参数的整数 Math.round(3.7)4
Math.floor() 向下取整 返回小于或等于参数的最大整数 Math.floor(3.7)3
Math.ceil() 向上取整 返回大于或等于参数的最小整数 Math.ceil(3.2)4
💡 核心开发建议

随机整数生成公式:获取 min, max 范围内的随机整数,推荐使用以下公式:

复制代码
Math.floor(Math.random() * (max - min + 1)) + min;
  • ES6+ 替代方案 :对于幂运算,现代 JS 中更推荐使用指数运算符 **,例如 2 ** 10 等同于 Math.pow(2, 10)

  • 注意精度问题 :JavaScript 使用 IEEE 754 双精度浮点数,涉及金融计算时,Math 方法的精度可能不足,建议使用专门的库(如 decimal.js)。

6.2.3 数组内置方法
方法 功能说明 代码示例
push() 末尾添加 向数组的末尾添加一个或多个元素,并返回新的长度 arr.push(4)
pop() 末尾删除 从数组的末尾移除最后一个元素,并返回该元素 arr.pop()
shift() 开头删除 从数组的开头移除第一个元素,并返回该元素 arr.shift()
unshift() 开头添加 向数组的开头添加一个或多个元素,并返回新的长度 arr.unshift(0)
map() 映射转换 创建一个新数组,其结果是该数组中的每个元素都调用一次提供的函数后的返回值 arr.map(x => x * 2)
filter() 过滤筛选 创建一个新数组,包含通过所提供函数实现的测试的所有元素 arr.filter(x => x > 0)
reduce() 累加聚合 对数组中的每个元素执行一个由您提供的 reducer 函数,将其结果汇总为单个返回值 arr.reduce((a, b) => a + b, 0)
forEach() 遍历执行 对数组的每个元素执行一次给定的函数(无返回值) arr.forEach(x => console.log(x))
find() 查找元素 返回数组中满足提供的测试函数的第一个元素的值,否则返回 undefined arr.find(x => x.id === 1)
includes() 判断包含 用来判断一个数组是否包含一个指定的值,返回 true 或 false arr.includes(5)
💡 核心开发建议
  • 改变原数组 vs 不改变原数组:在使用数组方法时,务必区分哪些会修改原数组。

    • 会修改原数组push, pop, shift, unshift, splice, sort, reverse
    • 不会修改原数组(返回新数组)map, filter, concat, slice, find。在 React/Vue 等现代框架开发中,推荐使用不修改原数组的方法以保持数据不可变性。
  • 链式调用mapfilterreduce 等方法返回的都是数组,因此支持链式调用,可以写出非常优雅的数据处理逻辑:

    const result = arr
    .filter(item => item.active) // 1. 筛选活跃项
    .map(item => item.name) // 2. 提取名字
    .join(', '); // 3. 拼接字符串

6.2.4 字符串内置方法
方法 功能说明 代码示例
length 获取长度 返回字符串的长度(字符个数) 'hello'.length5
toUpperCase() 转大写 将字符串转换为大写形式 'abc'.toUpperCase()'ABC'
toLowerCase() 转小写 将字符串转换为小写形式 'ABC'.toLowerCase()'abc'
substring() 截取子串 提取字符串中介于两个指定下标之间的字符 'hello'.substring(1, 3)'el'
split() 分割为数组 使用指定的分隔符将字符串分割成字符串数组 'a,b,c'.split(',')['a', 'b', 'c']
indexOf() 查找索引 返回指定值在字符串中首次出现的位置,未找到返回 -1 'hello'.indexOf('e')1
includes() 判断包含 判断字符串是否包含指定的子串,返回布尔值 'hello'.includes('ll')true
replace() 替换内容 替换字符串中匹配的内容(默认只替换第一个匹配项) 'hello'.replace('l', 'x')'hexlo'
💡 核心开发建议
  • 字符串不可变性 :JavaScript 中的字符串是原始值,具有不可变性。所有的字符串方法(如 toUpperCasereplace)都不会改变原字符串,而是返回一个新的字符串。如果需要保留结果,必须用变量接收:

    复制代码
    1let str = 'hello';
    2str.toUpperCase(); // 原字符串不变,仍为 'hello'
    3str = str.toUpperCase(); // 必须重新赋值,str 变为 'HELLO'
  • 全局替换replace() 方法默认只替换第一个匹配项。如果需要替换所有匹配项,可以使用正则表达式配合全局标志 /g,或者在 ES2021 中使用 replaceAll() 方法:

    复制代码
    1'hello'.replace(/l/g, 'x'); // 'hexxo'
    2'hello'.replaceAll('l', 'x'); // 'hexxo' (ES2021+)
  • 截取方法辨析substring(start, end)slice(start, end) 功能类似,但 substring 不支持负数索引(会自动将负数转为 0),而 slice 支持负数索引(从末尾倒数计算)。日常开发中两者均可,但 slice 的灵活性更高。

七、闭包函数(Closure)

7.1 什么是闭包

**闭包(Closure)** 是 JavaScript 中一个非常重要的概念。当一个内部函数引用了外部函数的变量时,就形成了一个闭包。

闭包的核心特性是:**即使外部函数执行完毕,内部函数仍然能够访问外部函数的变量**。

7.2 闭包基本示例:计数器

复制代码
// 外部函数
function createCounter() {
let count = 0; // 外部变量,被闭包捕获

// 内部函数 - 闭包
function increment() {
count++;
return count;
}

return increment; // 返回内部函数
}

// 全局接收内层函数
let counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
执行过程分析:
  1. createCounter() 执行

→ 创建 count 变量,值为 0

→ 返回 increment 函数(但未执行)

→ createCounter 执行完毕,作用域本应销毁

  1. counter() 第一次调用(即 increment())

→ 访问 count 变量

→ count++ → count = 1

→ 返回 1

  1. counter() 第二次调用

→ 访问同一个 count 变量

→ count++ → count = 2

→ 返回 2

7.3 闭包原理

> 💡 当 `createCounter()` 执行完毕后,其作用域本应被销毁。但由于 `increment` 函数引用了 `count` 变量,JavaScript 引擎会保留这个作用域(形成闭包),使 `count` 变量得以存活。

**内存模型:**

┌─────────────────────────────────────────────┐

│ Global Scope │

│ counter ──────────────────────────────────┐│

│ │ ││

│ ▼ ││

│ ┌─────────────────────────────────────┐ ││

│ │ Closure Scope │ ││

│ │ ┌─────────────────────────────┐ │ ││

│ │ │ count = 3 │ │ ││

│ │ └─────────────────────────────┘ │ ││

│ │ ┌─────────────────────────────┐ │ ││

│ │ │ increment() { count++; } │ │ ││

│ │ └─────────────────────────────┘ │ ││

│ └─────────────────────────────────────┘ ││

│ ││

└─────────────────────────────────────────────┘

7.4 闭包应用:私有变量

复制代码
// 使用闭包实现私有变量
function createPerson(name) {
let age = 0; // 私有变量,外部无法直接访问

return {
getName: function() {
return name;
},
getAge: function() {
return age;
},
growUp: function() {
age++;
return age;
}
};
}

let person = createPerson('小明');
console.log(person.getName()); // '小明'
console.log(person.getAge()); // 0
console.log(person.growUp()); // 1
console.log(person.getAge()); // 1
console.log(person.age); // undefined(无法直接访问)

7.5 闭包的四大用途

用途 功能说明 代码示例
🔒 私有变量 隐藏变量,避免全局污染 利用闭包创建外部无法直接访问的私有变量,保障数据安全 通过 IIFE 或工厂函数创建私有变量,外部只能通过暴露的接口(如 getter/setter)进行访问
📦 数据封装 将数据和操作封装在一起 将相关的状态与行为绑定,实现模块化设计 创建模块化的工具函数或对象,将内部状态与外部逻辑隔离
💾 状态保持 函数执行后仍保持变量状态 内部变量在函数执行完毕后依然存活于内存中,不被垃圾回收 实现计数器、数据缓存、防抖/节流等需要记忆上一次状态的场景
🔄 函数工厂 创建具有相似功能的函数 根据传入的参数,动态生成并返回定制化的新函数 根据参数配置,批量创建具有特定行为的函数(如动态生成校验器)

💡 核心开发建议

内存泄漏风险:由于闭包会让内部变量常驻内存,过度或不当使用闭包可能会导致内存消耗过大。当闭包不再需要时,建议手动将其置为 null,以便垃圾回收机制(GC)能够及时释放内存。

适用场景:闭包是 JavaScript 中实现模块化和数据私有化的核心基石。在现代前端框架(如 React Hooks 中的 useState)以及各类设计模式(如单例模式、观察者模式)中,闭包都发挥着不可替代的作用。

7.6 闭包应用场景

**场景1:函数工厂**

复制代码
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}

let double = createMultiplier(2);
let triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

**场景2:缓存函数**

复制代码
function createCache() {
let cache = {};

return function(key, value) {
if (value !== undefined) {
// 设置缓存
cache[key] = value;
return value;
}
// 获取缓存
return cache[key];
};
}

let cache = createCache();
cache('name', '小明');
console.log(cache('name')); // '小明'

**场景3:模块化**

复制代码
const module = (function() {
let privateVar = '私有变量';

function privateMethod() {
return '私有方法';
}

return {
publicVar: '公共变量',
publicMethod: function() {
return privateVar + ' - ' + privateMethod();
}
};
})();

console.log(module.publicVar); // '公共变量'
console.log(module.publicMethod()); // '私有变量 - 私有方法'
console.log(module.privateVar); // undefined

7.7 内置函数 vs 闭包函数

特性 内置函数 (Built-in Functions) 闭包函数 (Closure Functions)
定义 语言提供的现成函数 内部函数引用外部变量形成的机制
来源 JavaScript 引擎预先提供 用户自定义编写
核心 方便开发者直接调用 函数能够访问其外部作用域的变量
本质 官方提供的底层工具函数 作用域链的特殊表现(词法作用域的产物)
关系 任何函数(包括内置函数)都有可能成为闭包 闭包是一种机制,内置函数也可以利用这种机制
💡 核心开发建议
  • 概念区分 :内置函数是 JavaScript 提供给我们的"开箱即用"的工具(如 Math.random()Array.from()),而闭包是一种"机制"或"现象"。这两者并不是互斥的概念,而是从不同维度对函数进行的描述。
  • 内置函数中的闭包 :很多内置的高阶函数(如 mapfiltersetTimeout)在底层实现或与我们配合使用时,都深度依赖闭包机制。例如,当你向 setTimeout 传入一个箭头函数,并让它访问外部的变量时,这个回调函数本质上就是一个闭包。
  • 学习路径:内置函数是 JS 的"基础词汇",掌握它们能让我们高效编码;而闭包是 JS 的"核心语法",理解它能让我们写出更优雅、更安全的模块化代码。

7.8 闭包的注意事项

⚠️ 内存泄漏风险:

复制代码
function createHeavyObject() {
let heavyData = new Array(1000000).fill('data'); // 占用大量内存

return function() {
return heavyData.length; // 引用了heavyData,导致无法被GC回收
};
}

let fn = createHeavyObject();
// fn 持有对 heavyData 的引用,即使不需要了也不会被回收
// 解决方案:不再使用时,置为 null
// fn = null;

⚠️ 循环中的闭包问题:

复制代码
// 经典问题:循环中的闭包
for (var i = 0; i < 3; i++) {     //var 没有块级作用域,变量全局共享
setTimeout(function() {
console.log(i); // 输出: 3, 3, 3(不是期望的0, 1, 2)
}, 100);
}

// 解决方案1:使用let 
for (let i = 0; i < 3; i++) {     ////let有块级作用域,每次循环,产生新的i
setTimeout(function() {
console.log(i); // 输出: 0, 1, 2 
}, 100);
}

// 解决方案2:使用IIFE
for (var i = 0; i < 3; i++) {    
(function(j) {
setTimeout(function() {
console.log(j); // 输出: 0, 1, 2      //
}, 100);
})(i);
}

八、高阶函数

8.1 什么是高阶函数

高阶函数(Higher-Order Function) 是指:

  1. 接收函数作为参数,或者

  2. 返回一个函数

    // 示例1:接收函数作为参数
    function applyOperation(a, b, operation) {
    return operation(a, b);
    }

    let result = applyOperation(5, 3, function(a, b) {
    return a + b;
    });
    console.log(result); // 8

    // 示例2:返回函数
    function createOperation(operator) {
    if (operator === '+') {
    return function(a, b) { return a + b; };
    } else if (operator === '*') {
    return function(a, b) { return a * b; };
    }
    }

    let add = c
    reateOperation('+');
    console.log(add(5, 3)); // 8

8.2 常见高阶函数

map:

复制代码
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function(x) {
return x * 2;
});
console.log(doubled); // [2, 4, 6, 8]

filter:

复制代码
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter(function(x) {
return x % 2 === 0;
});
console.log(evens); // [2, 4, 6]

reduce:

复制代码
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce(function(accumulator, current) {
return accumulator + current;
}, 0);
console.log(sum); // 10

sort:

复制代码
const users = [
{ name: '小明', age: 25 },
{ name: '小红', age: 20 },
{ name: '小刚', age: 30 }
];

users.sort(function(a, b) {
return a.age - b.age; // 按年龄升序排序
});
console.log(users);

九、实战练习

9.1 练习1:数组去重

复制代码
function unique(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
if (!result.includes(arr[i])) {
result.push(arr[i]);
}
}
return result;
}

console.log(unique([1, 2, 2, 3, 3, 3])); // [1, 2, 3]

9.2 练习2:深拷贝

复制代码
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
return obj.map(function(item) {
return deepClone(item);
});
}

let clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}

let original = { a: 1, b: { c: 2 } };
let cloned = deepClone(original);
cloned.b.c = 3;
console.log(original.b.c); // 2(不受影响)

9.3 练习3:防抖函数

复制代码
function debounce(func, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(function() {
func.apply(this, args);
}, delay);
};
}

// 使用示例
let debouncedSearch = debounce(function(keyword) {
console.log('搜索:', keyword);
}, 300);

// 连续输入时,只有停止输入300ms后才执行
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc');
// 最终只输出: 搜索: abc

9.4 练习4:节流函数

复制代码
function throttle(func, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(function() {
inThrottle = false;
}, limit);
}
};
}

// 使用示例
let throttledScroll = throttle(function() {
console.log('滚动事件');
}, 1000);

// 滚动时,每1秒只执行一次
window.addEventListener('scroll', throttledScroll);

9.5 练习5:综合计算器

复制代码
const Calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => {
if (b === 0) {
return new Error('不能除以零');
}
return (a / b).toFixed(2);
},
power: (a, b) => Math.pow(a, b),
sqrt: (a) => {
if (a < 0) {
return new Error('不能对负数开方');
}
return Math.sqrt(a).toFixed(2);
},
percentage: (num, total) => ((num / total) * 100).toFixed(2) + '%'
};
// 使用计算器
console.log('5 + 3 =', Calculator.add(5, 3)); // 8
console.log('10 - 4 =', Calculator.subtract(10, 4)); // 6
console.log('6 × 7 =', Calculator.multiply(6, 7)); // 42
console.log('100 ÷ 4 =', Calculator.divide(100, 4)); // 25.00
console.log('2^10 =', Calculator.power(2, 10)); // 1024
console.log('√16 =', Calculator.sqrt(16)); // 4.00
console.log('50/200 =', Calculator.percentage(50, 200)); // 25.00%

十、常见问题与面试题

10.1 常见问题

**Q1:函数声明和函数表达式有什么区别?**

A:函数声明会被提升,可以在定义前调用;函数表达式不会被提升,必须在定义后调用。

**Q2:箭头函数和普通函数有什么区别?**

A:箭头函数没有自己的 `this`、`arguments`,不能作为构造函数,语法更简洁。

**Q3:什么是闭包?闭包有什么用?**

A:闭包是内部函数引用外部变量形成的机制。用途包括实现私有变量、数据封装、状态保持、函数工厂。

**Q4:闭包会导致内存泄漏吗?**

A:如果闭包持有对大量数据的引用且不再使用,可能导致内存泄漏。解决方案是及时释放引用。

**Q5:什么是高阶函数?**

A:接收函数作为参数或返回函数的函数。

10.2 面试题

**面试题1:写出一个闭包示例,并解释其原理。

复制代码
function createCounter() {
let count = 0;
return function() {
return ++count;
};
}
let counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

解析:`createCounter` 返回内部函数,该函数引用了外部变量 `count`。即使 `createCounter` 执行完毕,`count` 仍被内部函数引用,形成闭包,变量得以存活。

面试题2:解释 var 和 let 在循环中的区别。

复制代码
// var声明的变量存在循环闭包问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出: 3, 3, 3
}

// let声明的变量每个迭代都有独立的作用域
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 输出: 0, 1, 2
}

面试题3:实现一个防抖函数。

参考本章实战练习3。

面试题4:什么是柯里化?实现一个柯里化函数。

复制代码
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}

function add(a, b, c) {
return a + b + c;
}

let curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6

## 📝 总结

知识点梳理

知识点 核心重点
函数声明 使用 function 关键字定义,具有提升(Hoisting)特性,可在代码中定义之前调用
函数表达式 将函数赋值给变量,不会整体提升,必须在赋值之后才能调用
箭头函数 ES6 引入的简洁语法,没有自己的 this,始终继承外层作用域的 this
参数与返回值 支持默认参数、剩余参数 (...args) 以及通过解构赋值实现多值返回
内置函数 掌握全局函数、Math 对象、数组方法(如 mapfilter)及字符串方法的使用
闭包 内部函数能够访问外部变量,常用于实现私有变量、数据封装与状态保持
高阶函数 能够接收函数作为参数或返回一个新函数的函数,是函数式编程的基础
💡 核心开发建议
  • 知识体系构建:以上七个知识点构成了 JavaScript 函数编程的完整闭环。从基础的函数声明与表达式,到进阶的箭头函数与参数处理,再到高阶的闭包与高阶函数,它们是现代前端开发(如 React/Vue 框架)的基石。
  • 实践重于理论:建议在掌握这些概念后,通过实际项目(如手写防抖节流、封装模块化组件、处理复杂数据流)来加深理解,尤其是闭包和高阶函数的应用。

至此,我们前面探讨的所有表格(函数特性、箭头函数、参数对比、全局函数、Math方法、数组方法、字符串方法、闭包用途、内置vs闭包、知识点总结)都已经全部梳理完毕。

学习路径

函数基础 → 函数声明/表达式 → 箭头函数 → 参数与返回值

内置函数 → 闭包 → 高阶函数 → 实战应用

**如果觉得本文对你有帮助,请点赞、收藏、关注三连支持一下!感谢阅读!** 🎉

**欢迎在评论区留言讨论,共同进步!** 💬

相关推荐
KobeSacre2 小时前
DelayQueue 源码
java·开发语言
finyouIT2 小时前
锚点实现了点击导航平滑滚动到页面指定位置的三种方法
javascript·css
han_hanker2 小时前
重新认识枚举enum
开发语言
鹏易灵3 小时前
C++——8.移动语义初探(移动构造、移动赋值)
开发语言·c++·php
2601_962341303 小时前
计算机毕业设计之jsp考研在线复习平台
java·大数据·开发语言·hadoop·python·考研·课程设计
凯瑟琳.奥古斯特3 小时前
力扣1008:前序重建BST
开发语言·c++·算法·leetcode·职场和发展
2zcode3 小时前
项目文档:基于MATLAB图像处理的饮料瓶灌装液位检测系统设计与实现
开发语言·图像处理·matlab
月疯3 小时前
np.where()[0]的用法
开发语言·python·numpy
z落落4 小时前
C#五大加密算法(AES、DES、MD5、RSA、SHA)
开发语言·c#