学习时间:约 2 小时
前置知识:第3天的变量、类型、流程控制
目录
- 函数是什么
- 声明函数的三种方式
- 参数详解
- 返回值
- 作用域:变量的"活动范围"
- 作用域链:变量的"查找路径"
- 闭包:函数记住了自己的"出生环境"
- 高阶函数:函数也能当参数和返回值
- 箭头函数详解
- 常用数组方法(高阶函数实战)
- 练习
- 常见陷阱
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 普通函数的区别(先记住两点):
- 箭头函数没有自己的
this(第7节闭包会讲) - 箭头函数不能当构造函数(不能
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 的
defineProps、defineEmits都大量使用解构。你很快就会在 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)
用 {} 包起来的代码块,let 和 const 声明的变量只在块内有效:
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 高阶函数的定义
高阶函数 = 接收函数作为参数,或者返回函数的函数。
你其实已经见过高阶函数了------createCounter、createMultiplier、createDebounce 都是高阶函数。
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 问题)、构造函数