普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月5日首页

从原理到手写:彻底吃透 call / apply / bind 与 arguments 的底层逻辑

作者 swipe
2026年3月5日 16:02

引言:为什么我们要“手写”这些 API?

在日常开发中,callapplybind 几乎每天都会用到。无论是处理 this 绑定问题、实现函数复用,还是做函数柯里化,它们都是绕不开的基础能力。

但有一个现实问题:

很多人“会用”,但说不清楚为什么这样设计,也不知道边界在哪里。

一旦进入复杂业务场景,比如高阶函数封装、事件回调丢失上下文、React 中函数绑定优化等问题,底层理解不扎实就会成为瓶颈。

本文我们做三件事:

  • 手写实现 call / apply / bind
  • 理解它们的设计哲学与差异
  • 彻底讲清楚 arguments 的本质与演进

目标不是背代码,而是形成“可迁移的工程认知”。


一、手写 call / apply / bind

1.0 先明确三个 API 的语法

func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [argsArray])
const newFunc = func.bind(thisArg, arg1, arg2, ...)

区别非常明确:

方法 是否立即执行 参数形式 是否返回函数
call 参数列表
apply 数组
bind 参数列表

核心差异点只有两个:

  1. 是否立即执行
  2. 参数如何传递

1.1 手写 call —— 从“执行函数”开始

第一步:给所有函数添加能力

Function.prototype.hycall = function () {
  console.log("原型链调用了")
}

function foo() {
  console.log("foo函数调用了")
}

foo.hycall()

问题出现了:

只执行了 hycall,没有执行 foo 本身。

我们真正的目标是:

  • 谁调用 hycall
  • 就执行谁

关键点:

var fn = this
fn()

优化版:

Function.prototype.hycall = function () {
  var fn = this
  fn()
}

小结

  • Function.prototype 挂方法 = 所有函数都能用
  • this 指向调用 hycall 的函数
  • call 的第一能力:立即执行函数

1.2 改变 this 指向 —— 显式绑定的核心

默认调用:

fn()  // 默认绑定 → window

我们希望:

foo.hycall({ name: "小吴" })

this 指向传入对象。

关键思路:

借助“隐式绑定规则”

thisArg.fn = fn
thisArg.fn()

实现:

Function.prototype.hycall = function (thisArg) {
  var fn = this

  thisArg.fn = fn
  thisArg.fn()

  delete thisArg.fn
}

对比原生:

foo.hycall({ name: "小吴" })
foo.call({ name: "why" })

图10-1 call调用会执行函数

为什么这样能生效?

因为:

  • obj.fn() 是隐式调用
  • 隐式调用优先级 > 默认绑定
  • 所以 this 指向 obj

这就是“借鸡生蛋”的核心思想。


1.3 处理基本类型问题

问题:

foo.hycall(123)

报错,因为:

123.fn = fn // 不允许

解决方案:

thisArg = Object(thisArg)

最终优化:

Function.prototype.hycall = function (thisArg) {
  var fn = this

  thisArg =
    thisArg !== null && thisArg !== undefined
      ? Object(thisArg)
      : window

  thisArg.fn = fn
  var result = thisArg.fn()
  delete thisArg.fn

  return result
}

图10-4 转化为对象的处理方式结果

小结

  • 基本类型会被装箱
  • null / undefined 特殊处理
  • JS 实现无法做到“完全无痕绑定”

1.4 让 call 支持传参 —— ES6 剩余参数

核心能力:

foo.call(obj, a, b, c)

实现:

Function.prototype.hycall = function (thisArg, ...args) {
  var fn = this

  thisArg =
    thisArg !== null && thisArg !== undefined
      ? Object(thisArg)
      : window

  thisArg.fn = fn
  var result = thisArg.fn(...args)
  delete thisArg.fn

  return result
}

示例:

function foo(num1, num2, num3) {
  console.log(this, num1 + num2 + num3)
}

foo.hycall("小吴", 500, 20, 1)

为什么 call 必须支持参数?

因为:

  • 每次函数调用都会创建新的执行上下文
  • 改变 this 必须在“那次调用”里完成

错误方式:

foo.call("why")
foo(500,20,1) // this 失效

这是典型“刻舟求剑”。


1.5 手写 apply

