Javascript 闭包与高阶函数

9 阅读8分钟

一、闭包(Closure)

1.什么是闭包?

闭包是指函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。简单说,就是内部函数可以访问外部函数的变量。

2.理解作用域

JavaScript 是 词法作用域(Lexical Scope)

变量的作用域在定义时决定,而不是在调用时决定。

function outer() {
  let a = 10;

  function inner() {
    console.log(a);
  }

  inner();
}

outer(); // 10

inner 能访问 outer 里的 a,因为它定义在 outer 里面。

闭包的基本示例

function createCounter() {
  let count = 0; // 私有变量
  
  return function inner() {
    count++;
    return count;
  };
}

const counter1 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter1()); // 3

const counter2 = createCounter();
console.log(counter2()); // 1 (独立的计数器)

打印结果

发生了什么?

当 createCounter() 执行结束后:

  • 按理说 count 应该被销毁
  • 但是 inner 还在引用它
  • JS 引擎不会回收这个变量

👉 因为 inner 形成了闭包

3. 闭包的执行过程(原理)

当函数创建时,会:

  1. 创建一个执行上下文
  2. 生成一个词法环境
  3. 内部函数会保存对这个词法环境的引用

只要内部函数还存在,这个环境就不会被垃圾回收。

4. 闭包的关键特征

  • 函数嵌套函数:通常是一个外层函数包裹一个内层函数。
  • 内层函数引用外层函数的变量:这是形成闭包的必要条件。
  • 外层函数将内层函数返回(或传递出去) :使得内层函数可以在其原始作用域之外被调用。

5. 闭包的常见用途

1.数据私有化(封装)

function createUser() {
  let password = "123456";

  return {
    checkPassword(input) {
      return input === password;
    }
  };
}

const user = createUser();
console.log(user.checkPassword("123456")); // true

打印结果:

外部访问不到 password

2. 防抖/节流

function debounce(fn, delay) {
  let timer;

  return function () {
    clearTimeout(timer);
    timer = setTimeout(fn, delay);
  };
}

timer就是被闭包保存的

3.循环中的经典坑(经典面试题)

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

打印结果:

原因:
var 声明的 i 是函数作用域(或全局作用域),3个定时器回调共享同一个 i。当循环结束时,i 已经是 3,所以每个回调都打印 3。

解决方案:

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

使用let:

let 声明的变量是块级作用域,每次循环都会创建一个新的 i 绑定,效果等同于用闭包保存了状态。

或者使用

for (var i = 0; i < 3; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i);
    }, 1000);
  })(i);
}

打印结果:

这个就是利用闭包锁住变量

6.闭包的优缺点

优点
  • 数据私有化
  • 模块化开发
  • 避免全局变量污染
  • 实现函数式编程模式
缺点
  • 容易造成内存泄漏(长期引用大对象)
  • 不合理使用会影响性能
  • 变量长期存在难以调试

7.面试总结版

  • 闭包是指函数可以访问其定义时的词法作用域,即使外部函数已经执行结束。
  • 本质是函数持有对外部变量的引用。
  • 常见应用场景包括数据私有化、计数器、防抖节流等。

二、高阶函数

1.什么是高阶函数?

在 JavaScript 里,函数是一等公民(First-Class Function)

  • 可以赋值给变量
  • 可以作为参数传入
  • 可以作为返回值返回

高阶函数是指至少满足以下条件之一的函数:

  1. 接受一个或多个函数作为参数
  2. 返回一个函数作为结果

简单说:操作其他函数的函数

2. 常见的高阶函数示例

示例1:函数作为参数(回调函数)

function greet(name) {
  return "Hello " + name;
}

function processUserInput(callback) {
  const name = "Mike";
  console.log(callback(name));
}

processUserInput(greet);

打印结果:

示例2:函数作为返回值

function multiplier(factor) {
  console.log("打印factor:", factor); //打印 2 3
  return function (number) {
    console.log("打印number:", number); //打印 5 6
    return number * factor;
  };
}

const double = multiplier(2);
const triole = multiplier(3);

console.log(double(5)); //10
console.log(triole(6)); //18

打印结果:

示例3.经典数组方法

map() - 映射/转换
const numbers = [1, 2, 3, 4, 5];

// 每个元素乘以2
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// 转换为对象数组
const objects = numbers.map(num => ({ value: num }));
console.log(objects);
// [{value: 1}, {value: 2}, {value: 3}, {value: 4}, {value: 5}]

filter() - 过滤
const numbers = [1, 2, 3, 4, 5, 6];

// 获取偶数
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4, 6]

// 获取大于3的数
const greaterThan3 = numbers.filter(num => num > 3);
console.log(greaterThan3); // [4, 5, 6]

reduce() - 归约/累积
const numbers = [1, 2, 3, 4, 5];

// 求和
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum); // 15

// 求最大值
const max = numbers.reduce((acc, curr) => Math.max(acc, curr), -Infinity);
console.log(max); // 5

// 数组转对象
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

const usersById = users.reduce((acc, user) => {
  acc[user.id] = user;
  return acc;
}, {});

console.log(usersById);
// {
//   1: { id: 1, name: 'Alice' },
//   2: { id: 2, name: 'Bob' }
// }

