Skip to content

函数与闭包

函数

是什么

JavaScript 中的函数(Function)是一种封装了一段可以执行任务或计算值的代码块。函数在 JavaScript 中是一种基本的构建块,它允许你定义代码一次,然后在需要的时候多次调用执行。

函数可以带有参数,这些参数相当于函数执行时的输入值,它们可以在函数体内被使用。函数还可以返回一个值,这个值可以是任何 JavaScript 数据类型的值,包括数字、字符串、对象或者甚至是另一个函数。

下面是一个简单的 JavaScript 函数示例:

javascript
// 定义一个名为 "sayHello" 的函数,它接受一个参数 "name"
function sayHello(name) {
  alert("Hello, " + name + "!");
}

// 调用函数,并传递参数 "Alice"
sayHello("Alice"); // 弹出一个对话框显示 "Hello, Alice!"

函数可以通过几种不同的方式来定义:

  1. 函数声明(Function Declaration):
javascript
function myFunction(a, b) {
  return a + b;
}
  1. 函数表达式(Function Expression):
javascript
const myFunction = function (a, b) {
  return a + b;
};
  1. 箭头函数(Arrow Function), ES6 引入的新语法:
javascript
const myFunction = (a, b) => {
  return a + b;
};

// 对于单一表达式,可以省略花括号和 return 关键字
const myFunction = (a, b) => a + b;

函数是 JavaScript 中的一等公民(First-class citizens),意味着它们可以像其他值一样被赋值给变量、作为参数传递给其他函数、甚至可以作为其他函数的返回值。这种特性使得 JavaScript 支持高阶函数(Higher-order functions),这是函数式编程的一个重要概念。

知识点

JavaScript 函数是非常强大和灵活的,涉及到多个关键的知识点。以下是一些核心的知识点:

  1. 函数声明与表达式:

    • 函数声明(Function Declaration)定义一个命名的函数。
    • 函数表达式(Function Expression)定义一个作为表达式的函数,可以是匿名的,也可以是命名的。
  2. 箭头函数:

    • ES6 引入了箭头函数,它提供了一种更简洁的函数写法,并且不绑定自己的 thisargumentssupernew.target
  3. 函数参数:

    • 默认参数:允许在函数定义时给参数赋予默认值。
    • 剩余参数:使用 ... 语法允许将不确定数量的参数作为一个数组传入。
    • 参数解构:允许直接在参数定义中解构对象或数组。
  4. 返回值:

    • 函数可以返回值,使用 return 语句。如果没有 return 语句,函数默认返回 undefined
  5. 作用域(Scope):

    • 函数创建自己的作用域,变量在函数内声明时只能在函数内部访问。
    • 闭包:函数可以记住并访问它创建时所在的作用域,即使函数在外部执行。
  6. 提升(Hoisting):

    • 函数声明会被提升到作用域的顶部,意味着可以在声明之前调用函数。
    • 函数表达式(包括箭头函数)不会被提升。
  7. this 关键字:

    • 在函数中,this 关键字的值取决于函数的调用方式。
    • 箭头函数没有自己的 this,它会捕获其所在上下文的 this 值。
  8. 立即调用的函数表达式(IIFE):

    • IIFE 是一个在定义后立即执行的函数,常用于创建一个独立的作用域。
  9. 高阶函数:

    • 函数可以作为参数传递给其他函数,或者作为其他函数的返回值。
  10. 回调函数:

    • 一个函数可以作为参数传递给另一个函数,然后在某个事件或条件发生后被调用。
  11. 递归:

    • 函数可以调用自己,用于处理树形结构数据、执行重复任务等。
  12. 异步函数:

    • 使用 asyncawait 关键字可以编写异步代码,这实际上是基于 Promises 的语法糖。
  13. 构造函数与 new 关键字:

    • 函数可以用作构造函数,与 new 关键字一起使用来创建新的对象实例。
  14. 方法:

    • 函数可以作为对象的属性,即方法,可以使用 this 访问其所属对象的属性。
  15. 严格模式:

    • 在函数体内部使用 'use strict' 可以启用严格模式,这有助于捕获潜在的错误,如未声明的变量等。
  16. 函数的属性和方法:

    • 函数是对象,有自己的属性和方法,如 length 属性表示参数的数量,callapplybind 方法用于指定函数的 this 值和参数。
  17. 生成器函数:

    • 使用 function* 语法定义的函数,可以通过 yield 关键字暂停和恢复执行。
  18. 错误处理:

    • 在函数内部可以使用 try...catch 语句来处理错误和异常。

