后端转全栈学习-Day4-JavaScript 基础-2

学习时间:约 2 小时

前置知识:第3天的变量、类型、流程控制


目录

  1. 函数是什么
  2. 声明函数的三种方式
  3. 参数详解
  4. 返回值
  5. 作用域:变量的"活动范围"
  6. 作用域链:变量的"查找路径"
  7. 闭包:函数记住了自己的"出生环境"
  8. 高阶函数:函数也能当参数和返回值
  9. 箭头函数详解
  10. 常用数组方法(高阶函数实战)
  11. 练习
  12. 常见陷阱

1. 函数是什么

1.1 最简单的理解

函数 = 一段可以反复使用的代码块。

你给它起个名字,需要的时候"叫一声名字"就能执行。

js 复制代码
// 定义一个函数:打招呼
function sayHello() {
  console.log('你好!')
}

// 调用函数:叫一声名字
sayHello()    // 输出:你好!
sayHello()    // 输出:你好!(可以反复调用)
sayHello()    // 输出:你好!

1.2 为什么需要函数

假设你要在 10 个地方都输出"你好":

js 复制代码
// ❌ 没有函数:复制粘贴 10 次
console.log('你好!')
console.log('你好!')
console.log('你好!')
// ... 还有 7 次

// ✅ 有函数:写一次,调用 10 次
function sayHello() {
  console.log('你好!')
}
sayHello()
sayHello()
sayHello()
// ... 还有 7 次

如果要改成"大家好",没有函数你得改 10 处,有函数只改 1 处。

1.3 函数和 Java 方法的对比

java 复制代码
// Java:方法必须写在类里
public class Greeter {
    public void sayHello() {
        System.out.println("你好!");
    }
}
js 复制代码
// JS:函数可以独立存在,不需要类
function sayHello() {
  console.log('你好!')
}
Java JavaScript
方法必须在类里 函数可以独立存在
参数必须声明类型 参数不用声明类型
返回值必须声明类型 返回值不用声明类型
可以重载(同名不同参数) 不能重载,同名会覆盖

2. 声明函数的三种方式

2.1 函数声明(最常用)

js 复制代码
// 语法:function 函数名(参数) { 代码 }
function add(a, b) {
  return a + b
}

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

特点:函数声明会被"提升"到作用域顶部,意思是你可以先调用再声明:

js 复制代码
// ✅ 这样可以运行(虽然不推荐)
sayHello()

function sayHello() {
  console.log('你好!')
}

等同于:

js 复制代码
// JS 引擎会自动把函数声明移到顶部
function sayHello() {
  console.log('你好!')
}
sayHello()

2.2 函数表达式

把一个函数赋值给一个变量:

js 复制代码
// 语法:const 变量名 = function(参数) { 代码 }
const add = function(a, b) {
  return a + b
}

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

特点:不会被提升,必须先声明再调用:

js 复制代码
// ❌ 这样会报错
add(3, 5)    // TypeError: add is not a function

const add = function(a, b) {
  return a + b
}

什么时候用函数表达式?

当你想把函数当作一个"值"来传递的时候(后面高阶函数会讲)。

2.3 箭头函数(ES6,推荐)

箭头函数是函数表达式的简写:

js 复制代码
// 完整写法
const add = function(a, b) {
  return a + b
}

// 箭头函数写法
const add = (a, b) => {
  return a + b
}

// 更简写:只有一行 return,可以省略 {} 和 return
const add = (a, b) => a + b

// 只有一个参数,可以省略 ()
const double = x => x * 2

// 没有参数,必须写 ()
const sayHello = () => console.log('你好!')

