手动实现一个call

2,889 阅读4分钟

手动实现一个call

作者:青鸾

call

JS中,有一些很神奇的存在,比如我们今天要讲到的call,它可以改变this指向,而且用法简单,预支类似的还有bind、apply,三者的区别在于,bind的返回值是一个函数体,需要手动执行。call与apply基本一样,区别在于apply最多只接收两个参数,第二个参数是一个数组,其实在使用call和apply的时候,对我们来说无非就是是否要三个点,call和apply会立刻执行,无需再次执行。 今天我们来简单的去实现一个自己的call方法。

使用call的最简单案例

    let obj = { a: 1 }
    function fn() {
        console.log(this.a)
    }
    fn.call(obj)

fn中并没有a属性,我们使用call将this指向obj对象,这样this.a便有值了。 这是一个最基本call的使用,我们需要注意两点: 1、函数call之后被执行了 2、使用call的函数this指向到了call的第一个参数

第一步很容易,第二步如何实现呢,其实很简单,我们直接把这个方法添加为call参数的属性,比如上面的obj和fn,我们这样,即可实现类似第二步的效果,简单来说就是加属性

let obj = {
	a: 1,
	fn: function() {
		console.log(this.a)
	}
}

考虑到这两点,我们便可以先开始实现一个简单的自定义call方法

Function.prototype.call2 = function(v) {
     // 此处的v便是我们上文中的obj参数
     v.fn = this
     // 此处的this便是调用call的函数
     v.fn()
     // 此处的fn名称是任意的,我们加了一个属性名为fn的属性并且赋值进行了调用
     delete v.fn
     // 调用完成后删掉添加的属性fn
 }

我们再来试一下

let obj = { a: 1 }
  function fn() {
      console.log(this.a)
  }
  Function.prototype.call2 = function(v) {
      v.fn = this
      v.fn()
      delete v.fn
  }
  fn.call2(obj)
  // 1

好耶!第一步很顺利,接下来我们就该考虑call独有的传参了,call接收参数的个数是不限的,为此我们可以使用函数的arguments属性,这是一个只能在函数内部访问的属性,是一个有长度的类数组对象,虽然只能在函数内部访问,不过足够用了。

为了通俗易懂,我们直接将上文的例子稍加变动,如下:

let obj = { a: 1 }
function fn(x, y) {
    console.log(x)
    console.log(y)
    console.log(this.a)
}
fn.call(obj, 3, 2)
// 3
// 2
// 1

call的结果为3、2、1,这时候就需要对我们初版的call进行变动了。

Function.prototype.call2 = function() {
    const args = []
    // 此处我们加入了对第一个参数为null的判断
    const v = arguments[0] === null ? window : arguments[0]
    for(var i = 1; i < arguments.length; i++) {
        args.push(arguments[i])
    }
    // 此处需要注意,即使v是Array类型,依然可以直接给其添加属性
    v.fn = this
    // ...args便是除了第一个参数之外的剩余参数,剩余运算符是ES6的新特性之一
    v.fn(...args)
    delete v.fn
}

这时候我们再输出,

fn.call2(obj, 3, 2)
// 3
// 2
// 1

发现与call一样了,但我们又想到了,我们上述的函数fn,都是没有return没有返回值的,那么有返回值的又该如何处理呢,其实很简单,加入return就可以了

Function.prototype.call2 = function() {
    const args = []
    const v = arguments[0] === null ? window : arguments[0]
    for(var i = 1; i < arguments.length; i++) {
        args.push(arguments[i])
    }
    v.fn = this
    // 此处我们使用一个变量记录函数执行的返回值,然后将其返回
    let res = v.fn(...args)
    delete v.fn
    return res
}

这样就可以有返回值了,可能有小伙伴会对v.fn这块有疑问,觉得如果是数组或者函数怎么办,其实这些都不会有任何影响,我们简单尝试一个数组

var arr = [1];
function fn() {
    console.log(this[0])
}
fn.call(arr)
// 1

这里其实要涉及到一些JS原生部分,就不多做介绍了,简而言之,我们给数组添加的属性,我们是可以像对象那样去访问的。 不考虑极端情况,这个call我们已经基本完成了,但是笔者拿着写好的自定义的call方法在尝试的时候,发现了一个问题,在将Math.max.call更换为Math.max.call2时,出现了异常。

Math.max.call(...[1,2,3])
// 3
Math.max.call(null, ...[1,2,3])

我们写的call方法其实没有考虑到第一种情况——call的第一个参数是基本数据类型,比如1,'str',这种,使用官方提供的call我们可以看到有一步包装,这个笔者暂时还没想到怎么实现,各位有兴趣的也可以想一下,具体如下:

function fn() { console.log(this) }
fn.call(1)
// Number {1}
fn.call('str')
// String {'str'}

贴上最后的完整代码,最后一步暂时使用了穷举,还没有想到好的办法

Function.prototype.call2 = function () {
    const args = []
    var v
    if (typeof arguments[0] === 'object') {
        v = arguments[0] === null ? window : arguments[0]
        for (var i = 1; i < arguments.length; i++) {
            args.push(arguments[i])
        }
        // 此处需要注意,即使v是Array类型,依然可以直接给其添加属性
        v.fn = this
        let res = v.fn(...args)
        delete v.fn
        if (res !== undefined) {
            return res
        }
    } else {
        v = arguments[0] === undefined ? window : arguments[0]
        switch (typeof v) {
            case 'number':
                v = new Number(v)
                break;
            case 'string':
                v = new String(v)
            case 'boolean':
                v = new Boolean(v)
            default:
                break;
        }
        for (var i = 1; i < arguments.length; i++) {
            args.push(arguments[i])
        }
        // 此处需要注意,即使v是Array类型,依然可以直接给其添加属性
        v.fn = this
        let res = v.fn(...args)
        delete v.fn
        if (res !== undefined) {
            return res
        }
    }
}

技术分享宣传图@3x.png