手写call函数详解

266 阅读6分钟

前言

本人认为call函数是我前端学习路上的一大难点,在我接触的项目中用的很少,但是还是想把它研究清楚。接下来将根据它能做的事情一步一步将其实现。废话不说,直接进入正题。

call能干什么?

使用一个指定的this值、传入多个参数来调用这个函数函数。
这是最常用的方法 fun.call(obj,args)

call使用示例

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
const res = fn.call({x:10},20,30)
console.log('res',res);
输出如下:
//a 20
//b 30
//this {x: 10}
//res hello
//here

可以看到,在此用例中,call函数接受了三个参数。第一个是一个对象,第二个与第三个是一个number类型数据。最终输出显示,this已经变为传入的对象{x:10},并将后面的20,30作为参数打印出来。

实现

1、定义mycall

首先我们定义个mycall函数。选择Object.prototype.mycallFunction.prototype.mycall都可以。这里涉及到一个原型链的知识点,这里稍微提及一下。

比如我们这里使用了fn.mycall,它首先会在fn这个对象上找,fn这个对象肯定没有撒,那就顺着fn.__proto__去找。fn.__proto__也是个对象,这个对象具体是什么呢?

fn.__proto__这个对象就是Function.prototype这个对象。来验证一下

//接着上面的代码
console.log(fn.__proto__ == Function.prototype);//true

因此,我们如果将mycall函数写入Function.prototype,则是可以找到滴。


如果写到Object.prototype.mycall呢?那就继续找呗。怎么找?沿着__proto__找啊,也就是:fn.__proto__.__proto__。而fn.__proto__.__proto__这个东西又是什么呢?大家只需记住,就是Object.prototype。继续验证一番

//继续接着上面的代码
console.log(fn.__proto__.__proto__ == Object.prototype);//true

因此,将mycall写入Object.prototype也是可以滴。

扯远了扯远了,回到正题。

最终代码来了:

Object.prototype.mycall = function(){
         ...
}

好吧,属实废话了很多,继续下一步。

2、解构参数

经过上一步骤,我们mycall函数的框架已经搭建完毕,接下来就是要传入参数,并且执行了吧。

问题来了?我们要如何描述传入的参数呢?总不能直接指定a,b,c吧

Object.prototype.mycall = function(a,b,c){
         ...
}

万一多传几个值不就接收不到了,pass。

我们这里用到...args展开用算符

Object.prototype.mycall = function(...args){
         ...
}

当然这个不是什么难点,我提它只是想着重说明我个人写mycall的思路。

观察到,mycall函数第一个参数是一个对象,我们的目的是要将this指向这个对象。之后的参数都是mycall函数中使用到的。所以,我们可以用slice函数将参数分割开。

Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    //{x: 10} [20,30]
}

3、执行

上一步骤已经将参数成功提取出来,接下来就是要执行了。but,如何执行fn函数呢?

以下就是目前mycall的全部代码。

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
}
fn.mycall({x:10},20,30)

难道这样执行?

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    `fn()`
}
fn.mycall({x:10},20,30)

以上的执行方式是不行的,因为这个mycall是个公共函数,如果函数名不叫fn,直接无法使用。

注意,我们可以使用 this

大家都知道,普通函数(非箭头函数),this的指向就是函数的调用者。也就是说,mycall这个函数是由fn函数调用的。因此,在mycall函数内部,this指的就是fn函数,来验证一下

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    console.log('this',this);
}
fn.mycall({x:10},20,30)

执行结果如下:

image.png

所以呢,我们可以通过借用this来执行fn函数。

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    this(obj,...ar)
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);

执行结果如下:

image.png

4、解决this指向

由上一步骤可以看到,参数已成功传入,输出了a与b,但是this指向的是window,因为fn是在全局下执行的。但是我们想要的this是传入参数{x:10}这个this。怎么办呢?

首先我们肯定对下面这段代码很熟悉吧

let obj = {
    name:'test',
    say: function(){
        console.log("this",this,this.name);
    }
}
obj.say()

相信大家一眼就能知道输出。

image.png

这里的this指向的是obj这个对象,而不是window(总之一句话,非箭头函数,this指向调用它的人)。say函数被obj对象调用,因此this指向obj

利用这个特性,我们可以以如下方式来执行上一步中的this函数

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    obj.say = this
    obj.say(obj, ...ar)
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);

执行结果如下:

image.png

nice,可以看到this已经指向了我们传入的对象obj


也许我们会有这样的想法,这样写行不行呢?

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    obj.say = function(){
        console.log("this", this);
        this(obj, ...ar)
    }
    obj.say()
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);

这样首先会报this is not a function错误,为什么呢? 因为obj.say里的this已经不再是fn了,而变成obj这个对象了,obj既然是个对象,哪来的名为thisfunction,压根没定义。

image.png

好,顺着往下走,有同学会想,既然这个this不是fn,那么我就不用this(obj, ...ar)了,我把fn存起来,然后执行不就完了吗?所以我就这么写

function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);
    return 'hello'
}
Object.prototype.mycall = function(...args){
    const self = this
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    obj.say = function(){
      self(obj, ...ar)
    }
    obj.say()
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);

这样就行了吗?很遗憾,也是不可以的,因为this指向还是不对的。看一下运行结果,this又指向了window

image.png

为什么会这样呢?,是因为self不是被obj这个对象调用,而是被全局调用

可以类比这个例子

let obj = {
    name:'test',
    say: function(){
        fn()
    }
}
function fn(){
    console.log("this",this);
}
obj.say()

执行结果如下

image.png

这个涉及到了this的指向问题,在本文不是重点,我这里就不做过多的阐述。总而言之,是因为fn的调用者是全局对象window,而不是obj

5、确定返回值

以上4步就差不多完成自定义call函数了,还有个小尾巴,就是确定返回值。这个比较简单,没有很难的。我们只需要确定什么函数该返回什么值即可。

  1. 首先self函数(就是fn函数)返回的是"hello"
  2. res接收这个返回值,并作为mycall的返回值。
function fn(a,b){
    console.log('a',a);
    console.log('b',b);
    console.log('this',this,this.x);

    return 'hello'
}
Object.prototype.mycall = function(...args){
    const self = this
    const obj = args.slice(0,1)[0]
    const ar = args.slice(1)
    obj.say = function(){
      return self(obj, ...ar)
    }
    let res = obj.say()
    delete obj.say //删除掉obj.say,因为它只是暂时的。
    return res
}
const res2 = fn.mycall({x:10},20,30)
console.log('res2',res2);

执行结果:

image.png

总结

以上就是自定义call函数的实现,里面涉及了原型链this指向问题,这些知识点都是前端必须要掌握的。mycall函数是按照我的思路方式一步一步来写的,可能并不是完美的,但是自我感觉,还是最容易懂得。如果有什么问题,欢迎小伙伴们指正哦。

如果有同学还不是特别懂,可以观看一下b站上这个自定义call函数的实现,我就是受其启发。

参考视频

手写call函数