箭头函数 vs 普通函数的区别(先记住两点)

  1. 箭头函数没有自己的 this(第7节闭包会讲)
  2. 箭头函数不能当构造函数(不能 new
js 复制代码
// ✅ 推荐:简单逻辑用箭头函数
const double = x => x * 2
const isPositive = n => n > 0
const greet = name => `你好,${name}`

// ✅ 复杂逻辑用普通函数
function calculateTotal(items) {
  let total = 0
  for (const item of items) {
    total += item.price * item.quantity
  }
  return total
}

2.4 三种方式的对比

函数声明 函数表达式 箭头函数
语法 function f() {} const f = function() {} const f = () => {}
提升 ✅ 会提升 ❌ 不会 ❌ 不会
自己的 this ✅ 有 ✅ 有 ❌ 没有(继承外层)
能当构造函数 ✅ 能 ✅ 能 ❌ 不能
推荐场景 通用 传参/赋值 简单逻辑/回调

3. 参数详解

3.1 基本参数

js 复制代码
// 定义:参数就是函数的"输入"
function greet(name) {
  console.log(`你好,${name}!`)
}

// 调用:传入具体的值
greet('小明')     // 你好,小明!
greet('小红')     // 你好,小红!

JS 参数的特点:不声明类型,可以传任意类型

js 复制代码
function printValue(value) {
  console.log(value)
}

printValue(42)          // 数字
printValue('hello')     // 字符串
printValue(true)        // 布尔
printValue([1, 2, 3])   // 数组
printValue({ name: '小明' })  // 对象

参数个数不匹配不会报错

js 复制代码
function add(a, b) {
  return a + b
}

add(1, 2)       // 3(正常)
add(1)          // NaN(b 是 undefined,1 + undefined = NaN)
add(1, 2, 3)    // 3(多余的参数被忽略)
add()           // NaN(两个都是 undefined)

3.2 默认参数(ES6)

给参数设默认值,调用时没传就用默认值:

js 复制代码
// 以前的写法(手动判断)
function greet(name) {
  name = name || '陌生人'    // 如果 name 是 falsy,就用 '陌生人'
  console.log(`你好,${name}!`)
}

// ✅ 现在的写法(默认参数)
function greet(name = '陌生人') {
  console.log(`你好,${name}!`)
}

greet('小明')    // 你好,小明!
greet()          // 你好,陌生人!

多个默认参数

js 复制代码
function createProduct(name, price = 0, stock = 0) {
  return { name, price, stock }
}

createProduct('iPhone', 7999, 100)   // { name: 'iPhone', price: 7999, stock: 100 }
createProduct('iPhone', 7999)        // { name: 'iPhone', price: 7999, stock: 0 }
createProduct('iPhone')              // { name: 'iPhone', price: 0, stock: 0 }
createProduct()                      // { name: undefined, price: 0, stock: 0 }

默认参数可以是表达式

js 复制代码
function createUser(name, role = getDefaultRole()) {
  return { name, role }
}

function getDefaultRole() {
  return '普通用户'
}

3.3 剩余参数(...args)

当你不确定会有多少个参数时,用 ... 把它们收集成一个数组:

js 复制代码
// ...numbers 会把所有参数收集成一个数组
function sum(...numbers) {
  let total = 0
  for (const n of numbers) {
    total += n
  }
  return total
}

sum(1, 2, 3)           // 6
sum(1, 2, 3, 4, 5)     // 15
sum(100)               // 100
sum()                  // 0

前面有命名参数,后面用剩余参数

js 复制代码
function greetEveryone(greeting, ...names) {
  for (const name of names) {
    console.log(`${greeting},${name}!`)
  }
}

greetEveryone('你好', '小明', '小红', '小刚')
// 你好,小明!
// 你好,小红!
// 你好,小刚!

对比 Java 的可变参数

java 复制代码
// Java
public int sum(int... numbers) {
    int total = 0;
    for (int n : numbers) {
        total += n;
    }
    return total;
}
js 复制代码
// JS(几乎一样)
function sum(...numbers) {
  let total = 0
  for (const n of numbers) {
    total += n
  }
  return total
}

3.4 arguments 对象(旧写法,了解即可)

在箭头函数之前,函数内部有一个隐藏的 arguments 对象:

js 复制代码
function oldSum() {
  let total = 0
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i]
  }
  return total
}

oldSum(1, 2, 3)   // 6

不要用 arguments ,用剩余参数 ...args 替代------更清晰、更现代。

3.5 参数解构

直接从对象或数组中提取参数:

js 复制代码
// 普通写法
function printUser(user) {
  console.log(user.name)
  console.log(user.age)
  console.log(user.email)
}

printUser({ name: '小明', age: 25, email: 'xm@example.com' })
js 复制代码
// 解构写法:直接在参数里提取字段
function printUser({ name, age, email }) {
  console.log(name)
  console.log(age)
  console.log(email)
}

printUser({ name: '小明', age: 25, email: 'xm@example.com' })

数组参数解构

js 复制代码
// 普通写法
function printFirst(arr) {
  console.log(arr[0])
  console.log(arr[1])
}

// 解构写法
function printFirst([first, second]) {
  console.log(first)
  console.log(second)
}

printFirst(['苹果', '香蕉', '橙子'])   // 苹果  香蕉

解构 + 默认值

js 复制代码
function createUser({ name = '匿名', age = 0, role = '用户' } = {}) {
  return { name, age, role }
}

createUser({ name: '小明', age: 25 })   // { name: '小明', age: 25, role: '用户' }
createUser({ name: '小红' })            // { name: '小红', age: 0, role: '用户' }
createUser()                             // { name: '匿名', age: 0, role: '用户' }

为什么参数解构重要? 因为 Vue 3 的 definePropsdefineEmits 都大量使用解构。你很快就会在 Vue 代码里见到。


4. 返回值

4.1 return 关键字

js 复制代码
// 函数计算后返回结果
function add(a, b) {
  return a + b     // 把结果返回给调用者
}

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

return 会立即结束函数

js 复制代码
function checkAge(age) {
  if (age < 0) {
    console.log('年龄不能为负')
    return            // 提前结束,后面的代码不会执行
  }
  if (age < 18) {
    console.log('未成年')
    return
  }
  console.log('成年人')
}

checkAge(-1)    // 年龄不能为负
checkAge(15)    // 未成年
checkAge(25)    // 成年人

4.2 没有 return 的函数返回 undefined

js 复制代码
function sayHello(name) {
  console.log(`你好,${name}!`)
  // 没有 return
}

let result = sayHello('小明')
console.log(result)   // undefined

每个函数都有返回值 ------如果你不写 return,JS 就返回 undefined

4.3 返回多个值(用对象或数组)

