美文网首页
call,apply,bind实现以及原理

call,apply,bind实现以及原理

作者: 一现_ | 来源:发表于2019-08-07 10:29 被阅读0次

    (一) call源码解析

    例子:

    function add(c, d){
        return this.a + this.b + c + d
    }
    
    var obj = { a: 1, b: 2 }
    console.log(add.call(obj, 3, 4))
    

    网上介绍call大致说法是,call改变了this的指向,本来上下文环境调用的thiswindow,而callthis指向了obj,brabra,不管你懂不懂,成功绕晕你就已经ok了,但是实际发生的过程,可以看成下面的样子。

    var o = {
      a: 1,
      b: 2,
      add: function(c, d) {
        return this.a + this.b + c + d
      }
    };
    
    1. 调用之后,新创建一个对象o,obj上的属性变成o的属性
    2. 给o对象添加一个add属性,这个时候 this 就指向了 o,
    3. o.add(5,7)得到的结果和add.call(o, 5, 6)相同。
    4. 但是给对象o添加了一个额外的add属性,这个属性我们是不需要的,所以可以使用delete删除它。

    so, 基本上就三步

    // 1. 将函数设为对象的属性
     o.fn = bar
    // 2. 执行该函数
     o.fn()
    // 3. 删除该函数
     delete o.fn
    

    基于ES3实现 call

    Function.prototype.es3Call = function (context) { 
        var content = context || window; 
    2    content.fn = this; 
        var args = []; 
        // arguments是类数组对象,遍历之前需要保存长度,过滤出第一个传参 
        for (var i = 1, len = arguments.length ; i < len; i++) { 
            // 避免object之类传入 
            args.push('arguments[' + i + ']');  
        } 
        var result = eval('content.fn('+args+')'); 
        delete content.fn; 
        return result; 
        
    } 
    console.error(add.es3Call(obj, 3, 4)); // 10 
    console.log(add.es3Call({ a: 3, b: 9 }, 3, 4)); // 19
    console.log(add.es3Call({ a: 3, b: 9 }, {xx: 1}, 4)); // 12[object Object]4
    

    解析,第2步是关键,将实例的环境this传入,实际上就是函数调用,称其为实际执行函数,args是参数,实际上就是调用了这个函数,也就是传说中的改变this指向了

    ES6实现call,差别不大,es6新增...rest运算符,进行对有iterator属性进行取值操作

    // ES6 call 实现 
    Function.prototype.es6Call = function (context) { 
        var context = context || window; 
        context.fn = this; 
        var args = []; 
        for (var i = 1, len = arguments.length; i < len; i++) {      
            args.push('arguments[' + i + ']');  
        } 
        const result = context.fn(...args); 
        return result; 
        
    }
        console.error(add.es6Call(obj, 3, 4));  
        console.log(add.es3Call({ a: 3, b: 9 }, {xx: 1}, 4)); // 12[object Object]4 
    

    (二) apply源码解析

    apply 和 call 区别在于 apply 第二个参数是Array,而 call 是一个个传入

    // 基于es3的实现

    Function.prototype.es3Apply = function (context, arr) { 
        var context = context || window; 
        context.fn = this; 
        var result; 
        if (!arr) { 
            result = context.fn();  
        }  else { // 获取参数 
            var args = []; 
            for (var i = 0, len = arr.length; i < len; i++) {
                args.push('arr[' + i + ']'); 
                
            } 
            // 执行函数 
            result = eval('context.fn(' + args + ')') 
            
        } 
        delete context.fn; return result 
        
    } 
    console.log(add.es3Apply(obj, [1, 'abc', '2'])); // 4abc 
    console.log(add.apply(obj, [1, 2]));  // 6
    

    基于es6的实现

    Function.prototype.es6Apply = function(context, arr){
        var context = context || window;
        context.fn = this;
        var result;
        if(!arr) {
            result = context.fn();
        } else {
            if(!(arr instanceof Array)) 
                throw new Error('params must be array');
            result = context.fn(...arr)
        }
        delete context.fn;
        return result;
    }
    
    console.log(add.es6Apply(obj, [1, 2])); // 6
    

    (三)bind 源码解析

    bind()方法回创建一个新函数。
    当这个新函数被调用时,bind()第一个参数将作为它运行时的this,之后的一序列参数将会在传递的实参前传入作为它的参数

    bind方法实例

    function foo(c, d){
        this.b = 100;
        console.log(this.a);
        console.log(this.b);
        console.log(this.c);
        console.log(this.d);
    }
    // 将foo bind到{ a: 1 }
    var func = foo.bind({ a: 1 }, '1st');
    func('2nd');
    // 1 100 1st 2nd
    // 即使再次call也不能改变 this 
    func.call({ a: 2 } , '3rd');  
    // 1 100 1st 2nd
    
    // 当 bind 返回的函数作为构造函数的时候
    // bind 时指定的 this 会失效, 但传入的参数依然生效
    // 所以使用func为构造函数时,this不会指向{ a: 1 },this.a 为undefined
    new func('4th')
    //undefined 100 1st 4th
    

    bind()方法绑定了首参数的时候,就已经是改不了引用了,一直都用这个引用,除非当成是构造函数进行调用,this会改变指向,调用构造函数的话,this会指向实例

    首先用ES3来实现

    Function.prototype.es3Bind = function (context) {
        if(typeof this !== 'function')
            throw new TypeError('what is trying to be bound is not callback');
        var self = this;
        var args = Array.prototype.slice.call(arguments, 1);
        var fBound = function() {
            //获取函数的参数
            var bindArgs = Array.prototype.slice.call(arguments);
            // 返回函数的执行结果
            // 判断函数是作为构造函数还是普通函数
            // 构造函数this instanceof fNOP返回true,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值。
            // 当作为普通函数时,this 指向 window 此时结果为 false,将绑定函数的 this 指向 context
            return self.apply(this instanceof fNOP ? this: context, args.concat(bindArgs));
        }
        // 创建空函数
        var fNOP = function() {};
        // fNOP函数的prototype为绑定函数的prototype
        fNOP.prototype = this.prototype;
        // 返回函数的prototype等于fNOP函数的实例实现继承
        fBound.prototype = new fNOP();
        // 以上三句相当于Object.create(this.prototype)
        return fBound;
    }
    
    var func = foo.es3Bind({a: 1}, '1st'); 
    func('2nd'); // 1 100 1st 2nd 
    func.call({a: 2}, '3rd'); // 1 100 1st 3rd 
    new func('4th'); //undefined 100 1st 4th 
    

    es6实现

    Function.prototype.es6Bind = function(context, ...rest) { 
        if (typeof this !== 'function') 
            throw new TypeError('invalid invoked!'); 
        var self = this; 
        return function F(...args) { 
            if (this instanceof F) {
                return new self(...rest, ...args)  
            } else {
                return self.apply(context, rest.concat(args))  
            }
        }      
    } 
    var func = foo.es3Bind({a: 1}, '1st'); 
    func('2nd'); // 1 100 1st 2nd 
    func.call({a: 2}, '3rd'); // 1 100 1st 3rd new func('4th'); //undefined 100 1st 4th 
    

    面试问题:

    function fn1(){
       console.log(1);
    }
    function fn2(){
        console.log(2);
    }
    
    fn1.call(fn2);     //输出 1
     
    fn1.call.call(fn2);  //输出 2
    

    fn1.call(fn2), 只是改变了fn1内部this的指向,不是指向调用它的上下文环境也就是 window ,而是指向了fn2,但是不影响 fn1 代码的执行,输出 1

    fn1.call.call(fn2)要注意,其实是分两段来解读
    一 是 call(),后面加括号才代表是call函数,不然就是对象,也就是说fn1.call对象
    fn1.call会通过原型链找到最终对象,本质上是Function.prototype.call

    对于fn1.call.call(fn2)

    首先,调用call函数时,也就是 fn1.call.call(fn2) 的加粗部分,先将 fn2 作为临时的 context 对象。然后将 fn1.call 这个函数对象作为 实际执行函数属性: context.fn = fn1.call
    注意: fn1.call会通过原型链找到最终对象,其本质为 Function.prototype.call; 没有其他参数,直接执行 fn1.call() 函数,即 context.fn(); 此时函数的本质还是 Function.prototype.call 函数对象。 不过执行这个函数的环境是在 Function.prototype.call() 中, 只不过是第一次调用 call() 函数。 第一次调用 call() 函数将 this 关键字指向了 fn2,故而 在 fn1.call.call(fn2)加粗部分的 函数中执行的 call函数执行过程中的 this指向的是 fn2;传入的参数为空,故而 新的 call()函数对象 的this关键字 被替换为window; 而执行 this()时,就是执行 fn2();不涉及 this操作。故最终输出2

    相关文章

      网友评论

          本文标题:call,apply,bind实现以及原理

          本文链接:https://www.haomeiwen.com/subject/qxnndctx.html