今天我把 call、apply 和 bind 扒光了!—— 手写三剑客的奇妙冒险

0 阅读4分钟

各位前端探险家们,今天我要带你们体验一场刺激的解剖实验!当我们每天用着callapplybind时,有没有想过它们的五脏六腑长什么样?今天咱们就亲手扒开它们的"衣服",看看这三个函数界的三胞胎到底有什么异同!

第一幕: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
}

这段代码堪称"借尸还魂"大法!让我拆解它的黑魔法:

  1. 保镖式检查if (this instanceof Function === false) 确保调用者必须是函数,否则直接抛出错误——就像夜店门口的保安:"非函数人员禁止入内!"
  2. 空值救世主:当objectnullundefined时,自动指向window(浏览器环境),这解释了为什么func.call(null)不会报错而是指向全局
  3. Symbol 隐身术const fn = Symbol('fn')创建唯一键,避免属性冲突,就像给函数戴了隐身斗篷
  4. 偷梁换柱object[fn] = this把当前函数绑定到目标对象上,瞬间完成身份窃取
  5. 借壳执行object[fn](...args)以对象方法的形式调用函数,this自然指向目标对象
  6. 毁尸灭迹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
}

发现玄机了吗?applycall是失散多年的亲兄弟!唯一的区别是:

  • 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是三人中最"阴险"的——它不立即执行函数,而是返回一个新函数!其精妙在于:

  1. 双次参数合并...args(绑定时的参数)和...Args(调用时的参数)合并,实现柯里化
  2. 空值双保险:对objnull/undefined的情况单独处理,指向全局
  3. 闭包记忆术:返回的函数记住了objargs,形成闭包

测试用例展示其魔法:

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))

你可能掉进的坑

  1. Symbol不是必须的:用Date.now()或随机数也能生成唯一key,但Symbol是最优雅的方案
  2. 严格模式陷阱:在严格模式下,call(null)this指向null而非window,我们的实现做了简化
  3. 箭头函数叛变:箭头函数的this无法被call/bind改变,记住它们永远是"顽固派"!

终极思考:为什么需要改变 this?

想象你去朋友家做客:

  • 直接调用func()——你在自己家做饭
  • func.call(friend)——你去朋友家用他的厨房
  • func.bind(friend)——获得一个"在朋友家做饭"的技能包

this的本质就是函数执行的上下文环境,而三剑客就是控制环境的遥控器!

总结

今天我们亲手解剖了JS三大绑定神器:

  1. call —— 直来直去的急性子
  2. apply —— 喜欢打包的收纳师
  3. bind —— 步步为营的策画家

image.png 经过这场解剖实验,相信你再也不会被this的七十二变迷惑了!下次面试被问"手写bind"时,记得露出神秘的微笑——"您想听ES5版本还是ES6版本?" 😉