js 复制代码
// 用对象返回多个值(推荐,有名字)
function getUserInfo() {
  return {
    name: '小明',
    age: 25,
    email: 'xm@example.com'
  }
}

const user = getUserInfo()
console.log(user.name)    // 小明
console.log(user.age)     // 25
js 复制代码
// 用数组返回多个值(适合有序的数据)
function getMinMax(numbers) {
  return [Math.min(...numbers), Math.max(...numbers)]
}

const [min, max] = getMinMax([3, 1, 4, 1, 5, 9])
console.log(min)   // 1
console.log(max)   // 9

4.4 提前 return 的模式(Guard Clause)

用提前 return 减少嵌套,让代码更清晰:

js 复制代码
// ❌ 嵌套太深
function processOrder(order) {
  if (order) {
    if (order.items) {
      if (order.items.length > 0) {
        // 真正的业务逻辑在这里,缩进了 3 层
        console.log('处理订单')
      }
    }
  }
}

// ✅ 提前 return(Guard Clause)
function processOrder(order) {
  if (!order) return
  if (!order.items) return
  if (order.items.length === 0) return

  // 真正的业务逻辑,没有缩进
  console.log('处理订单')
}

5. 作用域:变量的"活动范围"

5.1 什么是作用域

作用域 = 变量可以被访问的范围。

就像公司里的信息权限:

  • 全局作用域 = 全公司公告(所有人能看到)
  • 函数作用域 = 部门内部文件(只有本部门的人能看到)
  • 块级作用域 = 会议室里说的话(只有会议室里的人能听到)

5.2 全局作用域

在函数外面声明的变量,任何地方都能访问:

js 复制代码
// 全局变量
const appName = '电商平台'
let currentUser = '小明'

function greet() {
  console.log(`欢迎来到${appName},${currentUser}!`)
}

function showInfo() {
  console.log(`当前用户:${currentUser}`)
}

greet()        // 欢迎来到电商平台,小明!
showInfo()     // 当前用户:小明

全局变量的缺点:任何函数都能改它,容易出错。

js 复制代码
let count = 0

function add() {
  count++     // 改了全局变量
}

function reset() {
  count = 0   // 也改了全局变量
}

add()
add()
console.log(count)   // 2(被改了)
reset()
console.log(count)   // 0(又被改了)

原则:尽量少用全局变量。 全局变量就像公司的公共会议室------任何人都能进去乱放东西。

5.3 函数作用域

在函数内部声明的变量,只有函数内部能访问:

js 复制代码
function calculateTotal() {
  // price 和 quantity 只在这个函数里有效
  let price = 99
  let quantity = 3
  let total = price * quantity
  console.log(`总价:¥${total}`)
}

calculateTotal()    // 总价:¥297

// ❌ 函数外面访问不到
console.log(price)     // ReferenceError: price is not defined
console.log(quantity)  // ReferenceError: quantity is not defined

函数作用域就像部门内部文件------只有本部门(函数)的人能看,外面的人看不到。

5.4 块级作用域(let 和 const)

{} 包起来的代码块,letconst 声明的变量只在块内有效:

js 复制代码
if (true) {
  let message = '我在 if 块里'
  const count = 42
  console.log(message)   // ✅ 能访问
}

// ❌ 块外面访问不到
console.log(message)   // ReferenceError: message is not defined
console.log(count)     // ReferenceError: count is not defined
js 复制代码
for (let i = 0; i < 3; i++) {
  let inner = '我在循环里'
  console.log(i, inner)
}

// ❌ 循环结束后,i 和 inner 都不存在了
console.log(i)       // ReferenceError: i is not defined
console.log(inner)   // ReferenceError: inner is not defined

对比 var(旧写法)

js 复制代码
// var 没有块级作用域(这就是为什么不要用 var)
if (true) {
  var leaked = '我逃出去了'
}
console.log(leaked)   // '我逃出去了'  ← 居然能访问!

// let 就不会
if (true) {
  let safe = '我被困在里面'
}
console.log(safe)     // ReferenceError: safe is not defined

5.5 三种作用域的总结

作用域 声明位置 访问范围 示例
全局 函数外面 任何地方 const APP = '电商'
函数 函数里面 只在函数内 function f() { let x = 1 }
块级 {} 里面(let/const) 只在块内 if (true) { let x = 1 }
js 复制代码
const global = '全局'

function outer() {
  const funcScope = '函数作用域'

  if (true) {
    const blockScope = '块级作用域'
    console.log(global)       // ✅ 能访问全局
    console.log(funcScope)    // ✅ 能访问函数作用域
    console.log(blockScope)   // ✅ 能访问块级作用域
  }

  console.log(global)       // ✅ 能访问全局
  console.log(funcScope)    // ✅ 能访问函数作用域
  console.log(blockScope)   // ❌ 不能访问块级作用域
}

console.log(global)       // ✅ 能访问全局
console.log(funcScope)    // ❌ 不能访问函数作用域
console.log(blockScope)   // ❌ 不能访问块级作用域

6. 作用域链:变量的"查找路径"

6.1 变量查找规则

当 JS 引擎遇到一个变量时,它会从当前作用域开始往外一层一层找,找到就停,找不到就报错。

js 复制代码
const level = '全局'

