深入理解apply,call,bind及源碼實現

對於前面2個 apply,call大家應該非常熟悉了,都可以改變this指向,都可以傳參數,但是bind的話很多人可能覺得和它們沒有什麼區別,估計用bind也用的少,下面我來一一分析下各自的實現原理:

1.call方法

1.第一個參數是this指向的對象。
2.使用的單個參數進行傳遞。
3.用於確定了函數的形參有多少個的時候用。

舉個例子:

       var name = '李四'
        var b = {name:'張三'};
        function a(n){
            console.log(this.name+n+'歲了')
        }
        a.call(b,'18');//'張三18歲了'
        a.call(null,'18');//'李四18歲了'

我們可以這樣理解:把a方法放到b裏面,然後我在b的環境下執行a(如果b沒有值,那就相當再window環境下執行a,this是指向window的),相當b.a(18)或者a(18),然後我再b裏面刪除a方法。知道原理了,那麼我們按照上面的例子來自己封裝下call方法:

    Function.prototype.call = function (obj) {
            // 當call的第一個參數沒有或者是null的時候,this的指向是window
            var obj= obj || window;
            // 把a方法放進裏面
            obj.fn = this;
            // 用於存儲call後面的參數
            var args = [];
            var len = arguments.length;
            // 這裏是爲了將一個函數的參數傳入到另外一個函數執行
            for (var i = 1; i < len; i++) {
                args.push('arguments[' + i + ']');
            };
            // 在eval的環境下 args數組會變成一個一個參數字符串(默認是會調用Array.toString())
            var result = eval('obj.fn(' + args + ')');
            // 刪除b裏面的a方法
            delete obj.fn;
            // 因爲函數可能有返回值,所以把結果也返回出去給他們
            return result;
        };
2.apply方法(和call沒啥區別)

1.第一個參數是this指向的對象。
2.使用的參數是數組進行傳遞。
3.用於確定了函數的形參的個數不確定的情況下使用。
原理我就不累贅了,和call沒有什麼區別,只有傳遞的形參不用,apply傳遞的一定要是是數組,那麼我們就可以在傳參的進行判斷下。我們來模擬下代碼的實現:

   Function.prototype.apply = function (obj, arr) {
      // 當apply的第一個參數是null的時候,this的默認指向是window
      var obj = obj || window;
      // 把該函數掛載到對象上
      obj.fn = this;
      //判斷有沒有傳值
      if (!arr) {
          result = obj.fn();
      } else {
          //判斷傳入的是不是數組,不是的話拋出異常
          if (!Array.isArray(arr)) {
              throw new Error('上傳的必須是數組');
          };
          var args = [];
          // 用於存儲apply後面的參數
          for (var i = 0; i < arr.length; i++) {
              args.push('arr[' + i + ']');
          };
          // 這裏的args默認是會調用Array.toString()方法的
          var result = eval('obj.fn(' + args + ')');
      }
      // 刪除函數
      delete obj.fn;
      // 因爲函數可能有放回值,所以把結果也返回出去給他們
      return result;
        }
2.bind方法

這裏重要講下bind,bind和call和apply還是有點區別的。
1.bind會創建一個函數(稱爲綁定函數),創建一個新函數而不執行,這是bind和call與apply方法的一個重要差別,call和apply這兩個方法都會立即執行函數,返回的是函數執行後的結果。而bind函數只創建一個新函數而不執行。
2.函數的柯里化(使用一個閉包返回一個函數),柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
下面我們來慢慢模擬一下bind的實現(首選返回一個函數):

  Function.prototype.bind = function(obj){
        var self =this;
        //第一個參數爲它運行時的this,應該取第二個之後的參數
        var args =Array.prototype.slice.call(arguments,1);
        //返回一個新函數閉包
        var newFn = function(){
            self.apply(obj,args);
        };
        return newFn;
    };
    //例子:
    var name = '李四'
    var b = {
        name: '張三',
    };

    function a(age) {
        console.log(this.name + age + '歲了')
    }
    var p =a.bind(b,'18');
    p();//"張三18歲了"

