JavaScript 函数
🎯 目标:2-3小时内掌握 JavaScript 函数核心,达到实战水平
目录
1. 核心概念与必须掌握的API
1.1 函数定义方式
函数声明 (Function Declaration)
function greet(name) { return `Hello, ${name}`; }
- 会被提升(hoisting)到作用域顶部,可在声明前调用
- 有函数名,适合需要递归或命名的场景
函数表达式 (Function Expression)
const greet = function(name) { return `Hello, ${name}`; };
- 不会被提升,必须先定义后使用
- 常用于赋值给变量或作为参数传递
箭头函数 (Arrow Function) ⭐
const greet = (name) => `Hello, ${name}`;
- 简洁语法,隐式返回(单表达式时)
- 没有自己的
this,继承外层作用域的this
1.2 核心概念
参数 (Parameters)
function add(a, b = 0) { return a + b; } // b 有默认值
- 默认参数:
function fn(x = 1) {} - 剩余参数:
function fn(...args) {}将多余参数收集为数组
返回值 (Return)
function double(n) { return n * 2; }
- 没有
return语句时返回undefined return后的代码不会执行
作用域 (Scope) ⭐⭐⭐
let global = 'outside';
function test() {
let local = 'inside'; // 只在函数内可见
console.log(global); // 可访问外层变量
}
- 函数创建独立作用域,内部可访问外部变量,反之不行
- 闭包:内层函数访问外层函数变量的能力
this 关键字 ⭐⭐⭐
const obj = {
name: 'Alice',
greet: function() { console.log(this.name); } // this 指向 obj
};
- 普通函数:
this由调用方式决定 - 箭头函数:
this继承定义时的外层作用域
高阶函数 (Higher-Order Function)
function map(arr, fn) { return arr.map(fn); }
- 接收函数作为参数或返回函数的函数
- 常见:
map(),filter(),reduce()
1.3 必须掌握的API
| API | 说明 | 示例 |
|---|---|---|
call(thisArg, ...args) |
改变 this 并立即调用函数 |
fn.call(obj, 1, 2) |
apply(thisArg, [args]) |
同 call,但参数以数组形式传递 |
fn.apply(obj, [1, 2]) |
bind(thisArg, ...args) |
返回绑定 this 的新函数,不立即执行 |
const newFn = fn.bind(obj) |
... 扩展运算符 |
展开数组或收集参数 | fn(...[1, 2, 3]) |
arguments 对象 |
类数组对象,包含所有传入参数(箭头函数无) | function(){ console.log(arguments) } |
2. 典型应用场景与设计取舍
2.1 应用场景
| 场景 | 最佳选择 | 原因 |
|---|---|---|
| 事件处理器 | 箭头函数 | 不绑定 this,避免 this 丢失 |
| 对象方法 | 普通函数 | 需要动态 this 指向对象 |
| 回调函数 | 箭头函数 | 简洁,保持外层 this |
| 构造函数 | 函数声明/表达式 | 箭头函数不能作为构造函数 |
| 工具函数库 | 函数声明 | 可提升,模块化导出方便 |
2.2 设计取舍
何时用箭头函数?
- ✅ 需要继承外层
this(如 React 组件方法) - ✅ 简短的回调函数 (如
map,filter) - ❌ 需要动态
this的对象方法 - ❌ 需要
arguments对象
何时用普通函数?
- ✅ 对象方法需要访问
this - ✅ 需要函数提升特性
- ✅ 需要作为构造函数
命名函数 vs 匿名函数
- 命名函数便于调试(堆栈信息清晰)
- 匿名函数适合一次性使用场景
3. 最小可运行示例
3.1 完整示例:任务管理器
/**
* 任务管理器 - 综合演示函数核心概念
* 运行环境:浏览器控制台或 Node.js
*/
// ===== 1. 函数声明:创建任务 =====
function createTask(title, priority = 'medium') {
return {
id: Date.now(),
title,
priority,
completed: false
};
}
// ===== 2. 箭头函数:任务过滤器 =====
const filterByPriority = (tasks, priority) => {
return tasks.filter(task => task.priority === priority);
};
// ===== 3. 闭包:创建计数器 =====
function createCounter() {
let count = 0; // 私有变量
return {
increment: () => ++count,
getCount: () => count
};
}
// ===== 4. 高阶函数:任务转换器 =====
function mapTasks(tasks, transformer) {
return tasks.map(transformer);
}
// ===== 5. this 绑定:任务管理对象 =====
const taskManager = {
tasks: [],
// 普通函数:this 指向 taskManager
addTask: function(title, priority) {
const task = createTask(title, priority);
this.tasks.push(task);
console.log(`✅ 已添加: ${title}`);
},
// 箭头函数作为方法(演示陷阱)
// 错误示例: getTasks: () => this.tasks // this 不指向 taskManager!
getTasks: function() {
return this.tasks;
},
// 使用剩余参数
addMultiple: function(...titles) {
titles.forEach(title => this.addTask(title, 'low'));
}
};
// ===== 6. 默认参数与解构 =====
function printTask({ title, priority = 'unknown', completed = false }) {
const status = completed ? '✓' : '○';
console.log(`${status} [${priority}] ${title}`);
}
// ===== 运行示例 =====
console.log('=== 任务管理器示例 ===\n');
// 添加任务
taskManager.addTask('学习 JavaScript 函数', 'high');
taskManager.addTask('完成作业');
taskManager.addMultiple('买菜', '运动', '读书');
// 获取所有任务
const allTasks = taskManager.getTasks();
console.log('\n📋 所有任务:');
allTasks.forEach(printTask);
// 过滤高优先级任务
const highPriority = filterByPriority(allTasks, 'high');
console.log('\n🔥 高优先级任务:');
highPriority.forEach(printTask);
// 使用高阶函数转换
const taskTitles = mapTasks(allTasks, task => task.title);
console.log('\n📝 任务标题列表:', taskTitles);
// 闭包计数器
const counter = createCounter();
console.log('\n🔢 计数器演示:');
console.log('计数:', counter.increment()); // 1
console.log('计数:', counter.increment()); // 2
console.log('当前值:', counter.getCount()); // 2
// ===== 7. call/apply/bind 演示 =====
const task = createTask('重要任务', 'high');
function announceTask(prefix) {
console.log(`${prefix}: ${this.title} (${this.priority})`);
}
console.log('\n🎯 this 绑定演示:');
announceTask.call(task, '提醒'); // call: 逐个传参
announceTask.apply(task, ['警告']); // apply: 数组传参
const boundFn = announceTask.bind(task); // bind: 返回新函数
boundFn('绑定调用');
3.2 运行步骤
浏览器环境:
- 打开浏览器开发者工具 (F12)
- 切换到 Console 面板
- 复制粘贴代码,回车运行
Node.js 环境:
# 保存为 functions.js
node functions.js
预期输出:
=== 任务管理器示例 ===
✅ 已添加: 学习 JavaScript 函数
✅ 已添加: 完成作业
✅ 已添加: 买菜
✅ 已添加: 运动
✅ 已添加: 读书
📋 所有任务:
○ [high] 学习 JavaScript 函数
○ [medium] 完成作业
○ [low] 买菜
○ [low] 运动
○ [low] 读书
🔥 高优先级任务:
○ [high] 学习 JavaScript 函数
📝 任务标题列表: [ '学习 JavaScript 函数', '完成作业', '买菜', '运动', '读书' ]
🔢 计数器演示:
计数: 1
计数: 2
当前值: 2
🎯 this 绑定演示:
提醒: 重要任务 (high)
警告: 重要任务 (high)
绑定调用: 重要任务 (high)
4. 常见坑与调试技巧
4.1 常见陷阱
坑1: 箭头函数的 this 陷阱
// ❌ 错误
const obj = {
name: 'Alice',
greet: () => console.log(this.name) // this 不指向 obj!
};
obj.greet(); // undefined
// ✅ 正确
const obj2 = {
name: 'Alice',
greet: function() { console.log(this.name); }
};
obj2.greet(); // 'Alice'
坑2: 回调函数丢失 this
const user = {
name: 'Bob',
friends: ['Alice', 'Charlie'],
printFriends: function() {
// ❌ 错误
this.friends.forEach(function(friend) {
console.log(this.name + ' knows ' + friend); // this 是 undefined!
});
// ✅ 方法1: 箭头函数
this.friends.forEach(friend => {
console.log(this.name + ' knows ' + friend);
});
// ✅ 方法2: bind
this.friends.forEach(function(friend) {
console.log(this.name + ' knows ' + friend);
}.bind(this));
}
};
坑3: 闭包的变量陷阱
// ❌ 错误:循环中的闭包
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 3 次 3
}, 100);
}
// ✅ 方法1: 使用 let
for (let i = 0; i < 3; 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);
}
坑4: 默认参数的求值时机
let x = 1;
function fn(a = x) {
let x = 2;
console.log(a); // 1 (默认参数在函数作用域外求值)
}
fn();
4.2 调试技巧
技巧1: 使用命名函数便于调试
// ❌ 匿名函数:堆栈信息显示 'anonymous'
const data = [1, 2, 3].map(function(x) {
throw new Error('test');
});
// ✅ 命名函数:堆栈信息显示 'doubleValue'
const data = [1, 2, 3].map(function doubleValue(x) {
throw new Error('test');
});
技巧2: 检查函数类型
function isArrowFunction(fn) {
return !fn.prototype; // 箭头函数没有 prototype
}
const regular = function() {};
const arrow = () => {};
console.log(isArrowFunction(regular)); // false
console.log(isArrowFunction(arrow)); // true
技巧3: 使用 console.trace() 追踪调用栈
function outer() {
inner();
}
function inner() {
console.trace('查看调用栈');
}
outer();
技巧4: 参数校验
function divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('参数必须是数字');
}
if (b === 0) {
throw new Error('除数不能为零');
}
return a / b;
}
5. 分层练习清单
5.1 入门级练习 (30分钟)
练习1: 温度转换器
// 要求:
// 1. 编写 celsiusToFahrenheit 函数(摄氏转华氏)
// 2. 编写 fahrenheitToCelsius 函数(华氏转摄氏)
// 3. 使用默认参数,默认转换 0 度
// 提示:
// 华氏 = 摄氏 * 9/5 + 32
// 摄氏 = (华氏 - 32) * 5/9
练习2: 数组操作
// 要求:
// 1. 使用箭头函数编写 double 函数(数组每项乘2)
// 2. 使用箭头函数编写 filterEven 函数(过滤偶数)
// 3. 测试: [1, 2, 3, 4, 5]
// 提示: 使用 map 和 filter
练习3: 对象方法
// 要求:
// 创建 calculator 对象,包含:
// - add(a, b)
// - subtract(a, b)
// - history 数组记录操作
// - printHistory() 打印历史
// 注意: 使用普通函数确保 this 正确
5.2 巩固级练习 (45分钟)
练习4: 购物车系统
// 要求:
// 1. 创建购物车对象,包含商品数组
// 2. addItem(name, price, quantity) 添加商品
// 3. removeItem(name) 删除商品
// 4. getTotal() 计算总价
// 5. applyDiscount(percent) 应用折扣(返回新函数,闭包实现)
// 测试:
// cart.addItem('苹果', 5, 3);
// cart.addItem('香蕉', 3, 5);
// const discounted = cart.applyDiscount(10); // 9折
// console.log(discounted());
练习5: 防抖函数 (Debounce)
// 要求:
// 实现 debounce(fn, delay) 函数
// - 延迟执行 fn
// - 如果在 delay 时间内再次调用,重置计时器
// 提示:
// 使用闭包保存 timer
// 使用 clearTimeout 和 setTimeout
// 测试:
// const search = debounce(() => console.log('搜索...'), 500);
// search(); search(); search(); // 只执行一次
练习6: 数组去重
// 要求:
// 1. 使用 filter 实现 unique(arr)
// 2. 使用 Set 实现 uniqueSet(arr)
// 3. 比较两种方法性能
// 测试: [1, 2, 2, 3, 3, 3, 4]
5.3 进阶级练习 (45分钟)
练习7: 函数柯里化 (Curry)
// 要求:
// 实现 curry(fn) 函数,将多参数函数转为单参数函数链
// 示例:
// function add(a, b, c) { return a + b + c; }
// const curriedAdd = curry(add);
// console.log(curriedAdd(1)(2)(3)); // 6
// console.log(curriedAdd(1, 2)(3)); // 6
// 提示: 使用闭包和递归
练习8: Promise 重试机制
// 要求:
// 实现 retry(fn, times) 函数
// - fn 返回 Promise
// - 失败时重试 times 次
// - 所有重试失败后抛出错误
// 测试:
// let count = 0;
// const flaky = () => {
// count++;
// return count < 3 ? Promise.reject('失败') : Promise.resolve('成功');
// };
// retry(flaky, 5).then(console.log); // '成功'
练习9: 函数组合 (Compose)
// 要求:
// 实现 compose(...fns) 函数,从右到左执行函数
// 示例:
// const addOne = x => x + 1;
// const double = x => x * 2;
// const square = x => x * x;
// const result = compose(square, double, addOne)(2);
// console.log(result); // 36 即: ((2+1)*2)^2
// 提示: 使用 reduce 或 reduceRight
6. 自测清单与闭卷题
6.1 自测清单
在开始测试前,确保你能回答以下问题:
- [ ] 能说出函数声明和函数表达式的区别
- [ ] 理解箭头函数的
this继承规则 - [ ] 能解释闭包的原理和应用场景
- [ ] 掌握
call、apply、bind的区别 - [ ] 理解高阶函数的概念
- [ ] 能正确使用默认参数和剩余参数
- [ ] 知道何时使用箭头函数,何时使用普通函数
- [ ] 能识别和避免常见的
this陷阱 - [ ] 理解函数作用域和变量提升
- [ ] 能使用闭包实现私有变量
6.2 闭卷测试题
第1题:代码输出 (30分)
var name = 'Global';
const obj = {
name: 'Object',
method1: function() {
console.log(this.name);
},
method2: () => {
console.log(this.name);
},
method3: function() {
const inner = () => {
console.log(this.name);
};
inner();
}
};
obj.method1();
obj.method2();
obj.method3();
const fn = obj.method1;
fn();
问题: 请写出每个 console.log 的输出结果,并解释原因。
第2题:实现函数 (40分)
// 实现一个 once 函数,确保传入的函数只执行一次
function once(fn) {
// 在此实现
}
// 测试
const initialize = once(() => console.log('初始化'));
initialize(); // 输出: 初始化
initialize(); // 不输出
initialize(); // 不输出
要求:
- 使用闭包实现
- 函数应该能接收参数并返回结果
- 多次调用返回第一次的结果
第3题:代码分析与修复 (30分)
function createButtons() {
const buttons = [];
for (var i = 0; i < 3; i++) {
buttons.push({
click: function() {
console.log('Button ' + i);
}
});
}
return buttons;
}
const buttons = createButtons();
buttons[0].click(); // 期望: Button 0, 实际: ?
buttons[1].click(); // 期望: Button 1, 实际: ?
buttons[2].click(); // 期望: Button 2, 实际: ?
问题:
- 实际输出是什么?为什么?
- 提供至少两种修复方法
6.3 标准答案
第1题答案:
obj.method1(); // 输出: 'Object'
// 原因: 普通函数,this 指向调用对象 obj
obj.method2(); // 输出: 'Global' (浏览器) 或 undefined (严格模式)
// 原因: 箭头函数继承外层 this,此处是全局对象
obj.method3(); // 输出: 'Object'
// 原因: 箭头函数 inner 继承 method3 的 this,指向 obj
const fn = obj.method1;
fn(); // 输出: 'Global' (浏览器) 或 undefined (严格模式)
// 原因: 普通函数调用,this 指向全局对象(非严格)或 undefined(严格)
评分标准:
- 每个输出正确 3分 (共15分)
- 每个解释正确 3分 (共15分)
第2题答案:
function once(fn) {
let called = false; // 标记是否已调用
let result; // 缓存结果
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args); // 保持 this 和参数
}
return result;
};
}
// 或使用箭头函数(不考虑 this)
function once(fn) {
let called = false;
let result;
return (...args) => {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
评分标准:
- 使用闭包保存状态 (10分)
- 正确判断首次调用 (10分)
- 缓存并返回结果 (10分)
- 正确传递参数 (10分)
第3题答案:
1. 实际输出:
buttons[0].click(); // 输出: Button 3
buttons[1].click(); // 输出: Button 3
buttons[2].click(); // 输出: Button 3
原因: var 声明的 i 是函数作用域,循环结束后 i = 3,所有闭包共享同一个 i。
2. 修复方法:
方法1: 使用 let
function createButtons() {
const buttons = [];
for (let i = 0; i < 3; i++) { // 改用 let
buttons.push({
click: function() {
console.log('Button ' + i);
}
});
}
return buttons;
}
方法2: IIFE (立即执行函数)
function createButtons() {
const buttons = [];
for (var i = 0; i < 3; i++) {
(function(j) { // 创建新作用域
buttons.push({
click: function() {
console.log('Button ' + j);
}
});
})(i);
}
return buttons;
}
方法3: 箭头函数 + forEach
function createButtons() {
return [0, 1, 2].map(i => ({
click: () => console.log('Button ' + i)
}));
}
评分标准:
- 正确分析输出 (5分)
- 解释原因 (10分)
- 每个修复方法 (7.5分,共15分)
📚 推荐资源
-
官方文档:
-
进阶阅读:
- 《你不知道的JavaScript(上卷)》- 作用域与闭包
- 《JavaScript高级程序设计》- 第10章(函数)
-
在线练习:
🎯 学习检查点
完成本教程后,你应该能够:
- ✅ 在实际项目中正确选择函数定义方式
- ✅ 理解并避免
this绑定陷阱 - ✅ 使用闭包实现数据封装和高级功能
- ✅ 编写清晰、可维护的函数式代码
- ✅ 调试函数相关的常见问题
下一步学习方向:
- 异步函数 (async/await)
- 函数式编程范式
- 设计模式中的函数应用
祝学习顺利! 🚀
