JavaScript 函数与作用域

章节介绍

函数是 JavaScript 编程的核心概念之一,它让我们能够将代码组织成可重用的块。理解函数的工作原理、作用域机制和闭包概念,对于编写高质量的 JavaScript 代码至关重要。本教程将深入讲解这些重要概念,为后续的 Node.js 学习打下坚实基础。

核心知识点

函数定义

JavaScript 提供了多种定义函数的方式:

函数声明

function greet(name) {
  console.log(`你好,${name}!`);
}

greet('张三');  // 输出:你好,张三!

函数表达式

const greet = function(name) {
  console.log(`你好,${name}!`);
};

greet('李四');  // 输出:你好,李四!

箭头函数(ES6)

const greet = (name) => {
  console.log(`你好,${name}!`);
};

// 简化形式(单个参数)
const greet2 = name => console.log(`你好,${name}!`);

// 简化形式(单行返回)
const add = (a, b) => a + b;

greet('王五');   // 输出:你好,王五!
console.log(add(5, 3));  // 输出:8

函数参数

默认参数

function greet(name = '朋友') {
  console.log(`你好,${name}!`);
}

greet();        // 输出:你好,朋友!
greet('张三');  // 输出:你好,张三!

剩余参数

function sumAll(...numbers) {
  return numbers.reduce((sum, num) => sum + num, 0);
}

console.log(sumAll(1, 2, 3, 4, 5));  // 15
console.log(sumAll(10, 20, 30));        // 60

参数解构

function createUser({ name, age, city = '北京' }) {
  return {
    name,
    age,
    city,
    createdAt: new Date()
  };
}

const user = createUser({
  name: '张三',
  age: 25
});

console.log(user);
// 输出:{ name: '张三', age: 25, city: '北京', createdAt: Date }

函数返回值

函数可以返回值,使用 return 语句:

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

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

// 没有返回值的函数默认返回 undefined
function logMessage(message) {
  console.log(message);
  // 没有 return 语句
}

const logged = logMessage('Hello');
console.log(logged);  // undefined

作用域概念

作用域决定了变量和函数的可访问性。JavaScript 有三种作用域:

全局作用域

let globalVar = '全局变量';

function showGlobal() {
  console.log(globalVar);  // 可以访问全局变量
}

showGlobal();  // 输出:全局变量
console.log(globalVar);  // 输出:全局变量

函数作用域

function testFunctionScope() {
  let localVar = '局部变量';
  console.log(localVar);  // 可以访问局部变量
  console.log(globalVar);  // 可以访问全局变量
}

testFunctionScope();
// console.log(localVar);  // 错误:无法访问函数局部变量

块级作用域

function testBlockScope() {
  if (true) {
    let blockVar = '块级变量';
    console.log(blockVar);  // 可以访问块级变量
  }
  
  // console.log(blockVar);  // 错误:无法访问块级变量
}

testBlockScope();

作用域链

作用域链是 JavaScript 查找变量的机制。当访问一个变量时,JavaScript 会从当前作用域开始,逐级向外查找,直到找到变量或到达全局作用域。

作用域链示意图:

全局作用域
├── globalVar = '全局变量'
└── 函数作用域
    ├── localVar = '局部变量'
    └── 块级作用域
        └── blockVar = '块级变量'

变量查找顺序:
1. 当前作用域
2. 外层作用域
3. 更外层作用域
4. ...直到全局作用域
let globalVar = '全局变量';

function outerFunction() {
  let outerVar = '外层变量';
  
  function innerFunction() {
    let innerVar = '内层变量';
    
    console.log(innerVar);  // 内层变量(当前作用域)
    console.log(outerVar);  // 外层变量(外层作用域)
    console.log(globalVar); // 全局变量(全局作用域)
  }
  
  innerFunction();
}

outerFunction();

闭包

闭包是指函数能够记住并访问其词法作用域中的变量,即使函数在其词法作用域之外执行。

