手写 call-apply-bind

34 阅读5分钟

手撕JS中的this显式绑定 call-apply-bind

能吃手撕牛肉,能吃手撕面包,当然能够手撕call-apply-bind

手撕call

  • 快速手撕 (可搭配使用官方的call方法来验证)
    Function.prototype.mycall = function(thisArg, ...args) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
        thisArg.fn = curFn
        var result = thisArg.fn(...args)
        delete thisArg.fn
        return result
    }

    function foo() {
        console.log(this, arguments) 
        if(arguments.length) {
            return Array.from(arguments).reduce((prev, item) => prev + item)
        }
    }

    foo() // window
    foo.mycall('abc') // String {'abc', fn: ƒ} 
    foo.mycall(null) // window
    foo.mycall(undefined) // window
    console.log(foo.mycall(Symbol('哈哈哈'), 20, 30, 40)) // Symbol {Symbol(哈哈哈), description: '哈哈哈', fn: ƒ}    90
  • 剖析
    Function.prototype.mycall = function() {
        var curFn = this
        // console.log(curFn)
        curFn()
    }
    function aaa() {
        console.log('aaa函数被执行了'this)
    }
    // 我们自己的call方法
    aaa.mycall() // window

    // 系统的call方法
    aaa.call() // window
  • 不是一个函数可以调用mycall方法,还有其他的任何函数都可以调用mycall方法,该怎么办?
    • 在函数的原型上添加一个自己的call方法,所有的函数就都可以使用自己的call方法了
  • 函数拥有了call方法并执行,但是调用call方法的那个函数的函数体没有执行(指aaa函数体),该怎么办?
    • 观察aaa.mycall()是不是是一个隐式调用,可以在mycall中获取this,打印结果为aaa函数
    • curFn()执行aaa函数
    Function.prototype.mycall = function(thisArg) {
        var curFn = this
        thisArg.fn = curFn
        thisArg.fn()
        delete thisArg.fn
    }
    function aaa() {
        console.log('aaa函数被执行了', this)
    }
    // 我们自己的call方法
    aaa.mycall({name: 'ywq'}) // {name: 'ywq', fn: ƒ}

    // 系统的call方法
    aaa.call({name: 'ywq'}) // {name: 'ywq'}
  • 系统的call方法,第一个参数是指定this指向的,思考一下,该怎么修改this指向呢?
    • 猜对了,进行隐式绑定,this为传递过来的thisArg。执行一下代码,试试吧
    • 代码执行完成后,是不是无故在内存中开辟了一个新的空间,我们来进行删除
    Function.prototype.mycall = function(thisArg) {
        var curFn = this
        thisArg.fn = curFn
        thisArg.fn()
        delete thisArg.fn
    }
    function aaa() {
        console.log('aaa函数被执行了', this)
    }
    // 我们自己的call方法
    aaa.mycall({name: 'ywq'}) // {name: 'ywq', fn: ƒ}
    aaa.mycall([1, 2, 3]) // [1, 2, 3, fn: ƒ]
    aaa.mycall(function bbb(){}) // ƒ bbb(){}
    // aaa.mycall(123) // Uncaught TypeError: thisArg.fn is not a function
    // aaa.mycall('哈哈哈') // Uncaught TypeError: thisArg.fn is not a function
    // aaa.mycall(false) // Uncaught TypeError: thisArg.fn is not a function
    // aaa.mycall(Symbol('符号')) // Uncaught TypeError: thisArg.fn is not a function

    // aaa.mycall(null) // {fn: ƒ}
    // aaa.mycall(undefined) // {fn: ƒ}

    // 系统的call方法
    aaa.call({name: 'ywq'}) // {name: 'ywq'}
    aaa.call([1, 2, 3]) // [1, 2, 3]
    aaa.call(function bbb(){}) // ƒ bbb(){}
    aaa.call(123) // Number {123}
    aaa.call('哈哈哈') // String {'哈哈哈'}
    aaa.call(false) // Boolean {false}
    aaa.call(Symbol('符号')) // Symbol {Symbol(符号), description: '符号'}

    aaa.call(null) // window
    aaa.call(undefined) // window
  • 我们来验证一下传入其他类型的绑定值,与系统的call是否一致。发现有的报错了,该怎么办?
    • 看报错信息,不是一个函数。哦,我们传入的大多都是一些基本数据类型报了这个错误,那使用Object() 或者 new Object()使thisArg成为对象,就可以添加方法并调用了
    Function.prototype.mycall = function(thisArg, ...args) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
        thisArg.fn = curFn
        var result = thisArg.fn(...args)
        delete thisArg.fn
        return result
    }
    function aaa() {
        console.log('aaa函数被执行了', this)
    }
    // 我们自己的call方法
    aaa.mycall(null) // 没有将thisArg转换为对象的结果是{fn: ƒ}
    aaa.mycall(undefined) // 没有将thisArg转换为对象的结果是{fn: ƒ}

    aaa.mycall(0, 10, 20, 30) // 

    // 系统的call方法
    aaa.call(null) // window
    aaa.call(undefined) // window

    aaa.call(0, 10, 20, 30)
  • 观察当this绑定的是null/undefined时,系统中的this指向window,我们自己的实现的call和系统的不一样,该怎么办呢?
    • 将null/undefined进行特殊处理
  • 系统中call还有其他的参数,我们该怎么做?
    • 使用剩余参数(...),获取全部的参数,传入函数并得到结果进行返回

