一步一步解析call/apply/bind实现原理

1,130 阅读4分钟

Header:

本篇不讨论具体用法,着重研究实现原理,全篇采用es6代码

callapplybind三个方法存在于Function.prototype上方法,所以所有函数都能调用该三个方法。三个方法的主要作用都在于改变函数执行上下文this,以达到不同的目的。

call、apply

因为俩者只是在参数传递上不一样,所以拿在一起研究,且也不模拟apply实现

原生call,apply使用:

    const obj1 = {
        value: 9
    };
    const showValue = function() {
        console.log(this.value);
    };
    
    // 执行call/apply函数改写context
    showValue.call(obj1); // 输出 9
    showValue(); // undefined

分析执行过程

  1. 函数调用call/apply方法

    说明call,apply方法存在于Function原型上,也就是Function.prototype上,那我们模拟时也在Function.prototype上添加call2方法

    Function.prototype.call2 = function() {};
    
  2. 传入想要改写成的上下文对象

    说明call/apply的第一个参数接受为一个对象

    Function.prototype.call2 = function(context) {};
    
  3. 语句执行,输出结果1

    this.value的值为1,说明this指向obj1,也就是说这个时候showValue函数是obj1的属性,则在call2函数内部,就应该将this(this代表showValue函数)添加到obj1上,然后直接执行。

    Function.prototype.call2 = function(context) {
        context.fn = this;
        context.fn();
    };
    
  4. 再次执行 showValue() 函数,输出结果undefined

    在第三步我们已经将obj1上面添加了属性showValue函数,而在再次执行时,this.value结果却不是预期的1,则可知在call内部添加obj1属性showValue并执行后,就将该属性从obj1中删除。

    Function.prototype.call2 = function(context) {
       context.fn = this;
       context.fn();
       delete context.fn;
    };
    

完整代码(注意:此处没有添加各种情况判断代码,如context=null||undefined||基本类型等)

    const obj1 = {
        value: 9
    };
    const showValue = function() {
        console.log(this.value);
    };
    
    // 执行call/apply函数改写context
    showValue.call(obj1); // 输出 9
    showValue(); // undefined
    
    // 模拟代码
    // 完善参数传递等其他部分
     Function.prototype.call2 = function(context, ...args) {
        context.fn = this;
        const res = context.fn(...args);
        delete context.fn;
        return res;
    };
     // 执行call2函数改写context
    showValue.call2(obj1); // 输出 9
    showValue(); // undefined
    

bind

原生bind使用:

1. 简单使用

    const obj = { title: 'obj-title' };
    const showTitle = function() {
        console.log(this.title);
    };
    const showObjTitle = showTitle.bind(obj);
    showObjTitle(); // obj-title;

1.1 分析执行过程

  1. 函数调用bind函数,传入context对象。

    发现bind()返回一个函数

    Function.prototype.bind2 = function(context) {
        return function() {};
    };
    
  2. 调用showObjTitle()函数,函数输出obj-title

    函数输出了第一步绑定的obj中的title,那么说明在返回的函数内部执行了call/apply类似的功能代码,那么次数就意味着可以直接用call/apply代替函数内的功能代码。

    Function.prototype.bind2 = function(context) {
        const fn = this;
        return function() {
            return fn.apply(context);
        };
    };
    

2. 作为柯里化函数使用

    const obj = { content: 'obj-content' };
    const showTitle = function(title, content) {
        let con = content;
        if (this.content) con += this.content;
        console.log(title, ':::', con);
    };
    const showObjTitle = showTitle.bind(obj, 'bind-title');
    showObjTitle('-bind-content'); // bind-title ::: -bind-contentobj-content

2.1 分析执行过程

  1. 调用bind函数时传入第二个参数'bind-title'

    bind函数可以接受第二个参数

    Function.prototype.bind2 = function(context, arg1) {
        const fn = this;
        return function() {
            return fn.apply(context);
        };
    };
    
  2. 调showObjTitle时传入了一个参数'-bind-content'

    bind函数返回的函数可以接受参数

    Function.prototype.bind2 = function(context, arg1) {
        const fn = this;
        return function(arg2) {
            return fn.apply(context);
        };
    };
    
  3. 调用showObjTitle()函数,函数输出obj-title:::obj-content-bind-content;

    最后可以看出前面分别在bind时和调用showObjTitle时传入的参数都被fn按顺序接收到了,所以由此看出,在返回的函数内部要将前面俩次传入的参数相加并传入到fn内部。

    Function.prototype.bind2 = function(context, arg1) {
        const fn = this;
        const argTemp = arg1 === undefined ? [] : [arg1];
        return function(arg2) {
            return fn.apply(context, arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp);
        };
    };
    

3. 使用new关键字使用(函数的构造调用)

    const obj = { title: 'obj-title', content: 'obj-content', time: '1999-9-9' };
    const News = function(title, content) {
       this.title = title;
       this.content = content;
       this.time = this.time || '2019-9-9';
    };
    News.prototype.getContent = function() {
        console.log(this.content);
    };
    const DogNews = News.bind(obj, 'dog');
    const dn = new DogNews('cute');
    console.log(dn.title); // dog
    console.log(dn.time); // 2019-9-9
    dn.getContent(); // cute

3.1 分析执行过程

  1. 使用new关键字调用了DogNews方法,语句返回了一个实例,并在下面的语句中输出了传入的参数。

    可以看出因为执行new关键字,导致前面bind的context失效,取而代之的this则是agentConstructor的一个实例,那么这里就需要做判断处理,从而兼容不使用new的情况。

    Function.prototype.bind2 = function(context, arg1) {
    const fn = this;
    const argTemp = arg1 === undefined ? [] : [arg1];
    const agentConstructor = function(arg2) {
        const argsOfAll = arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp;
        if (this instanceof agentConstructor) {
            fn.apply(this, argsOfAll);
        }
        return fn.apply(context, argsOfAll);
        };
    };
    return agentConstructor;
    
  2. dn.getContent()语句输出了cute。

    getContent是News原型上的方法,而通过new DogNews('cute')语句返回的DogNews实例也能够访问到是News原型上的方法,那么则说明在bind时返回的函数上的原型跟News原型发生了关系。但是如果直接使agentConstructor.prototype = News.prototype的话,在agentConstructor原型发生改变时,也将会影响News的原型,那么此处就要再使用一个不对外公开的空函数进行承接。

    Function.prototype.bind2 = function(context, arg1) {
    const fn = this;
    const argTemp = arg1 === undefined ? [] : [arg1];
    const TempFn = function() {};
    const agentConstructor = function(arg2) {
        const argsOfAll = arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp;
        if (this instanceof agentConstructor) {
            fn.apply(this, argsOfAll);
        }
        return fn.apply(context, argsOfAll);
        };
    };
    TempFn.prototype = fn.prototype;
    agentConstructor.prototype = new TempFn();
    return agentConstructor;
    }
    

完整代码

 Function.prototype.bind2 = function(context, arg1) {
 if (typeof this !== 'function') {
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    const fn = this;
    const argTemp = arg1 === undefined ? [] : [arg1];
    const TempFn = function() {};
    const agentConstructor = function(arg2) {
        const argsOfAll = arg2 !== undefined ? [ ...argTemp, arg2 ] : argTemp;
        if (this instanceof agentConstructor) {
            fn.apply(this, argsOfAll);
        }
        return fn.apply(context, argsOfAll);
        };
    };
    TempFn.prototype = fn.prototype;
    agentConstructor.prototype = new TempFn();
    return agentConstructor;
    }

结束语

至此,关于call/apply/bind的简单原理实现就完成了.该文章主要用于学习梳理。