function createCounter() {
  let count = 0;
  
  return function() {
    count++;
    return count;
  };
}

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1());  // 1
console.log(counter1());  // 2
console.log(counter2());  // 1(独立的计数器)
console.log(counter1());  // 3

闭包的实际应用

// 1. 数据私有化
function createPerson(name) {
  let age = 0;
  
  return {
    getName: () => name,
    getAge: () => age,
    setAge: (newAge) => {
      if (newAge >= 0) {
        age = newAge;
      }
    },
    incrementAge: () => {
      age++;
    }
  };
}

const person = createPerson('张三');
console.log(person.getName());  // 张三
console.log(person.getAge());   // 0
person.setAge(25);
console.log(person.getAge());   // 25
person.incrementAge();
console.log(person.getAge());   // 26
// person.age = -10;  // 无法直接访问 age 变量

// 2. 函数工厂
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

// 3. 缓存函数
function memoize(fn) {
  const cache = {};
  
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      console.log('从缓存获取:', key);
      return cache[key];
    }
    
    const result = fn(...args);
    cache[key] = result;
    console.log('计算并缓存:', key);
    return result;
  };
}

function expensiveCalculation(n) {
  console.log('执行复杂计算...');
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += i;
  }
  return result;
}

const memoizedCalc = memoize(expensiveCalculation);

console.log(memoizedCalc(100));  // 执行计算
console.log(memoizedCalc(100));  // 从缓存获取
console.log(memoizedCalc(100));  // 从缓存获取

高阶函数

高阶函数是指接受函数作为参数或返回函数的函数。

// 接受函数作为参数
function calculate(a, b, operation) {
  return operation(a, b);
}

const add = (x, y) => x + y;
const multiply = (x, y) => x * y;

console.log(calculate(5, 3, add));      // 8
console.log(calculate(5, 3, multiply)); // 15

// 返回函数
function createGreeter(greeting) {
  return function(name) {
    return `${greeting},${name}!`;
  };
}

const sayHello = createGreeter('你好');
const sayGoodbye = createGreeter('再见');

console.log(sayHello('张三'));   // 你好,张三!
console.log(sayGoodbye('李四')); // 再见,李四!

递归函数

递归函数是指函数调用自身:

// 计算阶乘
function factorial(n) {
  if (n <= 1) {
    return 1;
  }
  return n * factorial(n - 1);
}

console.log(factorial(5));  // 120

