JS:闭包、函数柯里化、工厂函数、偏函数、立即执行函数 相关知识点与面试题

文章目录

  • [一、 闭包](#一、 闭包)
    • [1. 闭包是什么?](#1. 闭包是什么?)
    • [2. 为什么设计闭包?解决了什么问题?](#2. 为什么设计闭包?解决了什么问题?)
    • [3. 闭包使用场景](#3. 闭包使用场景)
      • [A. 模拟私有变量(模块模式)](#A. 模拟私有变量(模块模式))
      • [B. 函数柯里化与偏函数](#B. 函数柯里化与偏函数)
      • [C. 回调函数与异步请求](#C. 回调函数与异步请求)
    • [4. 核心知识点总结](#4. 核心知识点总结)
    • [5. 常见面试题](#5. 常见面试题)
    • 补充知识点
      • [A. 函数工厂 (Function Factory)](#A. 函数工厂 (Function Factory))
      • [B. 函数柯里化 (Currying)](#B. 函数柯里化 (Currying))
      • [C. 偏函数 (Partial Application)](#C. 偏函数 (Partial Application))
      • [JS 高阶函数技巧对比表](#JS 高阶函数技巧对比表)
  • [二、 高阶函数与柯里化](#二、 高阶函数与柯里化)
    • [1. 什么是高阶函数? (Higher-Order Function)](#1. 什么是高阶函数? (Higher-Order Function))
    • [2. 函数柯里化 (Currying)](#2. 函数柯里化 (Currying))
    • [3. 为什么需要它们?(核心价值)](#3. 为什么需要它们?(核心价值))
    • [4. 常见面试题](#4. 常见面试题)
      • [A. 手写题:实现 add(1)(2)(3)](#A. 手写题:实现 add(1)(2)(3))
  • [三、 立即执行函数](#三、 立即执行函数)
    • [1. 什么是立即执行函数 (IIFE)?](#1. 什么是立即执行函数 (IIFE)?)
      • [A. 函数声明 (Function Declaration)](#A. 函数声明 (Function Declaration))
      • [B. 函数表达式 (Function Expression)](#B. 函数表达式 (Function Expression))
    • [2. 为什么需要它?(核心价值)](#2. 为什么需要它?(核心价值))
      • [A. 避免全局污染(最重要)](#A. 避免全局污染(最重要))
      • [B. 减少命名冲突](#B. 减少命名冲突)
    • [3. 应用场景](#3. 应用场景)
      • [A. 模拟块级作用域(ES6 之前的救星)](#A. 模拟块级作用域(ES6 之前的救星))
      • [B. 模块化封装(插件开发)](#B. 模块化封装(插件开发))
    • [4. 相关常见面试题](#4. 相关常见面试题)

一、 闭包

1. 闭包是什么?

一句话定义: 闭包是一个函数以及其捆绑的周边状态(词法环境)的引用。

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建阶段被同时创建。简单来说:闭包让你可以在一个内层函数中访问到其外层函数的作用域。

javascript 复制代码
function outer() {
  let count = 0; // 自由变量
  return function inner() {
    count++; // inner 引用了 outer 的变量
    console.log(count);
  };
}

const counter = outer();
counter(); // 1
counter(); // 2

即使 outer 函数已经执行结束并从调用栈中弹出,由于 inner 函数依然持有对 count 的引用,count 不会被垃圾回收(GC)销毁。


2. 为什么设计闭包?解决了什么问题?

闭包的设计核心是为了解决 变量生命周期控制作用域隔离 的问题。

  • 变量私有化:JS 早期没有私有属性(#)。如果没有闭包,所有变量要么是全局的(易被污染),要么是局部的(执行完即销毁)。

  • 持久化状态:它允许函数在多次调用之间保持状态,而不需要依赖全局变量。

  • 突破作用域限制:让外部环境可以访问函数内部的局部变量(通过返回函数的形式)。

3. 闭包使用场景

A. 模拟私有变量(模块模式)

这是最经典的用法,隐藏内部实现细节,只暴露接口。

javascript 复制代码
const User = (function() {
  let _password = "123"; // 私有变量
  return {
    checkPassword: function(input) {
      return input === _password;
    }
  };
})();

B. 函数柯里化与偏函数

利用闭包预设参数。

javascript 复制代码
function multiplier(factor) {
  return function(num) {
    return num * factor;
  };
}
const double = multiplier(2);
console.log(double(5)); // 10

C. 回调函数与异步请求

在 setTimeout 或 Promise 中,即使外部函数执行完了,回调函数依然能访问当时的变量。

4. 核心知识点总结

  • 词法作用域:JS 采用词法作用域,函数的作用域在定义时就决定了,而不是调用时。

  • 内存消耗 :由于闭包会保持对变量的引用,导致变量不被回收。如果滥用或处理不当,可能导致内存泄漏(解决方法:手动将函数引用置为 null)。

  • 作用域链 :闭包在寻找变量时,会沿着 当前作用域 -> 闭包环境 -> 全局环境 的顺序查找

5. 常见面试题

Q1:循环中的闭包陷阱

这是面试中最常考的,考察对 var 作用域和闭包的理解。

javascript 复制代码
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// 输出结果:4, 4, 4 (因为 var 是函数作用域,循环结束时 i 已变为 4)

// 如何修改输出 1, 2, 3?
// 方案一:用 let (块级作用域)
for (let i = 1; i <= 3; i++) { ... }

// 方案二:利用闭包(立即执行函数 IIFE)
for (var i = 1; i <= 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000);
  })(i);
}

Q2:读代码判断输出

javascript 复制代码
var name = "The Window";
var object = {
  name: "My Object",
  getNameFunc: function() {
    return function() {
      return this.name;
    };
  }
};
console.log(object.getNameFunc()());

解析: 输出 "The Window"。注意!闭包虽然能访问外层作用域的变量,但 this 是动态绑定的。object.getNameFunc()() 相当于直接调用返回的匿名函数,this 指向全局。如果想输出 "My Object",通常需要 var that = this; 利用闭包保存 that。

补充知识点

A. 函数工厂 (Function Factory)

定义: 一个能够根据输入参数,"批量生产"出特定功能函数的函数。

核心: 它是闭包最基础的应用。

javascript 复制代码
function makeMath(operator) {
  return function(a, b) {
    if (operator === 'add') return a + b;
    if (operator === 'multiply') return a * b;
  };
}

const adder = makeMath('add');
const multiplier = makeMath('multiply');

console.log(adder(5, 10));      // 15
console.log(multiplier(5, 10)); // 50

面试点: 函数工厂利用闭包,让返回的内层函数"记住"了 operator 这个配置参数。

B. 函数柯里化 (Currying)

定义: 将一个接收多个参数的函数,转化为一系列接收单个参数(或部分参数)且返回新函数的技巧。

公式: f ( a , b , c ) → f ( a ) ( b ) ( c ) f(a, b, c) \rightarrow f(a)(b)(c) f(a,b,c)→f(a)(b)(c)

javascript 复制代码
// 普通函数
function sum(a, b, c) {
  return a + b + c;
}

// 柯里化版本
function currySum(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

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

为什么用它?

  • 参数复用: 比如第一个参数是固定的校验规则,后面只传待校验的数据。

  • 延迟执行: 只有当所有参数都凑齐时,才真正执行核心逻辑。

C. 偏函数 (Partial Application)

  • 定义: 固定一个函数的部分参数,然后返回一个接收剩余参数的新函数。
  • 公式: f ( a , b , c ) → f ( a , b ) ( c ) f(a, b, c) \rightarrow f(a, b)(c) f(a,b,c)→f(a,b)(c)

它与柯里化的区别:

  • 柯里化是将 1 1 1 个函数拆分成 N N N 个函数,每个只接 1 1 1 个参数。
  • 偏函数是预先固定 M M M 个参数,返回接剩余 N − M N-M N−M 个参数的函数。
javascript 复制代码
// 原函数
function log(date, type, message) {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] ${message}`);
}

// 偏函数应用:固定第一个参数"当前时间"
const logNow = log.bind(null, new Date()); 

logNow("INFO", "这是一条实时日志"); // 只需传剩余 2 个参数

JS 高阶函数技巧对比表

概念 核心动作 侧重点 示例场景 主要优点
函数工厂 内部定义并返回新函数 配置化:根据传入参数定制出功能不同的工具函数 动态创建不同字号的字体设置器、创建不同级别的日志记录器 灵活性高:通过闭包隐藏私有配置,实现"生产线式"的代码复用,减少重复逻辑。
函数柯里化 拆分参数序列(一变多) 链式调用:强调参数是一个一个传入的,实现延迟执行 校验规则组合、数学逻辑拆分 f ( a ) ( b ) ( c ) f(a)(b)(c) f(a)(b)(c) 延迟执行:可以将参数收集与逻辑执行解耦,非常适合函数组合(Compose)和管道处理。
偏函数 提前固定部分参数 预设:减少重复传参,将复杂的 API 简化为更易用的接口 绑定特定的上下文(如 bind)、预设通用的 Ajax 请求头 简化接口:降低函数调用的复杂度,提高代码的可读性和稳定性(固定了易错的参数)。

一句话总结:

函数工厂是设计模式 ,偏函数是固定参数 ,柯里化是拆分参数。它们都是利用闭包在内存中"强行留住"变量的魔法。


二、 高阶函数与柯里化

1. 什么是高阶函数? (Higher-Order Function)

定义: 满足以下任意一个条件的函数,即为高阶函数:

  • 接受一个或多个函数作为参数。

  • 返回一个函数作为输出。

javascript 复制代码
// 场景1:接受函数作为参数
function repeat(fn, times) {
  for (let i = 0; i < times; i++) fn(i);
}
repeat(console.log, 3); // 输出 0, 1, 2

// 场景2:返回一个函数
function makeGreater(n) {
  return function(num) {
    return num > n;
  };
}
const isGreater10 = makeGreater(10);
console.log(isGreater10(15)); // true

2. 函数柯里化 (Currying)

定义: 将一个接收多个参数的函数,转化为一系列接收单个参数(或部分参数)且返回新函数的技巧。

公式: f ( a , b , c ) → f ( a ) ( b ) ( c ) f(a, b, c) \rightarrow f(a)(b)(c) f(a,b,c)→f(a)(b)(c)

javascript 复制代码
function curry(fn) {
  return function curried(...args) {
    // 如果累计参数够了,执行原函数
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      // 参数不够,继续返回新函数接收参数
      return function(...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    };
  };
}

3. 为什么需要它们?(核心价值)

  • 参数复用 (Abstraction): 通过柯里化固定某些公用参数,减少重复传参。

  • 逻辑延迟执行 (Lazy Evaluation): 高阶函数可以先收集参数,在真正需要结果时才触发计算。

  • 提高可重用性与可维护性: 将复杂的逻辑拆解为微小的、功能单一的单元,符合"单一职责原则"。

  • 组合性 (Composition): 高阶函数允许我们像搭积木一样,将多个小函数组合成一个大功能。

4. 常见面试题

A. 手写题:实现 add(1)(2)(3)

这是考察闭包和高阶函数最经典的一道题。

javascript 复制代码
function add(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
// 进阶版:支持无限累加 add(1)(2)(3)...(n)
function infiniteAdd(a) {
  let sum = a;
  function forward(b) {
    sum += b;
    return forward;
  }
  forward.toString = function() { return sum; }; // 利用隐式转换
  return forward;
}

三、 立即执行函数

立即执行函数(IIFE,Immediately Invoked Function Expression)是 JavaScript 中一种特殊的函数写法。

1. 什么是立即执行函数 (IIFE)?

定义: 声明一个匿名函数,并立即执行它。

它的标准写法通常是用一对圆括号将函数包裹起来,然后再紧跟一对圆括号表示调用:

javascript 复制代码
(function() {
  console.log("我刚诞生就执行了!");
})();

// 或者这种写法(常用)
(function(name) {
  console.log("你好," + name);
})("Gemini");

核心在于 "表达式 " (Expression) 与 "声明" (Declaration) 的区别:

  • 解析器的规则 :在 JS 中,如果以 function 关键字开头,解析器会认为这是一个函数声明。函数声明必须有名字,且不能直接在后面加 () 调用。

  • 强制转换 :通过外层的 (),我们将 function 开头的语句强制转换为了一个函数表达式

  • 立即调用:表达式会返回一个函数对象,后面紧跟的 () 就可以直接触发这个对象的执行。

A. 函数声明 (Function Declaration)

函数声明就像是在档案库里注册一个名字。它会被编译器优先处理(即变量提升)。

  • 语法特征:以 function 关键字开头,且必须有函数名

  • 执行行为:它不会产生一个立即可以使用的值,而是定义了一个可以在后续调用的标识符。

  • 限制:不能直接在后面加 () 来执行

javascript 复制代码
// 这是一个声明
function sayHello() {
  console.log("Hello");
}

// 报错!声明不能直接调用
// function(){ console.log(1) }();

B. 函数表达式 (Function Expression)

函数表达式将函数看作一个值(就像数字 1 或字符串 "abc" 一样)。

  • 语法特征:通常将函数赋值给变量,或者用括号等操作符将其包裹。

  • 执行行为:它在代码执行到这一行时才被创建,并返回一个函数对象。

  • 优势:因为它是"产生一个值"的过程,所以我们可以直接在这个"值"后面加 () 来调用它。

javascript 复制代码
// 这是一个表达式(赋值过程)
var myFunc = function() { return 1; };

// 这也是一个表达式(圆括号强制转换)
(function() { return 2; });
特性 函数声明 (Declaration) 函数表达式 (Expression)
定义语法 function name() { ... } var name = function() { ... }
提升 (Hoisting) 会被提升:在代码执行前解析器就已读取,可以在定义之前调用。 不会提升:必须在执行到定义该行的代码之后,才能被使用。
解析顺序 在代码执行前的"预编译阶段"被读取并创建。 代码执行到该行时,才会被动态解析并创建。
是否立即执行 不可以 。直接加 () 会导致语法错误。 可以 。只要后面紧跟 () 即可变为立即执行函数 (IIFE)。
函数名 必须有(除了在导出的特殊场景下)。 可有可无。通常使用匿名函数。
核心本质 是一条"指令",告诉解析器注册一个函数名。 是一个"值",它会产生一个函数对象供程序使用。

2. 为什么需要它?(核心价值)

A. 避免全局污染(最重要)

在 ES6 的 let 和 const 出现之前,JavaScript 只有"全局作用域"和"函数作用域"。

如果在全局写变量,很容易被别人覆盖。IIFE 创建了一个独立的私有作用域,内部定义的变量外部无法访问。

B. 减少命名冲突

你可以放心地在 IIFE 内部定义 count、data 等常用变量名,而不用担心破坏其他代码。

3. 应用场景

A. 模拟块级作用域(ES6 之前的救星)

在 for 循环中解决 var 带来的同步异步问题(这是经典面试题):

javascript 复制代码
for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000); // 1, 2, 3, 4, 5
  })(i);
}

B. 模块化封装(插件开发)

早期的 jQuery 等库都使用这种方式,保护内部变量,只向外暴露特定的接口。

javascript 复制代码
var myModule = (function() {
  var privateName = "秘密";
  return {
    getName: function() { return privateName; }
  };
})();

4. 相关常见面试题

Q1:代码改错

javascript 复制代码
function(){
  console.log(1);
}();

问: 这段代码会报错吗?

答: 会。解析器认为这是没有名字的"函数声明",会报语法错误(SyntaxError)。必须包裹 () 变为表达式。

Q2:IIFE 的返回值

javascript 复制代码
var result = (function(a, b) {
  return a + b;
})(10, 20);
console.log(result); // 30

问: result 拿到的具体是什么?

答: 拿到的是 IIFE 执行后的返回结果,而不是函数本身。

相关推荐
一只幸运猫.2 小时前
字节跳动Java大厂面试版
java·开发语言·面试
|晴 天|2 小时前
Element Plus 组件库实战技巧与踩坑记录
前端·javascript·vue.js·typescript
xier_ran2 小时前
【C++】“内部”、“外部”、“派生类”、“友元“类
java·开发语言·c++
im_AMBER2 小时前
从面试题看JS变量提升
开发语言·前端·javascript·前端框架
故事和你912 小时前
洛谷-数据结构1-2-二叉树1
开发语言·数据结构·c++·算法·leetcode·动态规划·图论
大橘2 小时前
【qml-5.1】qml与c++交互(QML_ELEMENT/QML_SINGLETON)
开发语言·c++·qt·交互·qml
凭君语未可2 小时前
从静态代理走向动态代理:理解 JDK 动态代理的本质
java·开发语言
llf_cloud2 小时前
Vue2 项目中的全局自动弹窗队列设计
前端·javascript·vue.js
小碗羊肉3 小时前
【从零开始学Java | 第三十八篇】序列化流(Object Stream)
java·开发语言