从原理到手写:彻底吃透 call / apply / bind 与 arguments 的底层逻辑
引言:为什么我们要“手写”这些 API?
在日常开发中,call、apply、bind 几乎每天都会用到。无论是处理 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.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 组件方法绑定
- 部分参数预设
四、复盘与团队落地建议
关键结论
- 显式绑定本质是利用隐式调用规则
- bind 是对 call 的延迟封装
- arguments 是历史产物,优先使用 rest
- 所有 this 问题本质都是“调用方式问题”
团队落地建议
- code review 中严格检查 this 丢失问题
- 优先使用箭头函数 + rest
- 对高阶函数封装做统一规范
- 面试训练时必须能手写实现
理解 API 不等于掌握它。
真正的掌握,是知道:
- 它解决什么问题
- 为什么这样设计
- 在复杂业务里如何避免踩坑
当你可以自己实现一遍,你就真正站在了语言机制这一层,而不只是使用层。