function outer() {
  const level = 'outer 函数'

  function inner() {
    const level = 'inner 函数'
    console.log(level)   // 'inner 函数'(先找自己内部的)
  }

  inner()
  console.log(level)     // 'outer 函数'(用自己的)
}

outer()
console.log(level)       // '全局'(用全局的)

6.2 逐层查找的示例

js 复制代码
const color = '红色'

function paintRoom() {
  const wallColor = '蓝色'

  function paintWall() {
    // JS 引擎查找 color 的过程:
    // 1. paintWall 内部 → 没找到
    // 2. paintRoom 内部 → 没找到
    // 3. 全局 → 找到了!'红色'
    console.log(`墙的颜色是${color}`)
  }

  paintWall()
}

paintRoom()   // 墙的颜色是红色

6.3 找不到就报错

js 复制代码
function greet() {
  // JS 引擎查找 myName 的过程:
  // 1. greet 内部 → 没找到
  // 2. 全局 → 没找到
  // 3. 报错!
  console.log(`你好,${myName}`)
}

greet()   // ReferenceError: myName is not defined

6.4 内部变量会"遮蔽"外部同名变量

js 复制代码
const name = '全局的小明'

function introduce() {
  const name = '函数里的小红'    // 这个 name "遮蔽"了全局的 name
  console.log(name)              // '函数里的小红'
}

introduce()
console.log(name)                // '全局的小明'(全局的没被影响)

这就像部门内部用了一个和公司同名的系统------部门内部的人优先用自己部门的系统,但公司的系统还在。


7. 闭包:函数记住了自己的"出生环境"

7.1 什么是闭包

闭包 = 一个函数 + 它出生时的环境。

当一个函数在它出生的作用域之外被调用时,它仍然能访问出生时的变量。这就是闭包。

js 复制代码
function createCounter() {
  let count = 0    // 这个变量在 createCounter 的作用域里

  // 返回一个函数,这个函数"记住"了 count
  return function() {
    count++
    console.log(count)
  }
}

const counter = createCounter()   // createCounter 执行完毕
counter()    // 1(count 被"记住"了)
counter()    // 2
counter()    // 3

为什么 createCounter 执行完了,count 还在?

因为返回的函数"闭包"了 count 变量------它带着 count 的引用,JS 引擎不会回收 count。

7.2 用生活例子理解闭包

闭包就像一个人带着自己老家的钥匙

js 复制代码
function createPerson(name) {
  // name 是"老家的东西"
  return {
    introduce() {
      // 这个方法能访问 name,因为它"带着老家的钥匙"
      console.log(`我叫${name}`)
    }
  }
}

const xiaoming = createPerson('小明')
const xiaohong = createPerson('小红')

xiaoming.introduce()   // 我叫小明
xiaohong.introduce()   // 我叫小红

虽然 createPerson 已经执行完了,但返回的对象方法仍然能访问 name

7.3 闭包的实际用途

用途 1:数据私有化(模拟私有变量)
js 复制代码
function createBankAccount(initialBalance) {
  let balance = initialBalance    // 私有变量,外面访问不到

  return {
    deposit(amount) {
      if (amount <= 0) {
        console.log('存款金额必须大于0')
        return
      }
      balance += amount
      console.log(`存入 ¥${amount},余额:¥${balance}`)
    },
    withdraw(amount) {
      if (amount > balance) {
        console.log('余额不足')
        return
      }
      balance -= amount
      console.log(`取出 ¥${amount},余额:¥${balance}`)
    },
    getBalance() {
      return balance
    }
  }
}

const account = createBankAccount(1000)
account.deposit(500)       // 存入 ¥500,余额:¥1500
account.withdraw(200)      // 取出 ¥200,余额:¥1300
console.log(account.getBalance())   // 1300

// ❌ 无法直接访问 balance
console.log(account.balance)   // undefined(被闭包保护了)

对比 Java

java 复制代码
// Java 用 private 实现数据私有
public class BankAccount {
    private double balance;   // private = 外面访问不到

    public void deposit(double amount) { ... }
    public void withdraw(double amount) { ... }
    public double getBalance() { return balance; }
}
js 复制代码
// JS 用闭包实现数据私有
function createBankAccount(initialBalance) {
  let balance = initialBalance   // 闭包变量 = 外面访问不到
  return { deposit(), withdraw(), getBalance() }
}
用途 2:函数工厂
js 复制代码
function createMultiplier(factor) {
  // factor 被闭包"记住"了
  return function(number) {
    return number * factor
  }
}

const double = createMultiplier(2)
const triple = createMultiplier(3)
const toPercent = createMultiplier(100)

console.log(double(5))       // 10
console.log(triple(5))       // 15
console.log(toPercent(0.85)) // 85
用途 3:防抖函数(Vue 项目常见)
js 复制代码
function createDebounce(fn, delay) {
  let timer = null   // 闭包记住 timer

  return function(...args) {
    clearTimeout(timer)   // 每次调用都取消上次的定时器
    timer = setTimeout(() => {
      fn(...args)         // delay 毫秒后才执行
    }, delay)
  }
}

// 用户输入时,停止打字 300ms 后才发请求
const search = createDebounce(function(keyword) {
  console.log('搜索:' + keyword)
}, 300)

