经典面试题——手写call/apply/bind方法

1,155 阅读3分钟

铁汁们这个问题最好自己动手写一遍,我以为我会了结果面试被问懵逼了...

Call

需求

使用指定的this调用函数

思路

变更this

首先要知道this是怎么指的,冴羽大大的这篇文章我觉得讲得很不错

github.com/mqyqingfeng…

简单一点,可以大概理解为this指向了函数调用者,[[调用者]].function

那么思路就是,让指定的object成为函数的调用者

处理参数

call接受任意个参数,从第二个开始作为被调用函数的参数。这是和apply不一样的地方。

接受任意参数有两种思路:

  • 处理arguments (原生js)

拼出一个字符串,像这样 "f(arguments[1], ...,arguments[n])" 然后用eval执行

  • 解构运算符 (ES6)
call(target, ...args)

原生js处理的思路并不是那么容易想到,但是见过了以后就会很轻松想到这上面。

说起来,拼字符串然后eval一下来实现code generation也是一个常规操作哈

实现

Function.prototype.myCall = function (thisTarger) {
    // 接受null
    var thisTarget = thisTarget || window
    thisTarger.f = this // 关键步骤
    args = []
    for (var i = 1; i < arguments.length; ++i) {
        args.push('arguments[' + i + ']')
    }
    // 数组这里会隐式转换成逗号分隔的形式
    var result = eval('thisTarger.f(' + args + ')')
    delete thisTarger.f
    return result // 记得返回值
}

apply

apply只接受两个参数,第二个是原函数参数数组,所以我们就把从arguments拼接变成从给的数组拼接就完事了。

Function.prototype.myApply = function (thisTarget, arr=[]) {
    var thisTarget = thisTarget || window
    thisTarget.f = this
    var args = []
    var result
    // 只有这里变了
    for (var i = 0; i < arr.length; ++i) {
        args.push('arr[' + i + ']')
    }
    result = eval('thisTarget.f(' + args + ')')
    delete thisTarger.f
    return result
}

bind

需求

  • bind返回一个函数,该函数执行时使用给定的this
  • bind可以接受参数,在调用时绑定函数的参数列表中

返回一个函数,那就是说可以被new,如果被new的话,this不应该指向给定的this,因为如果new的this指向会被改变的话,整个晋西北就乱成一锅粥了。

思路

bind会返回一个函数,方便表达就叫boundF吧。boundF一定是我们在bind内部声明的函数,所以整个过程涉及到三个函数不要搞混了。

  • 原函数
  • bind
  • bind中声明的boundF

调用使用指定this

用call/apply就好了

预绑定参数

在bind缓存arguments,用闭包绑到boundF中,和boundF接受的参数合并

处理new

new的表现

  • this指向了boundF正在构造的新对象
  • boundF的prototype在新对象的原型链上

解决思路

我们主要需要做两件事:

  • 判断是不是被new了,是的话把this还给新对象

即判断boundF是不是在新对象(this)的原型链上

  • 保证new出来的对象原型链和原函数一致而不是和boundF一致

把原函数的原型链搞到boundF的prototype上即可

实现

整活!实现中有一些细节问题要考虑,所以强烈推荐大家自己写一遍!

Function.prototype.myBind = function (thisTarget) {
    // this指向原函数
    var that = this
    // 构造闭包,因为arguments在boundF中会被覆盖访问不到了
    var args = Array.prototype.slice.apply(arguments, 1)

    var boundF =  function () {
        // 这里的arguments是boundF的arguments, 也就是实际调用时的参数
        var bindArgs = Array.prototype.slice.call(arguments)
        // 这里的this有两种:
        //      boundF (普通调用)
        //      newObj (new)
        // 注意boundF instanceof boundF是false的
        return that.apply(this instanceof boundF ? this : thisTarget, args.concat(bindArgs))
    }
    // 切换原型链
    boundF.prototype = Object.create(this)
    return boundF
}

Reference

github.com/mqyqingfeng… github.com/mqyqingfeng…

这是【恶补系列】的第二篇文章,给坚持下来的自己点个赞(逃