// 计算斐波那契数列
function fibonacci(n) {
  if (n <= 1) {
    return n;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(10));  // 55

// 递归深度限制
function safeFactorial(n, depth = 0, maxDepth = 1000) {
  if (depth > maxDepth) {
    throw new Error('递归深度超过限制');
  }
  if (n <= 1) {
    return 1;
  }
  return n * safeFactorial(n - 1, depth + 1, maxDepth);
}

实用案例分析

案例 1:函数式编程工具库

创建一个实用的函数式编程工具库:

// 工具函数库
const utils = {
  // 数组工具
  map: (array, fn) => array.map(fn),
  filter: (array, fn) => array.filter(fn),
  reduce: (array, fn, initial) => array.reduce(fn, initial),
  find: (array, fn) => array.find(fn),
  
  // 函数工具
  compose: (...fns) => (x) => fns.reduceRight((v, f) => f(v), x),
  pipe: (...fns) => (x) => fns.reduce((v, f) => f(v), x),
  
  // 类型检查
  isString: (value) => typeof value === 'string',
  isNumber: (value) => typeof value === 'number',
  isArray: (value) => Array.isArray(value),
  isObject: (value) => typeof value === 'object' && value !== null,
  
  // 字符串工具
  capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
  truncate: (str, length) => str.length > length ? str.slice(0, length) + '...' : str,
  
  // 数字工具
  clamp: (num, min, max) => Math.min(Math.max(num, min), max),
  random: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
};

// 使用示例
const numbers = [1, 2, 3, 4, 5];

// 使用 map
const doubled = utils.map(numbers, n => n * 2);
console.log(doubled);  // [2, 4, 6, 8, 10]

// 使用 filter
const evens = utils.filter(numbers, n => n % 2 === 0);
console.log(evens);  // [2, 4]

// 使用 reduce
const sum = utils.reduce(numbers, (acc, n) => acc + n, 0);
console.log(sum);  // 15

// 使用 compose
const addOne = n => n + 1;
const multiplyByTwo = n => n * 2;
const composed = utils.compose(multiplyByTwo, addOne);
console.log(composed(5));  // 12 (5 + 1) * 2

// 使用 pipe
const piped = utils.pipe(addOne, multiplyByTwo);
console.log(piped(5));  // 12 (5 + 1) * 2

案例 2:事件处理器工厂

创建一个事件处理器工厂,用于处理用户交互:

function createEventHandler() {
  let eventCount = 0;
  const eventLog = [];
  
  return {
    handleClick: (element, callback) => {
      element.addEventListener('click', (event) => {
        eventCount++;
        eventLog.push({
          type: 'click',
          target: element.tagName,
          timestamp: new Date()
        });
        callback(event);
      });
    },
    
    handleSubmit: (form, callback) => {
      form.addEventListener('submit', (event) => {
        event.preventDefault();
        eventCount++;
        eventLog.push({
          type: 'submit',
          formData: new FormData(form),
          timestamp: new Date()
        });
        callback(event);
      });
    },
    
    getStats: () => ({
      totalEvents: eventCount,
      recentEvents: eventLog.slice(-10)
    }),
    
    clearLog: () => {
      eventLog.length = 0;
    }
  };
}

// 使用示例(在浏览器环境中)
const handler = createEventHandler();

// 假设有一个按钮
const button = document.getElementById('myButton');
handler.handleClick(button, (event) => {
  console.log('按钮被点击了!');
});

// 假设有一个表单
const form = document.getElementById('myForm');
handler.handleSubmit(form, (event) => {
  console.log('表单被提交了!');
});

// 查看统计信息
console.log(handler.getStats());

案例 3:数据验证器

创建一个可配置的数据验证器:

function createValidator() {
  const validators = {};
  
  return {
    addRule: (name, validatorFn, errorMessage) => {
      validators[name] = {
        validate: validatorFn,
        message: errorMessage
      };
    },
    
    validate: (data, rules) => {
      const errors = [];
      
      for (const field in rules) {
        const fieldRules = rules[field];
        const value = data[field];
        
        for (const rule of fieldRules) {
          if (typeof rule === 'string') {
            const validator = validators[rule];
            if (validator && !validator.validate(value)) {
              errors.push({
                field,
                message: validator.message
              });
              break;
            }
          } else if (typeof rule === 'function') {
            const result = rule(value);
            if (result !== true) {
              errors.push({
                field,
                message: result
              });
              break;
            }
          }
        }
      }
      
      return {
        isValid: errors.length === 0,
        errors
      };
    }
  };
}

// 创建验证器实例
const validator = createValidator();

// 添加验证规则
validator.addRule('required', (value) => value !== undefined && value !== null && value !== '', 
  '此字段为必填项');
validator.addRule('email', (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  '请输入有效的邮箱地址');
validator.addRule('minLength', (value, min) => value && value.length >= min,
  (value, min) => `至少需要 ${min} 个字符`);
validator.addRule('maxLength', (value, max) => value && value.length <= max,
  (value, max) => `最多 ${max} 个字符`);
validator.addRule('min', (value, min) => value >= min,
  (value, min) => `不能小于 ${min}`);
validator.addRule('max', (value, max) => value <= max,
  (value, max) => `不能大于 ${max}`);

// 使用验证器
const userData = {
  username: '张三',
  email: 'zhangsan@example.com',
  age: 25
};

const validationRules = {
  username: ['required', (value) => value.length >= 2 || '用户名至少2个字符'],
  email: ['required', 'email'],
  age: ['required', (value) => value >= 18 || '必须年满18岁', (value) => value <= 120 || '年龄不能超过120岁']
};

const result = validator.validate(userData, validationRules);

if (result.isValid) {
  console.log('数据验证通过!');
} else {
  console.log('验证失败:');
  result.errors.forEach(error => {
    console.log(`- ${error.field}: ${error.message}`);
  });
}

代码示例

示例 1:作用域链演示

let globalVar = '全局变量';

function level1() {
  let level1Var = '第一层变量';
  
  function level2() {
    let level2Var = '第二层变量';
    
    function level3() {
      let level3Var = '第三层变量';
      
      console.log('在 level3 中:');
      console.log('level3Var:', level3Var);  // 第三层变量
      console.log('level2Var:', level2Var);  // 第二层变量
      console.log('level1Var:', level1Var);  // 第一层变量
      console.log('globalVar:', globalVar);  // 全局变量
    }
    
    level3();
    
    console.log('\n在 level2 中:');
    console.log('level2Var:', level2Var);  // 第二层变量
    console.log('level1Var:', level1Var);  // 第一层变量
    console.log('globalVar:', globalVar);  // 全局变量
    // console.log(level3Var);  // 错误:无法访问
  }
  
  level2();
  
  console.log('\n在 level1 中:');
  console.log('level1Var:', level1Var);  // 第一层变量
  console.log('globalVar:', globalVar);  // 全局变量
  // console.log(level2Var);  // 错误:无法访问
  // console.log(level3Var);  // 错误:无法访问
}

level1();

console.log('\n在全局中:');
console.log('globalVar:', globalVar);  // 全局变量
// console.log(level1Var);  // 错误:无法访问
// console.log(level2Var);  // 错误:无法访问
// console.log(level3Var);  // 错误:无法访问

示例 2:闭包内存管理

// 不好的闭包使用(可能导致内存泄漏)
function createBadClosure() {
  const largeArray = new Array(1000000).fill('data');
  
  return function() {
    console.log('函数被调用');
  };
}

const badClosure = createBadClosure();
// largeArray 仍然被占用,即使不再使用

// 好的闭包使用
function createGoodClosure() {
  const largeArray = new Array(1000000).fill('data');
  
  // 使用大数组
  const result = largeArray.slice(0, 10);
  
  // 清除大数组的引用
  // largeArray = null;  // 在函数结束时自动清除
  
  return function() {
    console.log('函数被调用');
    console.log('结果:', result);
  };
}

const goodClosure = createGoodClosure();
// largeArray 已被垃圾回收

// 使用弱引用避免内存泄漏(在支持的环境中)
function createWeakClosure() {
  const weakMap = new WeakMap();
  const data = { value: '重要数据' };
  weakMap.set(data, '关联数据');
  
  return function() {
    const result = weakMap.get(data);
    console.log('获取数据:', result);
  };
}

const weakClosure = createWeakClosure();

示例 3:函数柯里化

// 柯里化:将多参数函数转换为单参数函数序列
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return (...moreArgs) => curried(...args, ...moreArgs);
  };
}