和預想的一樣,bind得底部實現還是apply方法,很穩!!,有沒有發現啥問題,我只在返回的新函數裏面傳了第2個參數,調用的時候都沒傳參數,這是個問題!然後補充一下:

 Function.prototype.bind = function(obj){
        var self =this;
        //第一個參數爲它運行時的this,應該取第二個之後的參數
        var args =Array.prototype.slice.call(arguments,1);
        //返回一個新函數閉包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        return newFn;
    };
    //例子改下參數
    
    var name = '李四'
    var b = {
        name: '張三',
    };

    function a(age,sex) {
        console.log(this.name + age + '歲了,性別'+sex);
    }
    var p =a.bind(b,'18');
    p('男');//張三18歲了,性別男

和預想的一樣!可以接受2個參數,實現了函數的柯里化。是不是還覺得哪裏有問題?對的,函數才能使用bind!這裏需要判斷一下,再改下代碼:

   Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函數纔可以調用bind');
        };
        //第一個參數爲它運行時的this,應該取第二個之後的參數
        var args =Array.prototype.slice.call(arguments,1);
        //返回一個新函數閉包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        return newFn;
    };

好了,到這裏的話,感覺好像代碼實現的差不多,但是這裏有個重點,當我們把創建出來的新函數當做構造函數的時候,官方文檔有這麼一句話:“說明綁定過後的函數被new實例化之後,需要繼承原函數的原型鏈方法,且綁定過程中提供的this被忽略(繼承原函數的this對象),但是參數還是會使用。” 看上去不是很明白,其實主要是說,當new完之後 新創建出來的實例要繼承原函數的原型,並且this指向新創建出來的實例對象(綁定的this將失效)。首先實現下原理繼承:

    Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函數纔可以調用bind');
        };
        //第一個參數爲它運行時的this,應該取第二個之後的參數
        var args =Array.prototype.slice.call(arguments,1);
        //返回一個新函數閉包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        //繼承原函數的原型
        newFn.prototype = this.prototype;
        return newFn;
    };

如果這樣寫的話是有問題的,如果我改變了新創建出來的函數的原型同樣也修改了原函數的原型,所以這裏需要寫一個過渡函數(如果不懂繼承的同學,可以點擊這裏)。

   Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函數纔可以調用bind');
        };
        //第一個參數爲它運行時的this,應該取第二個之後的參數
        var args =Array.prototype.slice.call(arguments,1);
        //返回一個新函數閉包
        var newFn = function(){
            self.apply(obj,args.concat(Array.prototype.slice.call(arguments)));
        };
        //過渡函數
        var f = function(){};
        f.prototype = this.prototype;
        newFn.prototype = new f();
        return newFn;
    };

接下來需要判斷this的值,判斷到底是不是new出來的實例,通過instanceof 判斷。簡單解釋下 instanceof ,假如a instanceof b,意思是a是不是b的實例,或者這樣說:b的prototype是否在a的原型鏈上。再改下代碼:

    Function.prototype.bind = function(obj){
        var self =this;
        if(typeof this !=='function'){
            throw new Error('只有函數纔可以調用bind');
        };
        //第一個參數爲它運行時的this,應該取第二個之後的參數
        var args =Array.prototype.slice.call(arguments,1);
        //返回一個新函數閉包
        var newFn = function(){
            self.apply(this instanceof f ?  this : obj ,args.concat(Array.prototype.slice.call(arguments)));
        };
        //過渡函數
        var f = function(){};
        f.prototype = this.prototype;
        newFn.prototype = new f();
        return newFn;
    };

到這裏代碼就修改的差不多了,最後拿這段代碼“this instanceof f ? this : obj ”分析下,這裏的意思是如果是new出來的,this指向它實例出來的對象,如果不是,那麼this是指向綁定的obj對象。如果obj沒有值,不傳值或者是null,那麼這個this是指向window的,有些同學代碼在這裏做了下個判斷 “this instanceof f ? this : obj || this”,這樣寫的話,this不一定都指向window的,比如看下面這個例子:

    var name = '張三';
    var foo = {
        name:'李四',
        fn: fn.bind(null) //如果在這裏執行,this是指向widonw的
    };

    function fn() {
        console.log(this.name);
    }
	//foo這裏調用fn,所以在這裏把this的指向改變了,this===foo;
    foo.fn() //'張三'

所以說這個判斷是不對的。好了,到這裏就差不多已經說完了(如果有不對之處,歡迎指正,不勝感激!!!),本來還想說下bind的幾個難點,怕寫的太多都看不下去了,還是下一篇再說吧,歡樂的時光總是過得特別快,又到時候和大家講拜拜!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章