search('i')       // 不会立即搜索
search('ip')      // 取消上次,重新计时
search('iph')     // 取消上次,重新计时
search('ipho')    // 取消上次,重新计时
search('iphon')   // 取消上次,重新计时
search('iphone')  // 300ms 后执行:搜索:iphone

7.4 闭包的注意事项

注意:闭包会保持引用,不会被垃圾回收

js 复制代码
function createHeavyObject() {
  const bigData = new Array(1000000).fill('数据')   // 很大的数据

  return function() {
    console.log(bigData.length)
  }
}

const fn = createHeavyObject()
// bigData 不会被回收,因为 fn 还引用着它
// 如果 fn 不再需要了,应该设为 null 让 GC 回收

8. 高阶函数:函数也能当参数和返回值

8.1 函数是一等公民

在 JS 里,函数和数字、字符串一样,是一种"值":

  • 可以赋值给变量
  • 可以当参数传递
  • 可以当返回值
js 复制代码
// 函数赋值给变量
const greet = function(name) {
  console.log(`你好,${name}`)
}

// 函数当参数传递
function doSomething(action) {
  action('小明')    // 调用传进来的函数
}
doSomething(greet)   // 你好,小明

// 函数当返回值
function createGreeter() {
  return function(name) {
    console.log(`你好,${name}`)
  }
}
const greeter = createGreeter()
greeter('小红')   // 你好,小红

8.2 高阶函数的定义

高阶函数 = 接收函数作为参数,或者返回函数的函数。

你其实已经见过高阶函数了------createCountercreateMultipliercreateDebounce 都是高阶函数。

8.3 回调函数

回调函数 = 作为参数传给另一个函数的函数。

js 复制代码
// onSuccess 和 onError 都是回调函数
function fetchData(url, onSuccess, onError) {
  // 模拟网络请求
  const success = true

  if (success) {
    onSuccess({ name: '小明', age: 25 })   // 调用成功的回调
  } else {
    onError('网络错误')                      // 调用失败的回调
  }
}

fetchData('/api/user',
  function(data) {
    console.log('成功:', data)
  },
  function(error) {
    console.log('失败:', error)
  }
)

这就是异步编程的基础------你告诉 JS"做完这件事后调用这个函数"。

8.4 函数组合

js 复制代码
// 用高阶函数组合功能
function withLogging(fn) {
  return function(...args) {
    console.log(`调用函数,参数:`, args)
    const result = fn(...args)
    console.log(`返回结果:`, result)
    return result
  }
}

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

const loggedAdd = withLogging(add)
loggedAdd(3, 5)
// 调用函数,参数: [3, 5]
// 返回结果: 8

9. 箭头函数详解

9.1 语法回顾

js 复制代码
// 完整写法
const add = (a, b) => {
  return a + b
}

// 只有一行 return → 省略 {} 和 return
const add = (a, b) => a + b

// 只有一个参数 → 省略 ()
const double = x => x * 2

// 没有参数 → 必须写 ()
const getRandom = () => Math.random()

// 返回对象 → 要加 () 包起来
const createUser = (name, age) => ({ name, age })
// 如果不加 (),JS 会把 {} 当成代码块
const wrong = (name, age) => { name, age }   // ❌ 返回 undefined

9.2 箭头函数的 this(重要区别)

普通函数有自己的 this,箭头函数没有自己的 this(继承外层的 this)。

js 复制代码
const person = {
  name: '小明',

  // 普通函数:this 指向调用者(person)
  greet() {
    console.log(`你好,我是${this.name}`)
  },

  // 箭头函数:this 指向外层作用域(不是 person!)
  delayedGreet: () => {
    console.log(`你好,我是${this.name}`)   // this.name 是 undefined
  }
}

person.greet()         // 你好,我是小明  ✅
person.delayedGreet()  // 你好,我是undefined  ❌

为什么箭头函数的 this 不指向 person?

因为箭头函数没有自己的 this,它的 this 继承自外层作用域(这里是全局作用域,全局没有 name 属性)。

实际开发中的影响

js 复制代码
const button = {
  text: '点击我',

  // ❌ 箭头函数:this 不是 button
  handleClick: () => {
    console.log(this.text)   // undefined
  },

  // ✅ 普通函数:this 是 button
  handleClick() {
    console.log(this.text)   // '点击我'
  }
}

记住:对象的方法用普通函数,不用箭头函数。

9.3 箭头函数不能当构造函数

js 复制代码
const Person = (name) => {
  this.name = name
}

// ❌ 报错
const p = new Person('小明')   // TypeError: Person is not a constructor

9.4 什么时候用箭头函数

js 复制代码
// ✅ 适合:回调函数、简单逻辑
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(n => n * 2)
const evens = numbers.filter(n => n % 2 === 0)

// ✅ 适合:事件处理(Vue 里常见)
const handleClick = () => {
  console.log('按钮被点击')
}

// ❌ 不适合:对象的方法
const obj = {
  name: '小明',
  greet: () => console.log(this.name)   // ❌ this 不是 obj
}

// ❌ 不适合:需要 arguments 的场景
const fn = () => console.log(arguments)   // ❌ 箭头函数没有 arguments

10. 常用数组方法(高阶函数实战)