// 原始函数
function add(a, b, c) {
  return a + b + c;
}

// 柯里化后的函数
const curriedAdd = curry(add);

// 逐步调用
console.log(curriedAdd(1)(2)(3));  // 6

// 部分应用
const add1 = curriedAdd(1);
const add1and2 = add1(2);
console.log(add1and2(3));  // 6

// 实际应用
function multiply(a, b) {
  return a * b;
}

const curriedMultiply = curry(multiply);
const double = curriedMultiply(2);
const triple = curriedMultiply(3);

console.log(double(5));   // 10
console.log(triple(5));   // 15

// 创建特定领域的函数
function calculatePrice(price, tax, discount) {
  return price * (1 + tax) * (1 - discount);
}

const curriedCalculate = curry(calculatePrice);
const calculateWithTax = curriedCalculate(0.1);  // 10% 税
const calculateWithTaxAndDiscount = calculateWithTax(0.2);  // 20% 折扣

console.log(calculateWithTaxAndDiscount(100));  // 100 * 1.1 * 0.8 = 88

实现技巧与注意事项

函数设计原则

  1. 单一职责:每个函数只做一件事
  2. 纯函数:避免副作用,相同输入总是产生相同输出
  3. 有意义的命名:函数名应该清楚地描述其功能
  4. 适当的参数数量:避免参数过多,考虑使用对象参数

