文章目录
- [一、 闭包](#一、 闭包)
-
- [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. 相关常见面试题)
-
- Q1:代码改错
- [Q2:IIFE 的返回值](#Q2:IIFE 的返回值)
一、 闭包
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 执行后的返回结果,而不是函数本身。