编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

    • 综合案例

    • 专栏博客

      • README
      • 引擎解析编译执行
      • 隐藏类与回收机制
      • 类型隐式转换精算
      • 作用域链闭包原理
      • 函数绑定规则组合
        • 1. 案例与疑问引入
          • 1.1 React 类组
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构全景概览
          • 2.1 this 四规则
          • 2.2 为什么 JS 的
        • 3. 默认绑定规则
          • 3.1 严格模式 thi
          • 3.2 为什么 V8 这
        • 4. 隐式绑定丢失
          • 4.1 赋值 / 回调
          • 4.2 隐式丢失的根因
        • 5. 显式绑定详解
          • 5.1 手写 call
          • 5.2 手写 apply
          • 5.3 手写 bind
          • 5.4 bind 两次绑
        • 6. new绑定机制
          • 6.1 [[Constr
          • 6.2 构造函数返回对象
          • 6.3 手写 new 操
        • 7. 箭头词法this
          • 7.1 箭头函数不自带
          • 7.2 React 类组
          • 7.3 箭头函数为什么不
        • 8. 高阶函数全景
          • 8.1 map / fi
          • 8.2 compose
          • 8.3 用 compos
        • 9. 柯里化偏函数
          • 9.1 柯里化 vs 偏
          • 9.2 日志格式化器的柯
          • 9.3 尾调用优化(TC
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 日志管线从 10
          • 10.3 设计哲学回扣
          • 10.4 速查表
      • 原型链语法糖本质
      • 代理与元编程协议
      • 事件循环承诺机制
      • 工作线程并发调度
      • 页面渲染像素原理
      • 网络接口存储架构
      • 服务端运行时编程
      • 模块系统双轨操作
      • 现代工程链三件套
      • 设计模式函数哲学
      • 跨端架构终局总结
  • CodeX
  • JavaScript入门
  • 专栏博客
杨充
2026-06-11
目录

函数绑定规则组合

# 05.this 四规则与函数组合

📍 上接第 04 篇《作用域链与闭包深度》。闭包让函数记住了外部变量。本文回答:函数内部的 this 到底指向谁——为什么同一行代码在回调里就丢了?bind/call/apply 自己怎么写?compose 为什么比嵌套更优雅?JS 为什么选择了"调用时决定 this"而不是"定义时决定"?

# 目录介绍

  • 1. 案例与疑问引入
    • 1.1 React 类组
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构全景概览
    • 2.1 this 四规则
    • 2.2 为什么 JS 的
  • 3. 默认绑定规则
    • 3.1 严格模式 thi
    • 3.2 为什么 V8 这
  • 4. 隐式绑定丢失
    • 4.1 赋值 / 回调
    • 4.2 隐式丢失的根因
  • 5. 显式绑定详解
    • 5.1 手写 call
    • 5.2 手写 apply
    • 5.3 手写 bind
    • 5.4 bind 两次绑
  • 6. new绑定机制
    • [6.1 [Constr
    • 6.2 构造函数返回对象
    • 6.3 手写 new 操
  • 7. 箭头词法this
    • 7.1 箭头函数不自带
    • 7.2 React 类组
    • 7.3 箭头函数为什么不
  • 8. 高阶函数全景
    • 8.1 map / fi
    • 8.2 compose
    • 8.3 用 compos
  • 9. 柯里化偏函数
    • 9.1 柯里化 vs 偏
    • 9.2 日志格式化器的柯
    • 9.3 尾调用优化(TC
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 日志管线从 10
    • 10.3 设计哲学回扣
    • 10.4 速查表

# 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!*/}
      />
    );
  }
}
1
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!
  }
  // ... 其他代码不变
}
1
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 的四条绑定规则和函数组合模式,追问六个问题:

  1. ① 默认绑定:为什么严格模式下 this === undefined,非严格模式下 this === window?ES5 为什么要改?
  2. ② 隐式丢失:五种让 this"悄悄溜走"的场景——每种的根因在哪?为什么函数引用不携带对象上下文?
  3. ③ 显式绑定:call/apply/bind 的手写实现——"临时挂载"这个 trick 是怎么代替锁的?
  4. ④ new 绑定:new 的四步中,this 是在哪一步被创建、在哪一步被返回?
  5. ⑤ 箭头函数:为什么箭头函数"没有 this"反而解决了 React 类组件的 bind 问题?
  6. ⑥ 函数组合: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 章)──→ 案例彻底剖开 + 哲学四条 + 速查表
1
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 > 任何运行时绑定规则         │
└─────────────────────────────────────────────────┘
1
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(非严格)
1
2
3
4
5
6
7
8
9
10
11
12

# 2.2 为什么 JS 的

疑惑:第 04 篇告诉我们"变量查找走词法作用域——定义时决定"。为什么 this 不遵循同一个规则?为什么 inner() 里的 this 不由 inner 定义在哪决定?

论证——Brendan Eich 在 2001 年的采访中解释了这个设计:

  1. JS 诞生时的定位——"Java 的脚本小助手"。Java 的一切方法都属于 class,this 永远指向当前实例。JS 没有 class(1995 年还没有),但 Brendan Eich 想让 JS 的对象能有"类似方法"的行为——即 obj.method() 中的 method 内部能访问 obj。原型链解决了"方法共享",但还需要一个机制让"方法内部访问到调用它的那个对象"。

  2. "调用时决定 this"是唯一能在没有 class 的前提下实现"方法行为"的方案。如果 this 是词法的(定义时确定),那么:

    • Dog.prototype.bark 定义在 Dog.prototype 上——它的 this 永远指向 Dog.prototype(定义时的外层对象)
    • 但 new Dog() 创建的实例调用 bark() 时,需要 this 指向实例而不是 Dog.prototype
    • → 词法 this 不能满足这个需求
  3. "函数是一等公民"需要动态 this。JS 中函数可以被赋值、传递、脱离对象存在。如果 this 是词法的——当 const fn = obj.method 后调用 fn(),this 还是 obj 吗?如果是,那 this 从哪里来?"词法 this"完全依赖于定义位置——当函数被传递后,它就"忘记"了它曾经属于哪个对象(这正是第 4 章的类型系统告诉我们的——函数只是一个引用,不携带归属信息)。

  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 不被默认绑定——避免"无意间修改全局"
1
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();
1
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'  ← 全局被静默污染
1
2
3
4
5
6
7
8
9
10
11
12
13
14

严格模式的修正不是"限制",是"排除歧义":

非严格模式:fn() 的 this → 可能指向 window → 可能是个 bug,可能是有意为之
严格模式:   fn() 的 this → undefined → 如果函数内用了 this,立刻 TypeError
                                           → 没有歧义,就是 bug
1
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
1
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"
1
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
1
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"
1
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 对实际执行的函数没有影响
1
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;
1
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 是基本类型 → 忽略 → 返回新对象
1
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
1
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)
1
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!)
1
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} />;
}
1
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)
1
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)
1
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>"
1
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"
1
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');
1
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 * ...
}
1
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(不爆栈)
1
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 里加一行;删除步骤 = 删掉一行
// 每一步都可以单独写单元测试
1
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.原型链与类语法糖本质。

上次更新: 2026/06/16, 12:36:20
作用域链闭包原理
原型链语法糖本质

← 作用域链闭包原理 原型链语法糖本质→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式