闭包使用注意事项

  1. 内存管理:闭包会保持对外部变量的引用,可能导致内存泄漏
  2. 性能考虑:频繁创建闭包可能影响性能
  3. 代码可读性:过度使用闭包可能使代码难以理解

递归使用注意事项

  1. 基准条件:确保递归有明确的终止条件
  2. 递归深度:注意递归深度,避免栈溢出
  3. 尾递归优化:在支持的环境中,使用尾递归可以提高性能

作用域最佳实践

  1. 最小化作用域:在尽可能小的作用域中声明变量
  2. 避免全局污染:尽量减少全局变量的使用
  3. 使用 const 和 let:优先使用 const,需要重新赋值时使用 let

常见问题与解决方案

问题 1:闭包导致的意外行为

// 问题代码
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);  // 输出:5, 5, 5, 5, 5
  }, 100);
}

// 解决方案 1:使用 let
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);  // 输出:0, 1, 2, 3, 4
  }, 100);
}

// 解决方案 2:使用闭包
for (var i = 0; i < 5; i++) {
  (function(index) {
    setTimeout(() => {
      console.log(index);  // 输出:0, 1, 2, 3, 4
    }, 100);
  })(i);
}

问题 2:this 指向问题

// 问题代码
const obj = {
  value: 42,
  getValue: function() {
    return this.value;
  }
};

const unboundGetValue = obj.getValue;
console.log(unboundGetValue());  // undefined

// 解决方案 1:使用 bind
const boundGetValue = obj.getValue.bind(obj);
console.log(boundGetValue());  // 42

// 解决方案 2:使用箭头函数
const obj2 = {
  value: 42,
  getValue: () => this.value
};

console.log(obj2.getValue());  // 42

问题 3:递归导致的栈溢出

// 问题代码
function deepRecursion(n) {
  if (n <= 0) return 0;
  return n + deepRecursion(n - 1);
}

// deepRecursion(100000);  // 栈溢出

// 解决方案:使用迭代
function iterativeSum(n) {
  let sum = 0;
  for (let i = 0; i <= n; i++) {
    sum += i;
  }
  return sum;
}

console.log(iterativeSum(100000));  // 正常执行

// 或使用尾递归优化
function tailRecursiveSum(n, acc = 0) {
  if (n <= 0) return acc;
  return tailRecursiveSum(n - 1, acc + n);
}

问题 4:函数参数过多

// 问题代码
function createUser(name, age, email, phone, address, city, country, postalCode) {
  // 参数过多,难以维护
}

// 解决方案:使用对象参数
function createUser(options) {
  const {
    name,
    age,
    email,
    phone,
    address,
    city,
    country,
    postalCode
  } = options;
  
  // 更清晰、更易维护
}

// 使用
createUser({
  name: '张三',
  age: 25,
  email: 'zhangsan@example.com',
  phone: '1234567890',
  address: '北京市朝阳区',
  city: '北京',
  country: '中国',
  postalCode: '100000'
});

总结

本教程深入讲解了 JavaScript 的函数与作用域概念,包括函数定义、参数传递、作用域链和闭包等重要内容。这些概念是 JavaScript 编程的基础,对于理解 Node.js 的异步编程和模块系统至关重要。

通过本集的学习,您应该能够:

  1. 使用多种方式定义函数
  2. 理解函数参数的传递机制
  3. 掌握作用域链的工作原理
  4. 理解闭包的概念和应用场景
  5. 使用高阶函数和递归解决问题
  6. 避免常见的函数编程错误

在下一集中,我们将学习 JavaScript 的数组与对象操作,这是数据处理的核心内容。继续加油,您的 JavaScript 基础正在不断巩固!

« 上一篇 JavaScript 基础回顾 下一篇 » JavaScript 数组与对象