这些知识点构成了 JavaScript 函数的基础,并在日常编程中广泛使用。掌握这些概念对于理解和使用 JavaScript 函数至关重要。

使用场景

在 JavaScript 中,函数用于执行各种各样的任务。以下是一些常见的使用场景,包括示例:

  1. 封装代码以重用: 函数允许你将代码封装起来,以便在多个地方重用,而不必复制和粘贴相同的代码。

    javascript
    function calculateArea(width, height) {
      return width * height;
    }
    
    let area1 = calculateArea(10, 20);
    let area2 = calculateArea(5, 7);
  2. 事件处理: 在 Web 开发中,函数经常用于响应用户事件,如点击、鼠标移动、按键等。

    javascript
    function handleClick(event) {
      console.log("Button clicked!", event);
    }
    
    document.getElementById("myButton").addEventListener("click", handleClick);
  3. 回调函数: 函数可以作为参数传递给其他函数(称为回调函数),在某个操作完成时执行。

    javascript
    function processData(data, callback) {
      // 假设这里有一些数据处理
      let processedData = data + " processed";
      callback(processedData);
    }
    
    processData("raw data", function (result) {
      console.log(result); // 输出: raw data processed
    });
  4. 立即调用的函数表达式 (IIFE): 一种模式是立即调用函数表达式,这在创建一个私有作用域时很有用。

    javascript
    (function () {
      let privateVar = "I am private";
      console.log(privateVar);
    })();
    // privateVar 在这个匿名函数外部是无法访问的
  5. 返回函数的函数(高阶函数): 函数可以返回另一个函数,这在需要创建具有特定功能的新函数时很有用。

    javascript
    function makeMultiplier(multiplier) {
      return function (value) {
        return value * multiplier;
      };
    }
    
    let double = makeMultiplier(2);
    console.log(double(5)); // 输出: 10
  6. 数组方法中的函数: JavaScript 数组有许多内置方法,如 map, filter, reduce 等,这些方法都使用了回调函数。

    javascript
    let numbers = [1, 2, 3, 4, 5];
    let squaredNumbers = numbers.map(function (number) {
      return number * number;
    });
    console.log(squaredNumbers); // 输出: [1, 4, 9, 16, 25]
  7. 异步编程: 在处理异步编程,如使用 setTimeout 或与 Promises 交互时,函数也是必不可少的。

    javascript
    setTimeout(function () {
      console.log("This message is shown after 1 second.");
    }, 1000);
  8. 构造函数: 在 ES6 之前,JavaScript 使用构造函数模式来模拟类的行为。

    javascript
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    Person.prototype.greet = function () {
      console.log("Hello, my name is " + this.name + "!");
    };
    
    let person1 = new Person("Alice", 25);
    person1.greet(); // 输出: Hello, my name is Alice!

这些例子展示了 JavaScript 函数的灵活性和多功能性,它们是编写 JavaScript 代码的基础。随着 ES6 和后续版本的发展,JavaScript 提供了更多高级的函数特性,如箭头函数、默认参数、剩余参数、扩展运算符等,进一步增强了函数的能力。

