函数绑定规则组合
# 05.this 四规则与函数组合
📍 上接第 04 篇《作用域链与闭包深度》。闭包让函数记住了外部变量。本文回答:函数内部的 this 到底指向谁——为什么同一行代码在回调里就丢了?bind/call/apply 自己怎么写?compose 为什么比嵌套更优雅?JS 为什么选择了"调用时决定 this"而不是"定义时决定"?
# 目录介绍
- 1. 案例与疑问引入
- 2. 架构全景概览
- 3. 默认绑定规则
- 4. 隐式绑定丢失
- 5. 显式绑定详解
- 6. new绑定机制
- [6.1 [Constr
- 6.2 构造函数返回对象
- 6.3 手写 new 操
- 7. 箭头词法this
- 8. 高阶函数全景
- 9. 柯里化偏函数
- 10. 综合案例串讲
# 1. 案例与疑问引入
# 1.1 React 类组
先看一段每个 React 开发者都见过的经典错误代码——一个搜索输入框组件:
// SearchBox.jsx —— React 类组件(ES6)
class SearchBox extends React.Component {
constructor(props) {
super(props);
this.state = { query: '' };
}
handleChange(e) {
// ⚡ 这里会报错!TypeError: this.setState is not a function
this.setState({ query: e.target.value });
}
render() {
return (
<input
value={this.state.query}
onChange={this.handleChange} {/* ← 注意:没有 bind!*/}
/>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
现象:
- 用户在输入框里打字 → 页面白屏 → 控制台报
TypeError: this.setState is not a function this不是SearchBox的实例——它是undefined(严格模式)
修复——加上 bind:
class SearchBox extends React.Component {
constructor(props) {
super(props);
this.state = { query: '' };
this.handleChange = this.handleChange.bind(this); // ← 必须显式 bind!
}
// ... 其他代码不变
}
2
3
4
5
6
7
8
# 1.2 顺藤摸到根因
带着这条线往下挖:
- 根因:
onChange={this.handleChange}传递的是函数引用——不是函数调用。React 在内部当事件触发时,以独立函数调用的方式执行这个回调:handler(event)。此时this不是SearchBox的实例——因为没有"点对象"在调用点左边。 - 这不是 React 的 bug——这是 JS 的 this 绑定规则:函数调用时的 this 取决于调用点的语法形态,不由函数的定义位置决定。
this.handleChange定义在SearchBox类里,但在onChange中它被作为独立函数调用——this 丢失。 - React 为什么要让开发者手动 bind——因为 React 在 2013 年发布时,箭头函数(ES6)还不存在。今天可以用箭头函数(
onChange={e => this.handleChange(e)})或 class fields(handleChange = () => { ... })自动 bind——但历史遗留的类组件写法仍然需要理解 why。
# 1.3 我们要回答什么
本文围绕 this 的四条绑定规则和函数组合模式,追问六个问题:
- ① 默认绑定:为什么严格模式下
this === undefined,非严格模式下this === window?ES5 为什么要改? - ② 隐式丢失:五种让 this"悄悄溜走"的场景——每种的根因在哪?为什么函数引用不携带对象上下文?
- ③ 显式绑定:
call/apply/bind的手写实现——"临时挂载"这个 trick 是怎么代替锁的? - ④ new 绑定:
new的四步中,this 是在哪一步被创建、在哪一步被返回? - ⑤ 箭头函数:为什么箭头函数"没有 this"反而解决了 React 类组件的 bind 问题?
- ⑥ 函数组合:compose/pipe、柯里化、偏函数——高阶函数如何让代码从"描述过程"变成"描述转换"?
最后,把一个日志管线从 10 层嵌套重构为 1 行 compose。
本篇路线:
架构总图(第 2 章)
↓
默认绑定(第 3 章)──→ 解开"非严格全局污染 vs 严格防御修正"
↓
隐式绑定与丢失(第 4 章)──→ 解开"五种 this 丢失场景的同一个根因"
↓
显式绑定(第 5 章)──→ 解开"call/apply/bind 是怎么偷梁换柱的"
↓
new 绑定(第 6 章)──→ 解开"new 的四步中 this 的生命周期"
↓
箭头函数(第 7 章)──→ 解开"词法 this 为什么免疫所有绑定规则"
↓
高阶函数(第 8 章)──→ 解开"map/filter/reduce → compose/pipe 的声明式范式"
↓
柯里化/TCO(第 9 章)──→ 解开"参数控制 + 尾递归优化的现实边界"
↓
综合案例(第 10 章)──→ 案例彻底剖开 + 哲学四条 + 速查表
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
📌 本篇定位:上接闭包和作用域链(第 04 篇),下接原型链(第 06 篇)。闭包解决了"函数能看到什么变量",本篇回答:函数被调用时,this 指向谁——以及 JS 为什么选择了"调用时动态绑定"而不是"定义时静态绑定"。读完本篇后,你的每一行
this.xxx都能精确回答"这个 this 是什么、为什么是这个值"。
# 2. 架构全景概览
# 2.1 this 四规则
JS 的 this 绑定遵循四条规则——按优先级从低到高排列(高层可以覆盖低层):
┌─────────────────────────────────────────────────┐
│ this 四规则优先级链(ECMA-262 §13.3) │
├─────────────────────────────────────────────────┤
│ │
│ 优先级最低(可以被更高层覆盖) │
│ ┌─────────────────────────────────┐ │
│ │ ① 默认绑定 │ ← fn() 或 (()=>{}).call() │
│ │ this = undefined(严格) │ 严格或非严格决定 │
│ │ this = window/global(非严格) │ │
│ └─────────────────────────────────┘ │
│ ↑ 被覆盖 │
│ ┌─────────────────────────────────┐ │
│ │ ② 隐式绑定 │ ← obj.fn() │
│ │ this = obj │ 调用点前面的"那个对象" │
│ └─────────────────────────────────┘ │
│ ↑ 被覆盖 │
│ ┌─────────────────────────────────┐ │
│ │ ③ 显式绑定 │ ← fn.call(ctx) / bind(ctx) │
│ │ this = 指定的 ctx │ │
│ └─────────────────────────────────┘ │
│ ↑ 被覆盖 │
│ ┌─────────────────────────────────┐ │
│ │ ④ new 绑定 │ ← new Fn() │
│ │ this = 新创建的实例对象 │ │
│ └─────────────────────────────────┘ │
│ 优先级最高 │
│ │
│ ⑤ 特殊规则:箭头函数 │
│ → 不自带 this——从外层词法作用域继承(不受以上四规则影响)│
│ → call/apply/bind 对它无效 │
│ → 优先级:词法 this > 任何运行时绑定规则 │
└─────────────────────────────────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ECMA-262 规范视角——CallExpression 的三种求值方式:
规范 §13.3 规定:函数调用的 this 取决于 MemberExpression 的求值结果
① MemberExpression 解析为 Reference 类型(有 base 对象):
"obj.fn()" → Reference { base: obj, referencedName: 'fn', strict: false }
→ this = obj
② MemberExpression 解析为 Reference 类型(base 是 Environment Record):
"fn()" → Reference { base: envRec, referencedName: 'fn', strict: true }
→ this = undefined(严格模式)
③ MemberExpression 不是 Reference(如 (fn)()):
→ this = undefined(严格)/ globalThis(非严格)
2
3
4
5
6
7
8
9
10
11
12
# 2.2 为什么 JS 的
疑惑:第 04 篇告诉我们"变量查找走词法作用域——定义时决定"。为什么 this 不遵循同一个规则?为什么 inner() 里的 this 不由 inner 定义在哪决定?
论证——Brendan Eich 在 2001 年的采访中解释了这个设计:
JS 诞生时的定位——"Java 的脚本小助手"。Java 的一切方法都属于 class,
this永远指向当前实例。JS 没有 class(1995 年还没有),但 Brendan Eich 想让 JS 的对象能有"类似方法"的行为——即obj.method()中的method内部能访问obj。原型链解决了"方法共享",但还需要一个机制让"方法内部访问到调用它的那个对象"。"调用时决定 this"是唯一能在没有 class 的前提下实现"方法行为"的方案。如果 this 是词法的(定义时确定),那么:
Dog.prototype.bark定义在Dog.prototype上——它的 this 永远指向Dog.prototype(定义时的外层对象)- 但
new Dog()创建的实例调用bark()时,需要 this 指向实例而不是Dog.prototype - → 词法 this 不能满足这个需求
"函数是一等公民"需要动态 this。JS 中函数可以被赋值、传递、脱离对象存在。如果 this 是词法的——当
const fn = obj.method后调用fn(),this 还是obj吗?如果是,那 this 从哪里来?"词法 this"完全依赖于定义位置——当函数被传递后,它就"忘记"了它曾经属于哪个对象(这正是第 4 章的类型系统告诉我们的——函数只是一个引用,不携带归属信息)。JavaScript 不是 Java。很多从 Java 转来 JS 的开发者的第一反应是"this 应该像 Java 一样"。但 Brendan Eich 明确表示:JS 的 this 是特意设计成动态的——因为 JS 没有 class,没法静态决定 this。ES6 加了 class 语法糖之后,在 class 方法的上下文中 this 的行为确实更接近 Java——但这只是"糖"的效果,底层仍然是原型链 + 动态 this。
结论:JS 动态 this 不是"设计失误"——它是原始 JS 对象模型(prototype-based OOP)和"函数是一等公民"这两个设计在 1995 年的自然交汇。在 class 诞生之前,动态 this 是唯一能让 obj.method() 产生"方法行为"的机制。箭头函数在 ES6 补充了"词法 this"——但它只是提供了另一种选项,不是替换了底层机制。
# 3. 默认绑定规则
# 3.1 严格模式 thi
默认绑定是"没有其他规则生效时"的兜底:
// 非严格模式
function showThis() {
console.log(this);
}
showThis(); // → Window(浏览器)/ global(Node)
// this 被默认绑定到全局对象——JS 早期设计的"宽容"策略
// 严格模式
'use strict';
function showThisStrict() {
console.log(this);
}
showThisStrict(); // → undefined
// this 不被默认绑定——避免"无意间修改全局"
2
3
4
5
6
7
8
9
10
11
12
13
14
严格模式对嵌套函数也同样生效:
const obj = {
name: 'obj',
method() {
// 这里的 this 是 obj(隐式绑定)
function inner() {
console.log(this); // ← 默认绑定,不是 obj!
}
inner(); // → undefined(严格)/ window(非严格)
}
};
obj.method();
2
3
4
5
6
7
8
9
10
11
# 3.2 为什么 V8 这
疑惑:ES5 为什么要引入严格模式把默认绑定改成 undefined?表层原因是"防全局污染",但深层原因是什么?
论证——非严格模式的全局绑定埋下了两个致命安全隐患:
// 危险 1:无意的全局写入
function setName(name) {
this.name = name; // ← 非严格:this = window → 往 window 上挂属性
}
setName('global');
console.log(window.name); // → 'global' ← 全局被污染了!
// 危险 2:构造函数忘记 new——灾难性静默错误
function User(name) {
this.name = name; // ← 忘记 new → this = window → 全局属性!
}
const u = User('admin'); // ← 忘了 new!不报错!
console.log(u); // → undefined
console.log(window.name); // → 'admin' ← 全局被静默污染
2
3
4
5
6
7
8
9
10
11
12
13
14
严格模式的修正不是"限制",是"排除歧义":
非严格模式:fn() 的 this → 可能指向 window → 可能是个 bug,可能是有意为之
严格模式: fn() 的 this → undefined → 如果函数内用了 this,立刻 TypeError
→ 没有歧义,就是 bug
2
3
结论:默认绑定到全局不是"方便"——它是 JS 从 Java 借来的一个安全缺陷。它让一个无关的函数调用意外修改了全局状态——在最坏的情况下(构造函数忘记 new),这个修改是完全静默的。严格模式把"无意的全局修改"转成"显式的 TypeError 抛掷"——这是 fail-fast 哲学在 JS 异步安全中的早期实践。
# 4. 隐式绑定丢失
# 4.1 赋值 / 回调
| 场景 | 代码 | 为什么 this 丢了 |
|---|---|---|
| 赋值 | const fn = obj.method; fn() | 函数引用不携带对象上下文——obj.method 只返回函数对象本身 |
| 回调 | setTimeout(obj.method, 0) | setTimeout 内部执行 callback()——独立调用 |
| 参数传递 | callFn(obj.method) | 形参 fn = obj.method——和赋值丢失同一个机制 |
| 高阶函数 | [1,2,3].map(obj.method) | map 内部回调是独立调用 callback(item, index) |
| 表达式赋值 | (obj.method)() | 括号只改变求值顺序——不保存 this 绑定 |
// 赋值丢失
const obj = { name: 'obj', sayName() { console.log(this.name); } };
const fn = obj.sayName; // fn 是"裸函数引用"——没有携带 obj 的上下文
fn(); // → undefined
// 高阶函数——同样的问题
const arr = [1, 2, 3];
const lengths = arr.map(obj.sayName); // this = undefined → 每项都是 undefined
2
3
4
5
6
7
8
# 4.2 隐式丢失的根因
疑惑:为什么 obj.method 不返回"带着 obj 的上下文信息"的东西?JS 引擎能不能让方法引用"记住"它属于谁?
论证——这恰好是 JS 函数模型的根基:
在 JS 中,一个"方法"只是"碰巧存储在对象属性上的一个函数"。函数本身完全不知道它被存储在哪个对象上——sayName 只是一个 JSFunction 堆对象,它内部没有任何字段记录"我被哪个对象拥有"。当执行 obj.sayName 时,引擎只是做了属性查找(第 04 篇的 RHS)——返回的是函数对象本身,不包裹任何"来源信息"。
如果 JS 让方法引用"携带归属信息",那 const fn = obj.method 返回的就不只是函数——还需要返回一个包含 {fn, owner} 的包装对象。这会打破 JS "函数是一等公民"的承诺——函数不再是"可以被自由传递的独立值"。
结论:隐式丢失不是 bug——它是"函数一等公民 + 动态 this"这两个设计决策的必然结果。this 绑定于调用点,不绑定于引用点。这是 JS 的函数必须接受的"宪法"——一旦你理解了它,所有关于 this 的行为都变得可预测。
# 5. 显式绑定详解
# 5.1 手写 call
疑惑:fn.call(ctx) 怎么把 this 从默认/隐式值"强行改成" ctx?
论证——核心思想极其简单:把函数临时挂到 ctx 上,作为方法调用,然后摘掉:
Function.prototype.myCall = function(context, ...args) {
// 1) null/undefined 兜底——模拟非严格模式的全局对象行为
if (context == null) {
context = globalThis;
}
// 2) 生成唯一键——避免覆盖 ctx 上已有的属性
const uniqueKey = Symbol('myCall');
// 3) 把当前函数(this 就是 fn)临时挂在 context 上
context[uniqueKey] = this; // ← 关键:fn 变成 ctx.fn
// 4) 作为方法调用——隐式绑定生效——this = context
const result = context[uniqueKey](...args);
// 5) 清理——不留下痕迹
delete context[uniqueKey];
return result;
};
const person = { name: 'Alice' };
function greet(greeting) { return `${greeting}, ${this.name}`; }
console.log(greet.myCall(person, 'Hello')); // → "Hello, Alice"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
本质上:call 没有修改引擎的任何底层机制——它只是临时把"独立调用"包装成"方法调用",让隐式绑定规则替它完成 this 切换。这个"临时挂载 + Symbol 唯一键 + delete 清理"的三步 trick 就是 call 的全部精髓。
V8 原生实现 vs 手写实现的差异:
V8 的原生 call 不真的走"属性挂载→调用→删除"路径——它直接调用 C++ 层的 JSFunction::Call() 函数,把 context 作为接收者(receiver)传入,绕过了属性查找的整个路径。手写版本只是在 JS 层模拟行为——实际性能差一个数量级。
# 5.2 手写 apply
Function.prototype.myApply = function(context, argsArray) {
if (context == null) context = globalThis;
const uniqueKey = Symbol('myApply');
context[uniqueKey] = this;
const result = context[uniqueKey](...(argsArray || [])); // ← 唯一的区别:展开
delete context[uniqueKey];
return result;
};
const nums = [5, 6, 2, 3, 7];
console.log(Math.max.myApply(null, nums)); // → 7
2
3
4
5
6
7
8
9
10
11
# 5.3 手写 bind
bind 与 call/apply 的关键区别:它不是"立即执行"——它返回一个永久绑定了 this 的新函数:
Function.prototype.myBind = function(context, ...boundArgs) {
const originalFn = this; // ← 保存原始函数
const boundFn = function(...callArgs) {
// 合并 bind 时预设的参数 + 调用时传入的参数
const allArgs = [...boundArgs, ...callArgs];
// ⚠️ 判断:是通过 new 调用吗?
// new boundFn() → this(构造函数内的 this)是 boundFn 的新实例
// → 此时应该用 this(新实例),而不是 context
// 如果是普通调用 boundFn() → this === window/undefined
// → 此时应该用 context(绑定的 this)
return originalFn.apply(
this instanceof boundFn ? this : context,
allArgs
);
};
// 原型继承——new boundFn() 需要能访问 originalFn.prototype 上的方法
boundFn.prototype = Object.create(originalFn.prototype);
return boundFn;
};
// 验证
const person = { name: 'Alice' };
function introduce(title) { return `${title}: ${this.name}`; }
const boundIntroduce = introduce.myBind(person, 'Dr.');
console.log(boundIntroduce()); // → "Dr.: Alice"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
为什么 bind 需要处理 new 的情况——ECMA-262 规定:如果 bind 返回的绑定函数被 new 调用,this 应该是新创建的实例,而不是绑定的 context。这保证了 bind 和 new 的优先级顺序——new 绑定 > 显式绑定。
# 5.4 bind 两次绑
const fn = function() { console.log(this.name); };
const a = { name: 'a' }, b = { name: 'b' };
const bound1 = fn.bind(a); // 第一次:this 被硬编码为 a
const bound2 = bound1.bind(b); // 第二次:绑定 bound1 的 this?
bound2(); // → 'a' ← 不是 'b'!
// 原因:bound1 内部已经是 originalFn.apply(context, ...)
// context = a(第一次 bind 时固化)。bound2 绑的是 bound1 这个包装函数——
// 但 bound1 的 this 在它内部并不被使用(它内部用的是闭包保存的 context = a)
// → 第二次 bind 对实际执行的函数没有影响
2
3
4
5
6
7
8
9
10
11
12
# 6. new绑定机制
# 6.1 [[Constr
new Fn(...args) 的底层执行路径(ECMA-262 §13.2.2):
步骤 1: 创建全新的空对象
const obj = {};
步骤 2: 设置原型链
Object.setPrototypeOf(obj, Fn.prototype);
// 等价于:obj.__proto__ = Fn.prototype
步骤 3: 执行构造函数,this 绑定为新对象
const result = Fn.call(obj, ...args);
// Fn 内部所有的 this.xxx = ... 操作都在 obj 上进行
步骤 4: 返回结果
if (result 是对象) → 返回 result;
else → 返回 obj;
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.2 构造函数返回对象
function A() { this.name = 'A'; return this; } // 默认行为——this 就是新对象
function B() { this.name = 'B'; return { x: 1 }; } // 返回了另一个对象!
function C() { this.name = 'C'; return 42; } // 返回基本类型 → 被忽略
console.log(new A()); // → A { name: 'A' } ← 正常
console.log(new B()); // → { x: 1 } ← B 内部构造的 name 被丢弃!
console.log(new C()); // → C { name: 'C' } ← 42 是基本类型 → 忽略 → 返回新对象
2
3
4
5
6
7
# 6.3 手写 new 操
function myNew(Constructor, ...args) {
// 步骤 1+2:创建对象 + 设置原型
const obj = Object.create(Constructor.prototype);
// 步骤 3:执行构造函数,this = obj
const result = Constructor.apply(obj, args);
// 步骤 4:判断返回——对象走 result,否则走 obj
return result instanceof Object ? result : obj;
}
function User(name) { this.name = name; }
const user = myNew(User, 'Alice');
console.log(user.name); // → 'Alice'
console.log(user instanceof User); // → true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7. 箭头词法this
# 7.1 箭头函数不自带
疑惑:箭头函数的 this 为什么不受 call/apply/bind 影响?V8 在创建箭头函数时跳过了什么?
论证——箭头函数缺了一个 ECMA-262 规范规定的内部槽:
普通函数(ECMA-262 §10.2):
[[ThisMode]] = lexical(全局)/ strict(严格)/ global(非严格)
每次调用时,根据调用模式计算 this 的值
箭头函数(ECMA-262 §15.3):
[[ThisMode]] 不存在!——箭头函数没有这个内部槽。
→ 函数体内任何 this 的访问都会被直接转发到外层词法环境
→ 相当于:this = 定义时所在作用域的 this(闭包原理——第 04 篇 §7)
2
3
4
5
6
7
8
const obj = {
name: 'obj',
regularFn() { console.log(this.name); }, // this = 调用时决定
arrowFn: () => { console.log(this.name); } // this = 定义时决定(外层)
};
obj.regularFn(); // → 'obj'(隐式绑定 → this = obj)
obj.arrowFn(); // → undefined(箭头函数外层是全局作用域 → this = window/undefined)
// call 无法改变箭头函数的 this——它没有 [[ThisMode]] 槽供 call 修改
obj.arrowFn.call({ name: 'hacked' }); // → undefined(仍然用外层 this!)
2
3
4
5
6
7
8
9
10
11
# 7.2 React 类组
// React 类组件——this 丢失(需要手动 bind)
class Button extends React.Component {
constructor() {
super();
this.state = { clicked: false };
this.handleClick = this.handleClick.bind(this); // ← 必须手动 bind!
}
handleClick() {
this.setState({ clicked: true }); // 如果没有 bind,this = undefined → 报错
}
render() { return <button onClick={this.handleClick} />; }
}
// React 函数组件——箭头函数天然免疫
function ButtonFn() {
const [clicked, setClicked] = useState(false);
// ✅ 箭头函数从外层词法作用域继承 this
// 但函数组件外层没有有意义的 this——Hooks 不依赖 this
// 闭包捕获了 setClicked(第 04 篇的机制),不需要 this
const handleClick = () => setClicked(true);
return <button onClick={handleClick} />;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 7.3 箭头函数为什么不
const Arrow = () => {};
console.log(Arrow.prototype); // → undefined
// new Arrow(); // → TypeError: Arrow is not a constructor
// 原因:箭头函数缺少三个关键能力:
// ① 没有 [[Construct]] 内部方法(不能被 new)
// ② 没有 prototype 属性(不能为实例提供原型)
// ③ 没有 [[ThisMode]](不能为 new 创建的实例分配 this)
2
3
4
5
6
7
8
# 8. 高阶函数全景
# 8.1 map / fi
// ❌ 命令式:手动循环 —— "how"(怎么做)
const prices = [10, 20, 30, 40, 50];
let total = 0;
for (let i = 0; i < prices.length; i++) {
if (prices[i] > 20) {
total += prices[i] * 1.2;
}
}
// ✅ 声明式:数据转换管道 —— "what"(做什么)
const total2 = prices
.filter(p => p > 20) // [30, 40, 50]
.map(p => p * 1.2) // [36, 48, 60]
.reduce((sum, p) => sum + p, 0); // 144
// ⚠️ 注意:回调中 this 规则同样生效——普通函数回调会丢失 this
const obj = { tax: 0.2 };
const wrong = prices.map(function(p) {
return p * this.tax; // ← this = undefined → NaN!
});
// 修复:箭头函数 p => p * obj.tax 或 .bind(obj)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 8.2 compose
// compose:右→左 = 数学函数组合 (f ∘ g)(x) = f(g(x))
const compose = (...fns) => x => fns.reduceRight((v, fn) => fn(v), x);
// pipe:左→右 = 数据的管道流动方向
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
// 示例
const removeSpaces = str => str.replace(/\s+/g, '');
const toLower = str => str.toLowerCase();
const wrapDiv = str => `<div>${str}</div>`;
// compose 读法:从右到左——最后写的最先执行
const process1 = compose(wrapDiv, toLower, removeSpaces);
process1(' Hello World '); // → "<div>helloworld</div>"
// pipe 读法:从左到右——数据的流动方向
const process2 = pipe(removeSpaces, toLower, wrapDiv);
process2(' Hello World '); // → "<div>helloworld</div>"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 维度 | compose | pipe |
|---|---|---|
| 执行方向 | 右→左 f(g(h(x))) | 左→右 h(x)→g→f |
| 直觉匹配 | 数学函数组合 (f∘g)(x) | 数据管道的流动方向 |
| 读代码顺序 | 从下往上 / 从右往左 | 从上往下 / 从左往右 |
| 常用场景 | Redux middleware / FP 社区 | Node.js stream pipeline / 数据处理链 |
# 8.3 用 compos
(完整案例见 §10.2)
# 9. 柯里化偏函数
# 9.1 柯里化 vs 偏
| 柯里化(Currying) | 偏函数(Partial Application) | |
|---|---|---|
| 定义 | 把 f(a,b,c) 转成 f(a)(b)(c)——每次只传一个参数 | 固定函数的前几个参数,生成新函数 |
| 参数传递 | 每次一个,逐层返回新函数 | 一次固定任意个参数 |
| 结果 | 一串单参数函数链 | 一个"欠参数"的可调用函数 |
| 典型工具 | lodash _.curry | fn.bind(null, a, b) |
// 柯里化:逐次单参
const add = a => b => c => a + b + c;
add(1)(2)(3); // → 6
// 偏函数:一次固定
const greet = (greeting, name) => `${greeting}, ${name}`;
const sayHello = greet.bind(null, 'Hello'); // 偏函数——固定第一个参数
sayHello('World'); // → "Hello, World"
2
3
4
5
6
7
8
# 9.2 日志格式化器的柯
const createLogger = level => module => message => {
const ts = new Date().toISOString();
console.log(`[${ts}][${level}][${module}] ${message}`);
};
// 逐层配置:先定级别 → 再定模块 → 最后传入消息
const errorLog = createLogger('ERROR');
const infoLog = createLogger('INFO');
const authError = errorLog('auth'); // 错误级别 + auth 模块
const dbInfo = infoLog('database'); // 信息级别 + database 模块
authError('login failed'); // → [2026-06-12T...][ERROR][auth] login failed
dbInfo('connected');
2
3
4
5
6
7
8
9
10
11
12
13
14
# 9.3 尾调用优化(TC
疑惑:ES6 规范定义了尾调用优化(ECMA-262 §15.2)——为什么 V8 在 2016 年短暂支持后又移除了?
论证——TCO 面临一个根本矛盾:性能优化 vs 调试体验。TCO 会丢弃调用栈帧来回收内存——这意味着 DevTools 的 Call Stack 面板中看不到那些被优化掉的函数帧。开发者停在一个断点上,看不到"这个函数是从哪里被调用的"。
// ✅ 尾调用:函数的最后一步是 return 另一个函数调用(没有额外运算)
function factorial(n, acc = 1) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc); // ← 尾调用:return fn(...)
}
// ❌ 不是尾调用——return 后面还有乘法
function factorialBad(n) {
if (n <= 1) return 1;
return n * factorialBad(n - 1); // ← return 后面还有 n * ...
}
2
3
4
5
6
7
8
9
10
11
trampoline——递归的替代方案(不依赖 TCO——任何引擎都支持):
// trampoline:把递归变成"循环 + 函数返回"——栈深度恒定
function trampoline(fn) {
return function(...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result(); // ← 在同一个栈帧中循环调用——不增加栈深度
}
return result;
};
}
function sumTrampoline(n, acc = 0) {
if (n === 0) return acc;
return () => sumTrampoline(n - 1, acc + n); // ← 返回函数,不是调用!
}
const safeSum = trampoline(sumTrampoline);
console.log(safeSum(20000)); // → 200010000(不爆栈)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 10. 综合案例串讲
# 10.1 案例真相揭晓
回到第 1 章的 React SearchBox this 丢失——六个疑问现在能逐条作答:
| 疑问 | 答案 |
|---|---|
| ① 严格/非严格的默认绑定差异 | 第 3 章:非严格 this=window(兼容早期 JS)→ 严格 this=undefined(ES5 fail-fast 修正——把无意的全局污染变成显式的 TypeError) |
| ② 隐式丢失五种场景的根因 | 第 4 章:函数引用不携带对象上下文——obj.method 返回的是裸函数对象。赋值/回调/参数传递/高阶函数/表达式赋值都是"把函数引用剥离了对象"→调用点变成独立调用 |
| ③ call/apply/bind 的手写原理 | 第 5 章:call = "临时把 fn 挂到 ctx 上→作为方法调用→删除临时属性"。bind = 返回一个闭包——内部用 apply 强制执行原始函数,this 被"硬化"为 context |
| ④ new 的四步流程 | 第 6 章:① 创建空对象 → ② 设原型([[Prototype]] = Fn.prototype) → ③ 执行构造器(Fn.call(obj, args)) → ④ 返回(对象 or obj) |
| ⑤ 箭头函数为什么不需要 bind | 第 7 章:箭头函数没有 [[ThisMode]] 内部槽——this 从外层词法作用域继承(第 04 篇闭包机制)。call/apply/bind 对它无效 |
| ⑥ compose/pipe 的哲学差异 | 第 8 章:compose 右→左(数学函数组合 f(g(x))),pipe 左→右(数据流动方向)。两者本质等价——选"读起来顺"的那个 |
修复方案(按触发频繁程度排序):
方案 A:构造函数中 .bind(this)(React 类组件标配)
方案 B:class fields 箭头函数 handleClick = () => { ... } 方案 C:函数组件 + Hooks(彻底消除 this 依赖)
# 10.2 日志管线从 10
// === 重构前:10 层嵌套——每一步都对上下夹杂非核心逻辑 ===
function processLog(rawLog) {
const cleaned = rawLog.trim();
const parsed = JSON.parse(cleaned);
if (!parsed.timestamp) parsed.timestamp = Date.now();
if (parsed.level === 'DEBUG' && isProduction()) return null;
parsed.message = parsed.message.replace(/\s+/g, ' ');
if (parsed.message.length > 1000) {
parsed.message = parsed.message.slice(0, 997) + '...';
}
return parsed;
}
// === 重构后:pipe 扁平化——每一步独立、可测试、可替换 ===
const trim = str => str.trim();
const parseJSON = str => JSON.parse(str);
const ensureTS = obj => ({ ...obj, timestamp: obj.timestamp || Date.now() });
const filterDebug = obj => (obj.level === 'DEBUG' && isProduction()) ? null : obj;
const normalize = obj => ({ ...obj, message: obj.message.replace(/\s+/g, ' ') });
const truncate = (max = 1000) => obj =>
obj.message.length > max ? { ...obj, message: obj.message.slice(0, max-3)+'...' } : obj;
const skipNull = fn => x => x === null ? null : fn(x);
const processLog = pipe(
trim, parseJSON, ensureTS, filterDebug,
skipNull(pipe(normalize, truncate(1000)))
);
// 新增步骤 = 往 pipe 里加一行;删除步骤 = 删掉一行
// 每一步都可以单独写单元测试
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 10.3 设计哲学回扣
哲学一·「this 不属于函数——属于调用点」——JS 最具争议的设计取舍,也是"函数一等公民"的必然代价
在 Java/C++ 中,this 是对象本身——方法内部访问 this 永远指向声明它的类的实例。JS 走了另一条路:this 是一个运行时参数,由调用者决定。这不是"设计失误"——这是 JS 在 1995 年没有 class 的情况下,让对象拥有"方法行为"的唯一方案。代价是每个开发者都必须学会四条绑定规则——这是"函数灵活性"换来的"认知复杂度"。箭头函数在 ES6 提供了"词法 this"的替代——但这不是修复,而是为"不需要动态 this 的场景"提供了另一个选项。
哲学二·「组合优于继承,但组合需要基础设施——compose 和 pipe 是函数式的基石」
compose 和 pipe 看起来只是"把函数串起来"的语法糖——但实际上它们是函数式编程的基石操作。当你把每一步都写成纯函数(相同输入→相同输出、无副作用),一个 pipe 管线就可以被逐段测试、逐段替换、逐段复用。这是面向对象继承难以做到的粒度和组合性——继承是一个"打包的套餐",compose 是"任选食材组合"。
哲学三·「箭头函数不是语法糖——是语义跳跃。它取消了 this、取消了 [[Construct]]、取消了 prototype——这些'取消'是刻意的减法」
() => expr 不只是 function() { return expr; } 的简写。它取消了自带的 this、取消了 [[Construct]]、取消了 arguments 对象、取消了 prototype。这些"取消"不是缺失——它是一种故意减少:减少概念、减少陷阱、减少"错误的方式"。React 从类组件到函数组件的迁移,底层可以用一句话总结:从"this 绑定规则"切换到"箭头函数的词法 this + 闭包捕获状态"。
哲学四·「call 的本质不是'修改 this'——是临时利用隐式绑定规则。这个 trick 揭示了 JS 所有绑定规则都是同一套机制的不同入口」
fn.call(ctx) 没有修改引擎的任何底层 this 绑定机制——它只是临时把 fn 挂到 ctx 上、作为方法调用、然后摘掉。它让隐式绑定替它完成了 this 切换。 这告诉我们:call/apply/bind 不是"新的 this 机制"——它们只是利用同一个隐式绑定规则,换了一个不同的"点对象"。理解这一点后,四条规则不再是"四套独立的机制"——它们是一套机制("调用点左边有什么对象")在四种情境下的不同表现。
# 10.4 速查表
this 四规则优先级速查:
| 优先级 | 规则 | 判断条件 | this 值 | 独占覆盖权 |
|---|---|---|---|---|
| 最高 | new | new Fn() | 新创建的实例 | ✅ 覆盖显式和隐式 |
| ↓ | 显式 | fn.call(x) / fn.bind(x)() | 指定的 x | ✅ 覆盖隐式 |
| ↓ | 隐式 | obj.fn() | obj | ❌ |
| 最低 | 默认 | fn() | 严格→undefined / 非严格→globalThis | ❌ |
| 特殊 | 箭头 | () => this | 外层词法 this | ✅ 免疫以上所有规则 |
五种隐式丢失场景速查:
| 场景 | 代码 | 修复 1 | 修复 2 |
|---|---|---|---|
| 赋值 | const fn = obj.method; fn() | fn.bind(obj) | 箭头函数 () => obj.method() |
| 回调 | setTimeout(obj.method, 0) | .bind(obj) | () => obj.method() |
| 参数传递 | callFn(obj.method) | 同上 | 箭头传递 |
| 高阶函数 | arr.map(obj.method) | arr.map(obj.method.bind(obj)) | arr.map(x => obj.method(x)) |
| 表达式 | (obj.method)() | 不写了——换以上任意写法 | — |
compose vs pipe vs 柯里化 vs 偏函数:
| compose | pipe | 柯里化 | 偏函数 | |
|---|---|---|---|---|
| 方向 | 右→左 | 左→右 | 每次一个参数 | 一次多个参数 |
| 返回值 | 一个复合函数 | 一个复合函数 | 一串单参函数 | 一个欠参函数 |
| 直觉 | 数学 f(g(x)) | 数据管道 | 渐进配置 | 参数固化 |
| 典型 | Redux middleware | Node stream | add(1)(2)(3) | fn.bind(null, a) |
下一步:函数弄清楚了,回到 JS 的另一个核心——对象和原型链。{} 为什么能调用 .toString()?new 底层干了哪四件事?进入 06.原型链与类语法糖本质。