这些方法是 JS 开发中最常用的,每个都接收一个函数作为参数------它们就是高阶函数。

10.1 forEach ------ 遍历数组

js 复制代码
const products = ['iPhone', 'iPad', 'MacBook']

// 等同于 for 循环,但更简洁
products.forEach(function(product, index) {
  console.log(`${index + 1}. ${product}`)
})
// 1. iPhone
// 2. iPad
// 3. MacBook

// 箭头函数写法
products.forEach((product, index) => {
  console.log(`${index + 1}. ${product}`)
})

对比 for 循环

js 复制代码
// for 循环(5行)
for (let i = 0; i < products.length; i++) {
  console.log(`${i + 1}. ${products[i]}`)
}

// forEach(3行)
products.forEach((product, i) => {
  console.log(`${i + 1}. ${product}`)
})

10.2 map ------ 映射:把数组变换成新数组

js 复制代码
const prices = [100, 200, 300]

// 把每个价格打 8 折
const discounted = prices.map(price => price * 0.8)
console.log(discounted)   // [80, 160, 240]
console.log(prices)       // [100, 200, 300](原数组不变)
js 复制代码
// 把对象数组变换成另一个形状
const users = [
  { name: '小明', age: 25 },
  { name: '小红', age: 30 },
  { name: '小刚', age: 20 }
]

const names = users.map(user => user.name)
console.log(names)   // ['小明', '小红', '小刚']

const greetings = users.map(user => `${user.name}(${user.age}岁)`)
console.log(greetings)   // ['小明(25岁)', '小红(30岁)', '小刚(20岁)']

10.3 filter ------ 过滤:保留满足条件的元素

js 复制代码
const scores = [85, 42, 93, 67, 58, 91]

// 保留及格的(>= 60)
const passed = scores.filter(score => score >= 60)
console.log(passed)   // [85, 93, 67, 91]
js 复制代码
// 过滤对象数组
const products = [
  { name: 'iPhone', price: 7999, stock: 10 },
  { name: 'iPad', price: 3499, stock: 0 },
  { name: 'AirPods', price: 1999, stock: 50 },
  { name: 'MacBook', price: 12999, stock: 0 }
]

// 有库存的商品
const inStock = products.filter(p => p.stock > 0)
console.log(inStock)
// [{ name: 'iPhone', ... }, { name: 'AirPods', ... }]

// 价格低于 5000 的商品
const affordable = products.filter(p => p.price < 5000)
console.log(affordable)
// [{ name: 'iPad', ... }, { name: 'AirPods', ... }]

10.4 find ------ 查找第一个满足条件的元素

js 复制代码
const products = [
  { id: 1, name: 'iPhone', price: 7999 },
  { id: 2, name: 'iPad', price: 3499 },
  { id: 3, name: 'AirPods', price: 1999 }
]

// 找 id 为 2 的商品
const product = products.find(p => p.id === 2)
console.log(product)   // { id: 2, name: 'iPad', price: 3499 }

// 找不到返回 undefined
const notFound = products.find(p => p.id === 99)
console.log(notFound)   // undefined

find vs filter

js 复制代码
// find 返回第一个匹配的元素(一个对象)
const product = products.find(p => p.price < 5000)
// { id: 2, name: 'iPad', price: 3499 }

// filter 返回所有匹配的元素(一个数组)
const cheapProducts = products.filter(p => p.price < 5000)
// [{ id: 2, ... }, { id: 3, ... }]

10.5 some 和 every ------ 条件判断

js 复制代码
const scores = [85, 42, 93, 67, 58, 91]

// some:至少有一个满足条件
const hasPassed = scores.some(s => s >= 60)
console.log(hasPassed)   // true(有及格的)

const hasPerfect = scores.some(s => s === 100)
console.log(hasPerfect)   // false(没有满分的)

// every:全部满足条件
const allPassed = scores.every(s => s >= 60)
console.log(allPassed)   // false(不是全部及格)

const allPositive = scores.every(s => s > 0)
console.log(allPositive)   // true(全部大于 0)

实际场景

js 复制代码
const cart = [
  { name: 'iPhone', quantity: 1, stock: 10 },
  { name: 'iPad', quantity: 2, stock: 0 },
  { name: 'AirPods', quantity: 1, stock: 50 }
]

// 检查是否所有商品都有库存
const allInStock = cart.every(item => item.stock >= item.quantity)
console.log(allInStock)   // false(iPad 库存为 0)

// 检查是否有商品缺货
const anyOutOfStock = cart.some(item => item.stock < item.quantity)
console.log(anyOutOfStock)   // true

10.6 reduce ------ 归并:把数组"浓缩"成一个值

reduce 是最强大的数组方法,可以把数组变成任何东西。

js 复制代码
const numbers = [1, 2, 3, 4, 5]

// 求和
const sum = numbers.reduce((accumulator, current) => {
  return accumulator + current
}, 0)
console.log(sum)   // 15

// 执行过程:
// 第1次:accumulator=0, current=1 → 0+1=1
// 第2次:accumulator=1, current=2 → 1+2=3
// 第3次:accumulator=3, current=3 → 3+3=6
// 第4次:accumulator=6, current=4 → 6+4=10
// 第5次:accumulator=10, current=5 → 10+5=15