当然,除了上述提到的使用场景,JavaScript 函数还有更多的应用方式。补充一些其他的场景和示例:

  1. 模块化代码: 函数可以帮助你将代码分割成模块,这样可以更容易地维护和管理代码,也便于实现代码的复用和测试。

    javascript
    // math.js
    function add(x, y) {
      return x + y;
    }
    
    function subtract(x, y) {
      return x - y;
    }
    
    export { add, subtract };
    
    // main.js
    import { add, subtract } from "./math.js";
    
    console.log(add(5, 3)); // 输出: 8
    console.log(subtract(5, 3)); // 输出: 2
  2. 闭包: 函数可以创建闭包,即即使创建它的上下文已经销毁,它仍然能够记住并访问所在的词法作用域中的变量。

    javascript
    function createCounter() {
      let count = 0;
      return function () {
        count += 1;
        return count;
      };
    }
    
    let counter = createCounter();
    console.log(counter()); // 输出: 1
    console.log(counter()); // 输出: 2
  3. 递归: 函数可以调用自身,这称为递归。递归可以用来解决分而治之的问题,如树的遍历、排序算法等。

    javascript
    function factorial(n) {
      if (n === 0 || n === 1) {
        return 1;
      } else {
        return n * factorial(n - 1);
      }
    }
    
    console.log(factorial(5)); // 输出: 120
  4. 函数作为对象的方法: 在 JavaScript 中,函数也可以作为对象的属性,这时它们被称为方法。

    javascript
    let person = {
      name: "Alice",
      greet: function () {
        console.log("Hello, my name is " + this.name + "!");
      },
    };
    
    person.greet(); // 输出: Hello, my name is Alice!
  5. 默认参数和剩余参数: ES6 引入了默认参数和剩余参数,使得函数定义更加灵活。

    javascript
    // 默认参数
    function greet(name = "Guest") {
      console.log("Hello, " + name + "!");
    }
    
    greet("Alice"); // 输出: Hello, Alice!
    greet(); // 输出: Hello, Guest!
    
    // 剩余参数
    function sum(...numbers) {
      return numbers.reduce((acc, current) => acc + current, 0);
    }
    
    console.log(sum(1, 2, 3, 4)); // 输出: 10
  6. 链式调用: 通过在函数的末尾返回当前对象,可以实现链式调用,这是创建流畅接口(Fluent Interface)的关键。

    javascript
    let calculator = {
      value: 0,
      add(a) {
        this.value += a;
        return this;
      },
      subtract(a) {
        this.value -= a;
        return this;
      },
      multiply(a) {
        this.value *= a;
        return this;
      },
      getResult() {
        return this.value;
      },
    };
    
    let result = calculator.add(5).multiply(2).subtract(3).getResult();
    console.log(result); // 输出: 7
  7. 异步函数(Async Functions): ES2017 引入了 async/await,使得编写异步代码变得更加简洁和直观。

    javascript
    async function fetchData() {
      try {
        let response = await fetch("https://api.example.com/data");
        let data = await response.json();
        console.log(data);
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    }
    
    fetchData();

函数是 JavaScript 中非常强大的特性,它们的灵活性和多样性使得你可以用各种各样的方式来组织和执行代码。上述示例仅仅是冰山一角,随着你对 JavaScript 的深入学习,你会发现更多高级和有趣的函数用法。

闭包

是什么

JavaScript 中的闭包(Closure)是一个非常重要的概念,它允许一个函数访问并操作该函数外部作用域的变量。

闭包产生的情况通常是这样的:

  1. 当一个函数(我们称之为外部函数)声明了另一个函数(内部函数)。
  2. 内部函数可以访问外部函数中的变量。
  3. 外部函数的执行完成后,通常外部函数中的局部变量不再可用,因为生命周期结束了。
  4. 然而,如果内部函数在外部函数执行完毕后仍然存活(比如被返回或者在其他地方被引用),那么内部函数仍然可以访问外部函数的变量。

这种情况下,即使外部函数的执行上下文已经消失,内部函数依然保有外部函数变量的引用,这种引用就是闭包。

这里有一个简单的闭包例子:

javascript
function createCounter() {
  let count = 0;
  return function () {
    // 这里的匿名函数就是闭包,它可以访问外部函数createCounter的变量count
    count += 1;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3

在上面的例子中,函数 createCounter 返回了一个匿名函数,这个匿名函数可以访问 createCounter 函数作用域中的 count 变量。即使 createCounter 函数的执行上下文已经结束,返回的匿名函数仍然可以访问并修改 count 变量,因为闭包使得这个变量被保留下来了。

闭包的用途非常广泛,包括但不限于:

  • 创建私有变量
  • 封装函数
  • 模块化代码
  • 在异步编程中保持对某些变量的引用

需要注意的是,闭包可能会导致内存泄漏,因为闭包中的变量不会在外部函数执行完毕后被垃圾回收机制回收,除非闭包本身被销毁。因此,在使用闭包时要特别注意不要无意中保留了不再需要的大型对象的引用。

知识点

闭包(Closures)是 JavaScript 中一个重要的概念,涉及到作用域、函数和内存管理等多个知识点。以下是一些与闭包相关的关键知识点:

  1. 作用域(Scope)

    • 全局作用域:在代码中任何地方都能访问到的变量。
    • 局部作用域:在特定函数内部定义的变量只能在该函数内部访问。
    • 块级作用域(ES6 引入的 letconst):在特定代码块(如 if 语句或 for 循环)内定义的变量。
  2. 执行上下文(Execution Context)

    • 当函数被调用时,会为该函数创建一个新的执行上下文,包含了该函数的局部作用域。
    • 执行上下文有一个关联的变量对象,它包含了函数的参数、局部变量和函数声明。
  3. 作用域链(Scope Chain)

    • 函数定义时的作用域决定了它可以访问哪些变量,即作用域链。
    • 内部函数可以访问外部函数的变量,但外部函数不能访问内部函数的变量。
  4. 闭包的形成

    • 当一个函数返回另一个函数时,后者会保持对前者作用域的引用,这种结构就形成了闭包。
    • 即使外部函数执行完毕,闭包也可以访问外部函数作用域中的变量。
  5. 闭包的用途

    • 创建私有变量和函数。
    • 封装功能模块,防止全局污染。
    • 实现模块化和模块间通信。
    • 在异步编程中保持对状态的引用。
  6. 内存考量

    • 闭包可能会阻止垃圾回收机制回收不再需要的变量,因此可能导致内存泄漏。
    • 需要适时释放闭包,或者使用闭包时避免捕获大的数据结构。
  7. 立即执行函数表达式(IIFE)

    • 一种常见的模式,使用闭包来创建一个立即执行的函数作用域,避免变量污染全局作用域。
    • 形式如 (function() { /* 代码 */ })();
  8. ES6 中的块级作用域

    • letconst 关键字允许变量在块级作用域中有效,有助于减少闭包带来的问题。
  9. 闭包和循环

    • 在循环中使用闭包时,经典错误是闭包会捕获循环结束时的变量状态,而不是每次迭代时的状态。
    • ES6 的 let 关键字在 for 循环中为每次迭代创建了新的作用域,从而解决了这个问题。

理解闭包的工作原理和它的使用场景对于成为一名高效的 JavaScript 开发者至关重要。正确使用闭包可以编写出更加强大和灵活的代码。

使用场景

闭包在 JavaScript 中有许多实用的使用场景,以下是一些常见的例子:

  1. 数据封装和私有变量

闭包可以用来创建私有变量,使得这些变量不会暴露在全局作用域中,从而避免全局变量污染。

javascript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量,外部无法直接访问
  return {
    deposit: function (amount) {
      balance += amount;
      return balance;
    },
    withdraw: function (amount) {
      if (amount <= balance) {
        balance -= amount;
        return balance;
      } else {
        return "Insufficient funds";
      }
    },
    getBalance: function () {
      return balance;
    },
  };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
account.withdraw(20);
console.log(account.getBalance()); // 130
  1. 模块模式

闭包可以用于模块模式,这是一种软件设计模式,用于进一步封装和组织代码。

javascript
const myModule = (function () {
  let privateVar = "I am private";

  return {
    publicMethod: function () {
      console.log(privateVar);
    },
  };
})();

myModule.publicMethod(); // 输出 "I am private"
// privateVar 在模块外部不可访问
  1. 函数工厂

闭包可以用来创建设置特定信息的函数,这些函数可以访问闭包中的变量。

javascript
function makeMultiplier(multiplier) {
  return function (x) {
    return x * multiplier;
  };
}

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

console.log(double(5)); // 输出 10
console.log(triple(5)); // 输出 15
  1. 在异步操作中保持状态

闭包允许在异步操作(如 setTimeout 或 AJAX 调用)中保持对特定变量的引用。

javascript
function asyncGreeting(name, delay) {
  setTimeout(function () {
    console.log("Hello, " + name);
  }, delay);
}

asyncGreeting("Alice", 1000); // 1秒后输出 "Hello, Alice"
  1. 循环中创建闭包

使用闭包解决经典的循环中的变量作用域问题。

javascript
for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j); // 每个函数都有对应的 j 值的引用
    }, j * 1000);
  })(i);
}
// 输出 1 2 3 4 5,每秒一次
  1. 高阶函数