手撕apply

  • 快速手撕 (可搭配使用官方的apply方法来验证)
    Function.prototype.myapply = function(thisArg, arrArgs = []) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
        thisArg.fn = curFn
        var result = thisArg.fn(...arrArgs)
        delete thisArg.fn
        return result
    }

    function foo() {
        console.log(this)
        if(arguments.length) {
            return Array.from(arguments).reduce((prev, item) => prev + item)
        } 
    }

    foo() // window
    foo.myapply() // window
    foo.myapply(null) // window
    foo.myapply(undefined) // window
    foo.myapply(123) // Number {123, fn: ƒ}
    foo.myapply({ No: 1 }) // {No: 1, fn: ƒ}
    console.log(foo.myapply(true, [1, 3, 9, 12, 7])) // Boolean {true, fn: ƒ}  32
  • 剖析 apply和call方法的实现相同,唯一不同的是传入参数的区别。所以下面只解析不同的部分,相同的部分参考上面实现的call方法
    
    Function.prototype.myapply = function(thisArg, arrArg) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
        thisArg.fn = curFn
        var result = thisArg.fn(...arrArg)
        delete thisArg.fn
        return result
    } 

区别就是传入的参数。除了this绑定,剩余的参数,apply方法传入的是一个数组,因此,在内部执行调用apply方法的函数时,将arrArg进行解构

手撕bind

  • 快速手撕 (可搭配使用官方的apply方法来验证)
    Function.prototype.mybind = function(thisArg, ...args) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg) 
        return function(...restArgs) {
            thisArg.fn = curFn
            var allArgs = [...args, ...restArgs]
            var result = thisArg.fn(...allArgs)
            delete thisArg.fn
            return result
        }
    }

    function foo() {
        console.log(this) 
        if(arguments.length) {
            return Array.from(arguments).reduce((prev, item) => prev + item)
        }
    }

    foo.mybind()() // Window
    foo.mybind(null)() // Window
    foo.mybind(undefined)() // Window
    foo.mybind(function bar() {}, 345)() // ƒ bar() {}
    console.log(foo.mybind({}, 10, 20, 30)(100, 2)) //{fn: ƒ}  162
    console.log(foo.mybind({})(10, 20, 30, 100, 2)) //{fn: ƒ}  162
  • 剖析 bind方法和call/apply方法的实现类似,相同部分不再解析。详细看上面实现的call方法。
    Function.prototype.mybind = function() {
        return function() {

        }
    }
  • 系统的bind方法,我们在获取结果时,需要再次执行一下。那我们该怎么办?
    • 在函数内部再返回一个函数不就可以啦
    Function.prototype.mybind = function(thisArg, ...args) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
        return function() {
            thisArg.fn = curFn
            var result = thisArg.fn()
            delete thisArg.fn
            return result
        }
    }
  • 我们可以将这段代码,自己写一个函数套用一下,看是否和系统一样。最终发现,系统的bind方法,二次调用的时候,也可以传入参数。那我们该怎么办呢
    Function.prototype.mybind = function(thisArg, ...args) {
        var curFn = this
        thisArg = thisArg === null || thisArg === undefined ? window : Object(thisArg)
        return function(...restArgs) {
            thisArg.fn = curFn
            var result = thisArg.fn(...[...args, ...restArgs])
            delete thisArg.fn
            return result
        }
    }
  • 在返回的那个函数中,获取二次调用的参数(...restArgs)。在内部执行调用bind方法的函数时,将我们传递的参数进行合并并进行结果,获取结果并返回