求最大值

js 复制代码
const numbers = [3, 1, 4, 1, 5, 9, 2, 6]

const max = numbers.reduce((max, current) => {
  return current > max ? current : max
}, numbers[0])
console.log(max)   // 9

统计词频

js 复制代码
const words = ['苹果', '香蕉', '苹果', '橙子', '香蕉', '苹果']

const count = words.reduce((acc, word) => {
  acc[word] = (acc[word] || 0) + 1
  return acc
}, {})
console.log(count)
// { '苹果': 3, '香蕉': 2, '橙子': 1 }

计算购物车总价

js 复制代码
const cart = [
  { name: 'iPhone', price: 7999, quantity: 1 },
  { name: 'AirPods', price: 1999, quantity: 2 },
  { name: '数据线', price: 39, quantity: 3 }
]

const total = cart.reduce((sum, item) => {
  return sum + item.price * item.quantity
}, 0)
console.log(`总价:¥${total}`)   // 总价:¥12115

10.7 链式调用

这些方法可以串联起来用:

js 复制代码
const products = [
  { name: 'iPhone', price: 7999, stock: 10 },
  { name: 'iPad', price: 3499, stock: 0 },
  { name: 'AirPods', price: 1999, stock: 50 },
  { name: 'MacBook', price: 12999, stock: 5 },
  { name: '数据线', price: 39, stock: 100 }
]

// 需求:有库存的商品,价格打 8 折,只保留 5000 以下的,算总价
const total = products
  .filter(p => p.stock > 0)          // 有库存的
  .map(p => ({ ...p, price: p.price * 0.8 }))   // 打 8 折
  .filter(p => p.price < 5000)       // 5000 以下的
  .reduce((sum, p) => sum + p.price, 0)   // 求总价

console.log(`总价:¥${total}`)   // 总价:¥1631.2

10.8 方法速查表

方法 作用 返回值 示例
forEach 遍历 undefined [1,2].forEach(x => console.log(x))
map 映射 新数组 [1,2].map(x => x * 2)[2,4]
filter 过滤 新数组 [1,2,3].filter(x => x > 1)[2,3]
find 查找第一个 元素或undefined [1,2,3].find(x => x > 1)2
some 至少一个 boolean [1,2,3].some(x => x > 2)true
every 全部 boolean [1,2,3].every(x => x > 0)true
reduce 归并 任意值 [1,2,3].reduce((a,b) => a+b, 0)6

11. 练习

练习 1(初级):写函数

js 复制代码
// 1. 写一个函数,接收两个数,返回较大的那个
function max(a, b) {
  // 你的代码
}

// 2. 写一个函数,接收一个数组,返回所有元素的和
function sum(arr) {
  // 你的代码
}

// 3. 写一个函数,接收一个字符串,返回反转后的字符串
function reverse(str) {
  // 你的代码
}

// 测试
console.log(max(3, 5))          // 5
console.log(sum([1, 2, 3, 4]))  // 10
console.log(reverse('hello'))   // 'olleh'

练习 2(初级):默认参数 + 剩余参数

js 复制代码
// 1. 写一个创建用户的函数,name 必填,age 默认 0,role 默认 '用户'
function createUser(name, age, role) {
  // 你的代码
}

// 2. 写一个函数,接收任意个数字,返回它们的平均值
function average(...numbers) {
  // 你的代码
}

// 测试
console.log(createUser('小明'))           // { name: '小明', age: 0, role: '用户' }
console.log(createUser('小红', 25))       // { name: '小红', age: 25, role: '用户' }
console.log(average(80, 90, 100))         // 90
console.log(average(60, 70, 80, 90))      // 75

练习 3(中级):闭包

js 复制代码
// 1. 写一个计数器工厂,返回一个对象,有 increment / decrement / getCount 三个方法
function createCounter(initialValue = 0) {
  // 你的代码
}

// 2. 写一个缓存函数,第一次调用时计算,之后直接返回缓存的结果
function memoize(fn) {
  // 你的代码
}

// 测试计数器
const counter = createCounter(10)
counter.increment()   // count = 11
counter.increment()   // count = 12
counter.decrement()   // count = 11
console.log(counter.getCount())   // 11

// 测试缓存
const expensiveCalculation = memoize(function(n) {
  console.log('计算中...')
  return n * n
})
expensiveCalculation(5)   // 计算中...  25
expensiveCalculation(5)   // 25(没有"计算中...",用了缓存)
expensiveCalculation(3)   // 计算中...  9

练习 4(中级):数组高阶方法

js 复制代码
const students = [
  { name: '小明', score: 85 },
  { name: '小红', score: 92 },
  { name: '小刚', score: 58 },
  { name: '小李', score: 73 },
  { name: '小王', score: 96 },
  { name: '小张', score: 45 }
]

// 1. 用 filter 找出所有及格的学生(>= 60)
// 2. 用 map 把学生变换成 "姓名: 分数" 格式的字符串数组
// 3. 用 reduce 计算全班平均分
// 4. 用 some 判断是否有满分(100分)
// 5. 用 find 找到分数最高的学生

练习 5(综合):综合运用