闭包允许我们使用高阶函数,即函数可以接受函数作为参数或者返回一个函数。

javascript
function filter(array, test) {
  let result = [];
  for (let item of array) {
    if (test(item)) {
      result.push(item);
    }
  }
  return result;
}

const numbers = [1, 2, 3, 4, 5];
const oddNumbers = filter(numbers, function (n) {
  return n % 2 !== 0;
});

console.log(oddNumbers); // 输出 [1, 3, 5]

这些例子展示了闭包在各种情况下的应用,从数据隐藏和封装到在异步编程中保持变量的引用。闭包是 JavaScript 中一个强大的特性,能够提供更多的控制和抽象层次。

当然,闭包的使用场景远不止上述几个。闭包在 JavaScript 中的灵活性使得它可以用在许多其他场合:

  1. 部分应用(Partial Application)

闭包可以用来实现部分应用,即固定一个函数的一些参数,生成一个新的函数。

javascript
function greet(greeting, name) {
  console.log(greeting + ", " + name);
}

function partialApply(fn, ...fixedArgs) {
  return function (...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);
  };
}

const sayHelloTo = partialApply(greet, "Hello");
const sayByeTo = partialApply(greet, "Goodbye");

sayHelloTo("John"); // 输出 "Hello, John"
sayByeTo("John"); // 输出 "Goodbye, John"
  1. 记忆化(Memoization)

