盐汽水的海洋

JavaScript 函数

avatar

盐汽水

🎯 目标:2-3小时内掌握 JavaScript 函数核心,达到实战水平


目录

  1. 核心概念与必须掌握的API
  2. 典型应用场景与设计取舍
  3. 最小可运行示例
  4. 常见坑与调试技巧
  5. 分层练习清单
  6. 自测清单与闭卷题

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 运行步骤

浏览器环境:

  1. 打开浏览器开发者工具 (F12)
  2. 切换到 Console 面板
  3. 复制粘贴代码,回车运行

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 继承规则
  • [ ] 能解释闭包的原理和应用场景
  • [ ] 掌握 callapplybind 的区别
  • [ ] 理解高阶函数的概念
  • [ ] 能正确使用默认参数和剩余参数
  • [ ] 知道何时使用箭头函数,何时使用普通函数
  • [ ] 能识别和避免常见的 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(); // 不输出

要求:

  1. 使用闭包实现
  2. 函数应该能接收参数并返回结果
  3. 多次调用返回第一次的结果

第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, 实际: ?

问题:

  1. 实际输出是什么?为什么?
  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分)

📚 推荐资源

  1. 官方文档:

  2. 进阶阅读:

    • 《你不知道的JavaScript(上卷)》- 作用域与闭包
    • 《JavaScript高级程序设计》- 第10章(函数)
  3. 在线练习:


🎯 学习检查点

完成本教程后,你应该能够:

  • ✅ 在实际项目中正确选择函数定义方式
  • ✅ 理解并避免 this 绑定陷阱
  • ✅ 使用闭包实现数据封装和高级功能
  • ✅ 编写清晰、可维护的函数式代码
  • ✅ 调试函数相关的常见问题

下一步学习方向:

  • 异步函数 (async/await)
  • 函数式编程范式
  • 设计模式中的函数应用

祝学习顺利! 🚀

闽ICP备2021014815号-2
powered by emlog sitemap