区别只在参数形式。

Function.prototype.myapply = function (thisArg, argArray) {
  var fn = this

  thisArg =
    thisArg !== null && thisArg !== undefined
      ? Object(thisArg)
      : window

  thisArg.fn = fn

  argArray = argArray || []
  var result = thisArg.fn(...argArray)

  delete thisArg.fn

  return result
}

关键差异:

  • call:参数列表
  • apply:数组

小结

  • apply 更适合参数本来就是数组的场景
  • 本质逻辑与 call 一致
  • 区别只是“参数结构”

1.6 手写 bind —— 真正的升级版

bind 解决什么问题?

延迟执行 + 参数预设

示例:

function foo(num1, num2, num3, num4) {
  console.log(this, num1, num2, num3, num4)
}

三种用法:

var bar = foo.bind("小吴", 10, 20, 30, 40)
bar()

var bar = foo.bind("小吴")
bar(10, 20, 30, 40)

var bar = foo.bind("小吴", 10, 20)
bar(30, 40)

实现思路

  • 第一次调用 bind:固定 this + 默认参数
  • 返回新函数
  • 第二次执行:合并参数再执行

实现:

Function.prototype.mybind = function (thisArg, ...argArray) {
  var fn = this

  thisArg =
    thisArg !== null && thisArg !== undefined
      ? Object(thisArg)
      : window

  function proxyFn(...args) {
    thisArg.fn = fn

    var finalArgs = [...argArray, ...args]
    var result = thisArg.fn(...finalArgs)

    delete thisArg.fn
    return result
  }

  return proxyFn
}

工程理解

  • call:一次性执行
  • bind:函数工厂
  • bind 本质是“柯里化雏形”

二、认识 arguments

2.1 arguments 是什么?

定义:

类数组对象

特征:

  • 有 length
  • 可索引访问
  • 没有数组原型方法

示例:

function foo() {
  console.log(arguments.length)
  console.log(arguments[1])
  console.log(arguments.callee)
}

foo(10, 20, 30, 40, 50)

2.2 arguments 转数组

三种方式:

Array.prototype.slice.call(arguments)
Array.from(arguments)
[...arguments]

为什么 slice + call 能工作?

我们手写一个 slice:

Array.prototype.hyslice = function (start, end) {
  var arr = this
  start = start || 0
  end = end || arr.length

  var newArray = []

  for (var i = start; i < end; i++) {
    newArray.push(arr[i])
  }

  return newArray
}

var newArray = Array.prototype.hyslice.call(
  ["小吴", "why", "JS高级"],
  1,
  3
)

本质:

强行把 arguments 当作数组的 this


2.3 箭头函数为什么没有 arguments?

箭头函数:

  • 不绑定 this
  • 不绑定 arguments
  • 继承上层作用域

示例:

function foo() {
  var bar = () => {
    console.log(arguments)
  }

  return bar
}

var fn = foo(123)
fn()

图10-5 arguments打印结果

设计目的:

  • 保持语法简洁
  • 强化词法作用域一致性
  • 鼓励使用 ...rest

三、工程层面的思考

3.1 call / apply / bind 的真实差异

能力 call apply bind
改变 this
立即执行
返回函数
参数预设

3.2 实战建议

什么时候用 call?

  • 立即执行
  • 已知完整参数
  • 做方法借用

什么时候用 apply?

  • 参数已经是数组
  • Math.max.apply

什么时候用 bind?

  • 事件回调绑定
  • React 组件方法绑定
  • 部分参数预设

四、复盘与团队落地建议

关键结论

  1. 显式绑定本质是利用隐式调用规则
  2. bind 是对 call 的延迟封装
  3. arguments 是历史产物,优先使用 rest
  4. 所有 this 问题本质都是“调用方式问题”

团队落地建议

  1. code review 中严格检查 this 丢失问题
  2. 优先使用箭头函数 + rest
  3. 对高阶函数封装做统一规范
  4. 面试训练时必须能手写实现

理解 API 不等于掌握它。

真正的掌握,是知道:

  • 它解决什么问题
  • 为什么这样设计
  • 在复杂业务里如何避免踩坑

当你可以自己实现一遍,你就真正站在了语言机制这一层,而不只是使用层。

❌
❌