闭包可以用来实现记忆化,这是一种优化技术,通过缓存函数的执行结果来提高程序的性能。

javascript
function memoize(fn) {
  const cache = {};
  return function (...args) {
    const key = JSON.stringify(args);
    if (!cache[key]) {
      cache[key] = fn.apply(this, args);
    }
    return cache[key];
  };
}

const factorial = memoize(function (n) {
  if (n === 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
});

console.log(factorial(5)); // 计算并缓存
console.log(factorial(5)); // 直接从缓存中读取结果
  1. 迭代器和生成器

闭包在迭代器和生成器的实现中扮演着关键角色,它们用于控制数据流的迭代过程。

javascript
function makeIterator(array) {
  let nextIndex = 0;
  return {
    next: function () {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { done: true };
    },
  };
}

const it = makeIterator(["yo", "ya"]);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true
  1. 事件处理器和回调

闭包经常用于事件处理和回调函数中,以便于访问其他作用域中的变量。

javascript
function setupButton(buttonId, onClick) {
  const button = document.getElementById(buttonId);
  button.addEventListener("click", function (event) {
    // 这个匿名函数就是一个闭包
    onClick(event);
  });
}

setupButton("myButton", function (event) {
  console.log("Button was clicked!");
});
  1. 柯里化(Currying)

柯里化是将接受多个参数的函数转换成接受一个单一参数的函数的技术,闭包在这个过程中用于保存每次函数调用的参数。

javascript
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

const curriedSum = curry(sum);

console.log(curriedSum(1)(2)(3)); // 输出 6
  1. 配置对象

闭包可以用来创建特定的配置对象,这些对象可以在多个函数调用之间共享和修改状态。

javascript
function createConfig() {
  let config = {};

  return {
    set: function (key, value) {
      config[key] = value;
    },
    get: function (key) {
      return config[key];
    },
  };
}

const myConfig = createConfig();
myConfig.set("language", "en");
console.log(myConfig.get("language")); // 输出 'en'

这些例子进一步展示了闭包的多样性和实用性。无论是函数式编程的概念,如柯里化和部分应用,还是性能优化,如记忆化,闭包都是实现这些概念的基石。此外,闭包在事件处理、迭代器和配置管理等方面也非常有用。正确理解和使用闭包,可以极大地提高 JavaScript 代码的效率和可维护性。

hancenter808@outlook.com