forEach() - 遍历
const fruits = ['apple', 'banana', 'orange'];

fruits.forEach((fruit, index) => {
  console.log(`${index + 1}. ${fruit}`);
});
// 1. apple
// 2. banana
// 3. orange

find() / findIndex() - 查找
const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Alice' }
];

const alice = users.find(user => user.name === 'Alice');
console.log(alice); // { id: 1, name: 'Alice' }

const aliceIndex = users.findIndex(user => user.name === 'Alice');
console.log(aliceIndex); // 0

some() / every() - 条件检查
const numbers = [1, 2, 3, 4, 5];

// 是否有偶数?
const hasEven = numbers.some(num => num % 2 === 0);
console.log(hasEven); // true

// 是否所有数都大于0?
const allPositive = numbers.every(num => num > 0);
console.log(allPositive); // true

3.自定义高阶函数

1.什么是自定义高阶函数

自定义高阶函数,本质就是:

用函数去增强、控制、复用另一个函数

一般分三类:

  1. 函数增强(装饰)
  2. 行为抽象(解耦)
  3. 执行控制(节流、防抖等)

2.最经典:函数增强(装饰器思想)

function before(fn, beforeFn) {
  return function (...args) {
    beforeFn.apply(this, args);  // 先执行 beforeFn
    return fn.apply(this, args);  // 再执行原始函数 fn
  };
}

function after(fn, afterFn) {
  return function (...args) {
    const result = fn.apply(this, args);  // 先执行原始函数 fn
    afterFn.apply(this, args);  // 再执行 afterFn
    return result;  // 返回原始函数的结果
  };
}

function say(name) {
  console.log("Hello " + name);
}

const newSay = before(say, function () {
  console.log("准备执行...");
});

newSay("Jake");

Before函数:

**调用 newSay("Jake") 时:

  1. 执行 beforeFn() → "准备执行..."
  2. 执行 say("Jake") → "Hello Jake"
  3. 返回 say 的返回值(undefined)**

After 函数:

**调用 afterFn 装饰的函数时:

  1. 执行原始函数 fn → 得到结果
  2. 执行 afterFn()
  3. 返回 fn 的结果**

打印结果:

3.执行控制型高阶函数

执行控制型高阶函数是指那些能够控制函数执行时机、频率或条件的高阶函数,它们在前端开发中非常实用。

3.1 自定义节流函数
function throttle(fn, delay) {
  let lastTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime > delay) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log("滚动触发");
  }, 1000)
);

3.2 自定义防抖函数
function debounce(fn, delay) {
  let timer = null;

  return function (...args) {
    clearTimeout(timer);

    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

4.行为抽象型高阶函数

指将程序中具体的操作逻辑(行为)提取出来,使其不依赖于具体的数据,而是通过参数(特别是函数参数)来定义。这使得代码更通用、更可复用。

4.1 抽象重复逻辑
function withLoading(fn) {
  return async function (...args) {
    console.log("loading...");
    try {
      const result = await fn(...args);
      return result;
    } finally {
      console.log("loading end");
    }
  };
}


async function fetchData() {
  return "数据";
}

const newFetch = withLoading(fetchData);

newFetch();

打印结果:

5.函数柯里化(进阶自定义高阶函数)

函数柯里化是一种将接受多个参数的函数转换为接受单个参数(或更少参数)的函数序列的技术。

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return function (...nextArgs) {
        return curried(...args, ...nextArgs);
      };
    }
  };
}


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

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6

打印结果:

6.链式增强

链式增强是 JavaScript 中一种强大的编程模式,它通过方法链(Method Chaining)让代码更加流畅、可读。这种模式特别适用于数据处理、DOM 操作和构建复杂配置。

Function.prototype.before = function (beforeFn) {
  const self = this;
  return function (...args) {
    beforeFn.apply(this, args);
    return self.apply(this, args);
  };
};


function say() {
  console.log("hello");
}

say = say.before(function () {
  console.log("before...");
});

say();

打印结果:

7.自定义高阶函数的本质结构

function higherOrder(fn) {
  return function (...args) {
    // 1. 执行前逻辑

    const result = fn.apply(this, args);

    // 2. 执行后逻辑

    return result;
  };
}

8.作为前端开发你必须掌握的几个自定义高阶函数

  • before / after
  • throttle
  • debounce
  • curry
  • once(只执行一次)
  • memoize(缓存结果)

比如 memoize:

function memoize(fn) {
  const cache = {};

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache[key]) {
      return cache[key];
    }

    const result = fn.apply(this, args);
    cache[key] = result;

    return result;
  };
}

9.终极理解(非常重要)

高阶函数解决的问题是:

  • 不修改原函数
  • 不侵入原逻辑
  • 动态增强功能

这就是:

  • AOP
  • 装饰器模式
  • 中间件机制
  • React HOC
  • Vue 插件机制

的底层原理。

🌱 写在最后
写作这些技术文章的过程,也是我重新梳理知识体系的过程。
编程不仅是实现功能,更是一种思考方式。希望这篇文章不仅能帮你解决眼前的问题,更能激发你对[某个技术点]的更深层思考。
最后:保持好奇,保持热爱。