## 📚 前言
函数是 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 关键字,无法作为生成器函数使用 |
补充提示
- 适用场景 :非常适合用于回调函数(如数组的
map、filter、forEach)以及需要绑定外层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) |
| 数组方法 | 可直接使用 支持 map、filter、reduce 等所有数组方法 |
不可直接使用 需先通过 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.PI → 3.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 等现代框架开发中,推荐使用不修改原数组的方法以保持数据不可变性。
- 会修改原数组 :
-
链式调用 :
map、filter、reduce等方法返回的都是数组,因此支持链式调用,可以写出非常优雅的数据处理逻辑:const result = arr
.filter(item => item.active) // 1. 筛选活跃项
.map(item => item.name) // 2. 提取名字
.join(', '); // 3. 拼接字符串
6.2.4 字符串内置方法
| 方法 | 功能说明 | 代码示例 |
|---|---|---|
length |
获取长度 返回字符串的长度(字符个数) | 'hello'.length → 5 |
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 中的字符串是原始值,具有不可变性。所有的字符串方法(如
toUpperCase、replace)都不会改变原字符串,而是返回一个新的字符串。如果需要保留结果,必须用变量接收: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
执行过程分析:
- createCounter() 执行
→ 创建 count 变量,值为 0
→ 返回 increment 函数(但未执行)
→ createCounter 执行完毕,作用域本应销毁
- counter() 第一次调用(即 increment())
→ 访问 count 变量
→ count++ → count = 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()),而闭包是一种"机制"或"现象"。这两者并不是互斥的概念,而是从不同维度对函数进行的描述。 - 内置函数中的闭包 :很多内置的高阶函数(如
map、filter、setTimeout)在底层实现或与我们配合使用时,都深度依赖闭包机制。例如,当你向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:接收函数作为参数
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 对象、数组方法(如 map、filter)及字符串方法的使用 |
| 闭包 | 内部函数能够访问外部变量,常用于实现私有变量、数据封装与状态保持 |
| 高阶函数 | 能够接收函数作为参数或返回一个新函数的函数,是函数式编程的基础 |
💡 核心开发建议
- 知识体系构建:以上七个知识点构成了 JavaScript 函数编程的完整闭环。从基础的函数声明与表达式,到进阶的箭头函数与参数处理,再到高阶的闭包与高阶函数,它们是现代前端开发(如 React/Vue 框架)的基石。
- 实践重于理论:建议在掌握这些概念后,通过实际项目(如手写防抖节流、封装模块化组件、处理复杂数据流)来加深理解,尤其是闭包和高阶函数的应用。
至此,我们前面探讨的所有表格(函数特性、箭头函数、参数对比、全局函数、Math方法、数组方法、字符串方法、闭包用途、内置vs闭包、知识点总结)都已经全部梳理完毕。
学习路径
函数基础 → 函数声明/表达式 → 箭头函数 → 参数与返回值
↓
内置函数 → 闭包 → 高阶函数 → 实战应用
**如果觉得本文对你有帮助,请点赞、收藏、关注三连支持一下!感谢阅读!** 🎉
**欢迎在评论区留言讨论,共同进步!** 💬