JavaScript学习笔记:5.函数
上一篇咱们解锁了JS的“重复干活技能”(循环与迭代),这一篇来攻克JS的核心组件——函数。如果说变量是JS的“砖瓦”,循环是“重复施工工具”,那函数就是“预制构件厂”:把常用逻辑封装起来,需要时直接调用,不用重复写一堆代码。
函数不仅能让代码更简洁,还藏着JS的核心特性——闭包、this绑定、箭头函数等,这些知识点既是面试高频考点,也是实际开发中“少踩坑”的关键。今天就用“生活化比喻+实战避坑”的方式,带你吃透函数的方方面面,从此写出高复用、高可读性的代码~
一、函数的本质:把“重复逻辑”装进“工具箱”
函数的核心作用就两件事:代码复用(写一次用多次)和逻辑封装(把复杂逻辑藏起来,只暴露简单接口)。比如“计算平方”这个逻辑,写个函数封装后,不管是计算3的平方还是10的平方,直接调用就行,不用重复写n*n。
1. 函数的两种“出生方式”:声明vs表达式
JS里定义函数有两种核心方式,就像“正式员工”和“临时工”,各有不同的“入职规则”。
(1)函数声明:有“提升特权”的正式员工
函数声明是最传统的定义方式,用function关键字开头,自带“函数提升”特权——可以在声明之前调用(就像正式员工提前到岗干活)。语法:
// 函数声明:function + 函数名 + 参数 + 函数体
function 计算平方(数字) {
return 数字 * 数字;
}
// 可以在声明前调用(提升特权)
console.log(计算平方(5)); // 25(正常执行,不报错)
(2)函数表达式:无“提升特权”的临时工
函数表达式是把函数赋值给变量,分“匿名”和“命名”两种,没有提升特权——必须先定义再调用(临时工得先入职才能干活)。语法:
// 匿名函数表达式(无函数名)
const 计算平方 = function (数字) {
return 数字 * 数字;
};
// 命名函数表达式(有函数名,方便调试)
const 计算阶乘 = function 阶乘(n) {
return n < 2 ? 1 : n * 阶乘(n - 1);
};
// 不能在声明前调用(会报错)
console.log(计算平方(5)); // 25(定义后调用,正常)
console.log(计算阶乘(3)); // 6(命名表达式的函数名只能在内部使用)
核心坑:函数提升的“差异陷阱”
新手最容易栽在“提升”上:函数声明会被完整提升到作用域顶部,而函数表达式(尤其是用let/const声明的)不会提升,提前调用会报错:
// 正面例子:函数声明可以提前调用
console.log(加一(3)); // 4(正常)
function 加一(n) {
return n + 1;
}
// 反面例子:函数表达式提前调用报错
console.log(减一(3)); // ReferenceError: 减一 is not defined
const 减一 = function (n) {
return n - 1;
};
怎么选?
- 优先用函数声明:如果函数逻辑独立,且需要在多处调用,用声明(可读性高,支持提前调用)。
- 用函数表达式:如果函数是临时使用(比如作为参数传递给其他函数),或需要根据条件定义函数,用表达式。
2. 函数的“干活流程”:参数→执行→返回值
函数就像一个“加工机器”:接收输入(参数),经过内部处理(函数体),输出结果(返回值)。
(1)参数:函数的“原材料”
参数分“形参”(函数定义时的占位符)和“实参”(函数调用时的实际值)。JS的参数传递有个关键规则:
- 基本类型(数字、字符串、布尔):按值传递,函数内修改不会影响外部。
- 引用类型(对象、数组):按引用传递,函数内修改对象/数组的属性/元素,会影响外部。
// 基本类型:按值传递(不影响外部)
function 修改数字(n) {
n = 10; // 只修改函数内的副本
}
let a = 5;
修改数字(a);
console.log(a); // 5(外部变量没变化)
// 引用类型:按引用传递(影响外部)
function 修改对象( obj) {
obj.姓名 = "李四"; // 修改的是对象的引用地址
}
let 张三 = { 姓名: "张三" };
修改对象(张三);
console.log(张三.姓名); // 李四(外部对象被修改)
(2)返回值:函数的“加工成果”
用return语句返回结果,return后面的代码不会执行(相当于“下班信号”)。如果没有return,函数默认返回undefined。
function 加乘( a, b) {
return a + b; // 返回结果,后面的代码不执行
console.log("这句话永远不会执行");
}
console.log(加乘(2, 3)); // 5(返回结果)
console.log(加乘(2)); // NaN(b默认是undefined,2+undefined=NaN)
二、函数的“生存空间”:作用域与闭包
作用域决定了变量的“访问权限”,而闭包是JS的“黑魔法”——让函数能“记住”自己的出生环境,即使离开也能访问外部变量。这部分是JS的核心难点,也是面试必问。
1. 函数作用域:变量的“专属领地”
函数内定义的变量是“局部变量”,只能在函数内访问;函数外定义的变量是“全局变量”,函数内可以访问。嵌套函数还能访问外层函数的变量(作用域链)。
// 全局变量:整个脚本都能访问
const 全局变量 = "我是全局的";
function 外层函数() {
// 外层局部变量:外层和内层都能访问
const 外层变量 = "我是外层的";
function 内层函数() {
// 内层局部变量:只有内层能访问
const 内层变量 = "我是内层的";
console.log(全局变量); // 可以访问(作用域链向上查找)
console.log(外层变量); // 可以访问
console.log(内层变量); // 可以访问
}
内层函数();
console.log(内层变量); // ReferenceError: 内层变量 is not defined(外层不能访问内层)
}
外层函数();
关键规则:作用域链
变量访问遵循“就近原则”:先找自己的作用域,找不到就向上找外层作用域,直到全局作用域。如果全局也没有,就报错ReferenceError。
2. 闭包:带“记忆功能”的函数
闭包的本质是“嵌套函数+外层函数的作用域”——内层函数被返回到外层函数之外调用时,依然能访问外层函数的变量。就像你离开家时,把家门钥匙带在了身上,即使不在家,也能打开家门。
闭包的经典用法:保存状态(计数器例子)
// 外层函数:创建计数器的“环境”
function 创建计数器() {
let 计数 = 0; // 外层变量,被闭包记住
// 内层函数:操作计数,形成闭包
return function () {
计数++;
return 计数;
};
}
// 创建两个独立的计数器(各自记住自己的计数)
const 计数器1 = 创建计数器();
const 计数器2 = 创建计数器();
console.log(计数器1()); // 1
console.log(计数器1()); // 2(记住了上一次的计数)
console.log(计数器2()); // 1(独立计数,不影响)
闭包的另一个用法:封装私有变量
JS没有原生的“私有变量”,但可以用闭包模拟——让变量只能通过特定方法访问,不能直接修改,保证数据安全。
function 创建用户(姓名) {
let 密码 = "123456"; // 私有变量,外部无法直接访问
return {
getName() {
return 姓名; // 暴露“读姓名”的方法
},
修改密码(旧密码, 新密码) {
// 暴露“改密码”的方法,带验证逻辑
if (旧密码 === 密码) {
密码 = 新密码;
return "密码修改成功";
}
return "旧密码错误";
},
};
}
const 用户 = 创建用户("张三");
console.log(用户.getName()); // 张三(可以访问)
console.log(用户.密码); // undefined(无法直接访问私有变量)
console.log(用户.修改密码("123456", "654321")); // 密码修改成功
闭包的坑:内存泄漏
闭包会让外层函数的变量一直存在于内存中(不会被垃圾回收),如果滥用闭包(比如大量创建闭包且不释放),会导致内存泄漏,让页面卡顿。
避坑指南:
- 只在需要“保存状态”或“封装私有变量”时使用闭包。
- 不需要时,手动解除闭包引用(比如
计数器1 = null),让垃圾回收机制回收变量。
三、函数的“高级参数玩法”:默认参数、剩余参数与arguments
参数是函数的“原材料入口”,JS提供了多种灵活的参数处理方式,让函数能应对不同的输入场景。
1. 默认参数:给“原材料”设个默认值
以前如果函数参数没传,默认是undefined,需要手动判断赋值。ES6的默认参数可以直接在定义时给参数设默认值,简洁又优雅。
// 以前的写法:手动判断undefined
function 乘法(a, b) {
b = typeof b !== "undefined" ? b : 1; // 没传b就默认1
return a * b;
}
// ES6默认参数:直接设默认值
function 乘法(a, b = 1) {
return a * b;
}
console.log(乘法(5)); // 5(b默认是1)
console.log(乘法(5, 3)); // 15(传了b就用传入的值)
避坑点:默认参数的“暂时性死区”
默认参数的作用域是独立的,不能访问后面的参数,否则会报错:
// 反面例子:默认参数访问后面的参数,报错
function 错误例子(a = b, b = 1) {
return a + b;
}
console.log(错误例子()); // ReferenceError: Cannot access 'b' before initialization
// 正面例子:后面的参数可以访问前面的参数
function 正确例子(a = 1, b = a) {
return a + b;
}
console.log(正确例子()); // 2(a=1,b=a=1)
2. 剩余参数:接收“不确定数量”的原材料
如果函数的参数数量不确定,以前要用arguments对象处理,现在用剩余参数(...变量名)更简洁,还能直接当成数组使用。
// 剩余参数:接收所有传入的参数,变成数组
function 求和(...数字们) {
return 数字们.reduce((总和, 数字) => 总和 + 数字, 0);
}
console.log(求和(1, 2)); // 3
console.log(求和(1, 2, 3, 4)); // 10(不管传多少个参数都能处理)
剩余参数vs arguments
arguments是函数内的内置对象,也能获取所有参数,但有两个缺点:
- 是“类数组”,不是真正的数组,需要
Array.from(arguments)转换才能用数组方法。 - 箭头函数没有
arguments对象。
剩余参数直接是数组,支持所有数组方法,且箭头函数也能使用,推荐优先用剩余参数。
3. arguments对象:老派的“参数容器”
arguments是函数内的内置对象,存储了所有传入的实参,适合老项目兼容或需要动态处理参数的场景。
function 连接字符串(分隔符) {
let 结果 = "";
// arguments[0]是分隔符,从arguments[1]开始是要连接的字符串
for (let i = 1; i < arguments.length; i++) {
结果 += arguments[i] + 分隔符;
}
return 结果;
}
console.log(连接字符串("、", "红", "橙", "黄")); // 红、橙、黄、
注意:箭头函数没有arguments对象,如果需要获取所有参数,只能用剩余参数。
四、箭头函数:ES6的“简化版函数”
ES6新增的箭头函数(() => {})是函数表达式的“简化语法”,写法更简洁,还解决了传统函数的this绑定问题,是开发中的“高频工具”。
1. 箭头函数的“简化语法”
箭头函数的语法可以根据场景简化,越简单的逻辑写起来越爽:
// 1. 单参数+单语句返回:省略括号和return
const 加一 = n => n + 1; // 等价于 function(n) { return n + 1; }
// 2. 多参数+单语句返回:参数加括号
const 求和 = (a, b) => a + b;
// 3. 多语句+返回值:需要大括号和return
const 计算平方和 = (a, b) => {
const 平方A = a * a;
const 平方B = b * b;
return 平方A + 平方B;
};
// 4. 无参数:括号不能省
const 说Hello = () => console.log("Hello");
2. 箭头函数的核心优势:无独立this
传统函数的this绑定很“混乱”——谁调用它,this就指向谁(全局调用指向全局,对象调用指向对象)。而箭头函数没有自己的this,它的this继承自外层执行上下文的this,解决了“this绑定丢失”的经典问题。
经典场景:定时器中的this
// 传统函数:this绑定丢失(指向全局)
function 传统用户() {
this.姓名 = "张三";
this.年龄 = 20;
setInterval(function () {
this.年龄++; // this指向window,不是用户对象
console.log(this.年龄); // NaN(window.年龄不存在)
}, 1000);
}
// 箭头函数:this继承外层(指向用户对象)
function 箭头用户() {
this.姓名 = "张三";
this.年龄 = 20;
setInterval(() => {
this.年龄++; // this指向外层的用户对象
console.log(this.年龄); // 21、22、23...(正确)
}, 1000);
}
const 用户 = new 箭头用户();
3. 箭头函数的“禁忌场景”
箭头函数虽好,但不是万能的,以下场景不能用:
- 不能作为构造函数(不能用
new关键字调用):箭头函数没有prototype,用new会报错。 - 不能作为对象的方法:对象方法中的
this需要指向对象本身,而箭头函数的this继承自外层,会导致错误。 - 需要
arguments对象的场景:箭头函数没有arguments,只能用剩余参数替代。
// 反面例子1:箭头函数作为构造函数(报错)
const Person = () => {};
const p = new Person(); // TypeError: Person is not a constructor
// 反面例子2:箭头函数作为对象方法(this指向错误)
const 对象 = {
姓名: "张三",
说姓名: () => console.log(this.姓名) // this指向全局,不是对象
};
对象.说姓名(); // undefined
五、函数的“其他高级玩法”:递归与预定义函数
除了上面的核心知识点,函数还有两个实用玩法:递归(自己调用自己)和预定义函数(JS内置的现成函数)。
1. 递归:函数的“自我调用”
递归是函数调用自身的写法,适合解决“分治问题”(比如遍历树结构、计算阶乘、斐波那契数列),逻辑比循环更简洁。
经典例子:计算阶乘(n! = n × (n-1) × … × 1)
function 阶乘(n) {
// 终止条件:n=0或1时返回1(避免无限递归)
if (n === 0 || n === 1) {
return 1;
}
// 递归调用:n × 阶乘(n-1)
return n * 阶乘(n - 1);
}
console.log(阶乘(5)); // 120(5×4×3×2×1)
递归的坑:无限递归与栈溢出
递归必须有“终止条件”,否则会陷入无限递归,导致栈溢出(浏览器报错Maximum call stack size exceeded)。
避坑指南:
- 每次递归调用时,参数必须“靠近”终止条件(比如
n-1)。 - 复杂递归可以用“尾递归优化”(函数最后一句是递归调用,无其他计算),但JS对尾递归优化支持有限,大额递归建议用循环替代。
2. 预定义函数:JS的“现成工具”
JS内置了很多预定义函数(全局函数),不用自己写,直接调用就能实现常见功能:
parseInt(str, 进制):字符串转整数(必须指定进制,避免坑)。parseFloat(str):字符串转浮点数。isNaN(value):判断是否是NaN(注意:NaN !== NaN,不能直接用===判断)。encodeURI(url)/decodeURI(url):编码/解码URL(不编码特殊字符如&)。encodeURIComponent(url)/decodeURIComponent(url):编码/解码URL组件(编码所有特殊字符)。
// 常用预定义函数示例
console.log(parseInt("101", 2)); // 5(二进制转十进制)
console.log(parseFloat("3.14abc")); // 3.14(忽略后面的非数字字符)
console.log(isNaN(NaN)); // true(判断NaN的正确方式)
console.log(encodeURIComponent("https://www.baidu.com?name=张三"));
// 编码后:https%3A%2F%2Fwww.baidu.com%3Fname%3D%E5%BC%A0%E4%B8%89
六、函数实战避坑总结
- 函数定义:需要提前调用用“函数声明”,临时使用用“函数表达式”。
- 作用域:变量访问遵循“就近原则”,嵌套函数能访问外层变量。
- 闭包:只在需要“保存状态”或“封装私有变量”时使用,避免内存泄漏。
- 参数:优先用“默认参数+剩余参数”,替代
arguments和手动判断undefined。 - 箭头函数:适合回调函数(如定时器、数组方法),不适合构造函数和对象方法。
- 递归:必须有终止条件,复杂递归优先用循环替代。
七、最后:函数的“效率秘籍”
- 复用逻辑优先封装成函数:避免重复代码,提高可读性和维护性。
- 函数职责单一:一个函数只做一件事(比如“求和”就只求和,不做排序、过滤等其他操作)。
- 函数名要“见名知意”:比如
计算平方而不是fn1,验证密码而不是check。
函数是JS的核心,掌握了函数的定义、作用域、闭包、箭头函数等知识点,就能从“会写JS”升级到“写好JS”。
1654

被折叠的 条评论
为什么被折叠?