js 复制代码
// 写一个购物车系统
const cart = [
  { name: 'iPhone 15', price: 7999, quantity: 1 },
  { name: 'AirPods Pro', price: 1999, quantity: 2 },
  { name: '数据线', price: 39, quantity: 3 },
  { name: '手机壳', price: 99, quantity: 1 }
]

// 1. 计算总价
// 2. 找出最贵的商品
// 3. 过滤掉价格低于 100 的商品
// 4. 生成订单摘要字符串:"共 N 件商品,总价 ¥XXXX"

12. 常见陷阱

陷阱 1:箭头函数在对象方法中丢失 this

js 复制代码
const person = {
  name: '小明',
  // ❌ 箭头函数的 this 不是 person
  greet: () => {
    console.log(this.name)
  }
}
person.greet()   // undefined

// ✅ 用普通函数
const person2 = {
  name: '小明',
  greet() {
    console.log(this.name)
  }
}
person2.greet()   // '小明'

陷阱 2:闭包变量共享问题

js 复制代码
// ❌ 经典坑:循环中的闭包
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i)
  }, 100)
}
// 输出:3, 3, 3(不是 0, 1, 2!)

// 原因:var 没有块级作用域,三个函数共享同一个 i
// 循环结束后 i 变成 3,三个函数都打印 3

// ✅ 用 let(块级作用域,每次循环创建新的 i)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i)
  }, 100)
}
// 输出:0, 1, 2

// ✅ 或者用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j)
    }, 100)
  })(i)
}
// 输出:0, 1, 2

陷阱 3:忘记 return

js 复制代码
// ❌ 忘记 return
function add(a, b) {
  a + b    // 没有 return,返回 undefined
}

console.log(add(3, 5))   // undefined

// ✅ 加上 return
function add(a, b) {
  return a + b
}

console.log(add(3, 5))   // 8

陷阱 4:箭头函数返回对象忘记加 ()

js 复制代码
// ❌ JS 会把 {} 当成代码块
const getUser = (name) => { name: name }
console.log(getUser('小明'))   // undefined

// ✅ 用 () 包起来
const getUser2 = (name) => ({ name: name })
console.log(getUser2('小明'))   // { name: '小明' }

陷阱 5:forEach 里用 return 不会跳出循环

js 复制代码
const numbers = [1, 2, 3, 4, 5]

// ❌ return 在 forEach 里只是跳过本次,不是跳出循环
numbers.forEach(n => {
  if (n === 3) return    // 只是跳过 n===3 这次
  console.log(n)
})
// 输出:1, 2, 4, 5(3 被跳过了,但 4 和 5 还是会执行)

// ✅ 想跳出循环用 for...of + break
for (const n of numbers) {
  if (n === 3) break
  console.log(n)
}
// 输出:1, 2

附:今日速查

js 复制代码
// 声明函数(三种方式)
function add(a, b) { return a + b }       // 函数声明
const add = function(a, b) { return a + b } // 函数表达式
const add = (a, b) => a + b                // 箭头函数(推荐)

// 参数
function f(x = 0) { }          // 默认参数
function f(...args) { }         // 剩余参数
function f({ name, age }) { }  // 参数解构

// 返回值
return value                    // 返回值(立即结束函数)
return { name, age }            // 返回对象
return [min, max]               // 返回数组

// 作用域
// 全局:函数外声明,到处能访问
// 函数:函数内声明,只有函数内能访问
// 块级:{} 内 let/const,只有块内能访问

// 闭包
function createCounter() {
  let count = 0
  return { increment() { count++ }, getCount() { return count } }
}

// 高阶函数:接收函数或返回函数的函数
// 回调函数:作为参数传给另一个函数

// 数组方法
arr.forEach(item => { })           // 遍历
arr.map(item => newValue)          // 映射
arr.filter(item => condition)      // 过滤
arr.find(item => condition)        // 查找第一个
arr.some(item => condition)        // 至少一个
arr.every(item => condition)       // 全部
arr.reduce((acc, item) => acc, 初始值)  // 归并

// 箭头函数注意
// ✅ 回调函数、简单逻辑
// ❌ 对象方法(this 问题)、构造函数
相关推荐
小科先生2 小时前
初学者安装java
java·开发语言
一楼的猫2 小时前
叙事指纹93.2%的技术确认与AI写作同质化——网文创作的差异化路径分析
人工智能·学习·机器学习·写作·ai写作
ID_180079054732 小时前
小红书笔记评论 API 接口深度解析(带全套 JSON 示例・技术实战版)
java·开发语言·windows
折戟不必沉沙2 小时前
C++四种类型转换是什么
开发语言·c++
天青色等烟雨..2 小时前
AI赋能R-Meta分析核心技术:从热点挖掘到高级模型、助力高效科研与论文发表
开发语言·人工智能·r语言
red_redemption2 小时前
自由学习记录(199)
学习·dram 二線廠商·git partclone·4y halving 減半·3.125btc·手續費 sat/vb
AI玫瑰助手2 小时前
Python函数:递归函数的定义与阶乘案例实现
开发语言·python·信息可视化
qq_366086222 小时前
测试接口传参数时,放在Header和Body中后台接收参数的区别
java·开发语言·前端
Jun6262 小时前
QT(8)-线程锁
java·开发语言