各位前端探险家们,今天我要带你们体验一场刺激的解剖实验!当我们每天用着
call
、apply
和bind
时,有没有想过它们的五脏六腑长什么样?今天咱们就亲手扒开它们的"衣服",看看这三个函数界的三胞胎到底有什么异同!
第一幕:call 的诞生记 —— 霸道总裁式绑定
先看我们手写的myCall
实现:
Function.prototype.myCall = function (object, ...args) {
if (this instanceof Function === false) {
throw new TypeError('Function.prototype.myCall called on non-function')
}
if (object === null || object === undefined) {
object = window
}
const fn = Symbol('fn')
object[fn] = this
const res = object[fn](...args)
delete object[fn]
return res
}
这段代码堪称"借尸还魂"大法!让我拆解它的黑魔法:
- 保镖式检查:
if (this instanceof Function === false)
确保调用者必须是函数,否则直接抛出错误——就像夜店门口的保安:"非函数人员禁止入内!" - 空值救世主:当
object
为null
或undefined
时,自动指向window
(浏览器环境),这解释了为什么func.call(null)
不会报错而是指向全局 - Symbol 隐身术:
const fn = Symbol('fn')
创建唯一键,避免属性冲突,就像给函数戴了隐身斗篷 - 偷梁换柱:
object[fn] = this
把当前函数绑定到目标对象上,瞬间完成身份窃取 - 借壳执行:
object[fn](...args)
以对象方法的形式调用函数,this
自然指向目标对象 - 毁尸灭迹:
delete object[fn]
删除临时属性,深藏功与名
测试用例也很有趣:
func.myCall(null, 1, 2) // 全局调用
p.myCall(Person) // 非函数报错,TypeError警告!
第二幕:apply 的模仿秀 —— 数组参数的艺术
再看myApply
实现:
Function.prototype.myApply = function (obj, args) {
// ...类似myCall的检查
const fn = Symbol('fn')
obj[fn] = this
const res = obj[fn](...args) // 关键区别在这里!
delete obj[fn]
return res
}
发现玄机了吗?apply
和call
是失散多年的亲兄弟!唯一的区别是:
call
接受参数列表:func.call(obj, 1, 2)
apply
接受参数数组:func.apply(obj, [1, 2])
在实现上,仅仅是...args
和...arguments[1]
的区别。测试时特别注意数组处理:
func.myApply(Person, [1, 2]) // 数组解构传参
console.log(func.myApply(Person, [1, 2])); // 打印返回值
第三幕:bind 的变身术 —— 柯里化大师
最精彩的myBind
来了:
Function.prototype.myBind = function (obj, ...args) {
if (this instanceof Function === false) {
throw new TypeError('TypeError: Not a function')
}
if (obj === null || obj === undefined) {
return (...Args) => this.call(window, ...args, ...Args)
}
return (...Args) => this.call(obj, ...args, ...Args)
}
bind
是三人中最"阴险"的——它不立即执行函数,而是返回一个新函数!其精妙在于:
- 双次参数合并:
...args
(绑定时的参数)和...Args
(调用时的参数)合并,实现柯里化 - 空值双保险:对
obj
为null/undefined
的情况单独处理,指向全局 - 闭包记忆术:返回的函数记住了
obj
和args
,形成闭包
测试用例展示其魔法:
const res = func.myBind(person, 1, 2, 3, 4) // 预置参数
res() // 稍后调用
const res2 = func.myBind(null, 1, 2) // 绑定全局
res2(3, 4) // 追加参数
三剑客对比表:性格大揭秘
方法 | 调用方式 | 返回值 | 核心技能 |
---|---|---|---|
call | 立即执行 | 函数结果 | 参数列表、快速绑定 |
apply | 立即执行 | 函数结果 | 参数数组、适合变长参数 |
bind | 返回新函数(延迟执行) | 绑定后函数 | 参数柯里化、灵活调用 |
实战中的骚操作
场景1:伪数组转正
Array.prototype.slice.myCall(arrayLike)
场景2:Math.max查数组最大值
Math.max.myApply(null, [1, 2, 3])
场景3:事件绑定防丢失
button.addEventListener('click', handler.myBind(user))
你可能掉进的坑
- Symbol不是必须的:用
Date.now()
或随机数也能生成唯一key,但Symbol是最优雅的方案 - 严格模式陷阱:在严格模式下,
call(null)
时this
指向null
而非window
,我们的实现做了简化 - 箭头函数叛变:箭头函数的
this
无法被call/bind改变,记住它们永远是"顽固派"!
终极思考:为什么需要改变 this?
想象你去朋友家做客:
- 直接调用
func()
——你在自己家做饭 func.call(friend)
——你去朋友家用他的厨房func.bind(friend)
——获得一个"在朋友家做饭"的技能包
this
的本质就是函数执行的上下文环境,而三剑客就是控制环境的遥控器!
总结
今天我们亲手解剖了JS三大绑定神器:
- call —— 直来直去的急性子
- apply —— 喜欢打包的收纳师
- bind —— 步步为营的策画家
经过这场解剖实验,相信你再也不会被
this
的七十二变迷惑了!下次面试被问"手写bind"时,记得露出神秘的微笑——"您想听ES5版本还是ES6版本?" 😉