JavaScript学习笔记:5.函数

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

六、函数实战避坑总结

  1. 函数定义:需要提前调用用“函数声明”,临时使用用“函数表达式”。
  2. 作用域:变量访问遵循“就近原则”,嵌套函数能访问外层变量。
  3. 闭包:只在需要“保存状态”或“封装私有变量”时使用,避免内存泄漏。
  4. 参数:优先用“默认参数+剩余参数”,替代arguments和手动判断undefined
  5. 箭头函数:适合回调函数(如定时器、数组方法),不适合构造函数和对象方法。
  6. 递归:必须有终止条件,复杂递归优先用循环替代。

七、最后:函数的“效率秘籍”

  • 复用逻辑优先封装成函数:避免重复代码,提高可读性和维护性。
  • 函数职责单一:一个函数只做一件事(比如“求和”就只求和,不做排序、过滤等其他操作)。
  • 函数名要“见名知意”:比如计算平方而不是fn1验证密码而不是check

函数是JS的核心,掌握了函数的定义、作用域、闭包、箭头函数等知识点,就能从“会写JS”升级到“写好JS”。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿蒙Armon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值