1. 程式人生 > >手動實現call() , apply() , bind()

手動實現call() , apply() , bind()

這篇文章簡單的介紹了實現call() , apply() , bind()的思路

實現call(obj,arg,arg....)

將目標函式的this指向傳入的第一個物件,引數為不定長,且立即執行

實現思路

  • 改變this指向:可以將目標函式作為這個物件的屬性
  • 利用arguments類陣列物件實現引數不定長
  • 不能增加物件的屬性,所以在結尾需要delete

那麼怎麼將不定長的引數傳遞給函式呢?有三種辦法:eval,apply,ES6的解構語法

eval('obj.fn('+args+'));
obj.fn.apply(obj,args);
obj.fn(...args);
Function.prototype.mycall = function(obj){
         var args = Array.prototype.slice.apply(arguments,[1]);
         obj.fn = this;
         obj.fn(...args);//es6的解構語法,也可以使用obj.fn.apply(obj,args);
         delete  obj.fn;
}

使用eval時,eval會將一個字串解析為變數,所以如果傳入的引數為字串,會報 xxx is not defined,解決辦法如下:

Function.prototype.mycall = function(obj){
        obj = obj||window;
         var args = [];
        for(var i = 1 ; i < arguments.length; i++) {
                  args.push('arguments[' + i + ']');
         }

         obj.fn = this;
         eval('obj.fn('+args+'));
         delete  obj.fn;
}

實現apply()

與call()只有一個區別,apply第二個引數為陣列

Function.prototype.myapply = function(obj,arr){
        obj.fn = this;
         if(!arr){
             obj.fn();
        }else{
             var args = []; 
       for(var i = 0; i < arr.length; i++) {
             args.push('arr[' + i + ']');
         }
         
         eval('obj.fn('+args+')');
 }
       
         delete  obj.fn;

}

實現bind()

返回一個與被調函式具有相同函式體的新函式,且這個新函式也能使用new操作符。

實現思路

  1. 返回一個新函式,可以使用閉包
  2. dind()傳入的引數長度不定,使用函式內建的arguments物件陣列,可利用Array.prototype.slice.call(arguments,1 )將其轉化為陣列
  3. 返回的新函式中,使用apply改變被呼叫函式的this指向,將arguments轉化成的陣列作為apply的第二個引數
  4. 因為返回的新函式也可以使用new操作符,所以在新函式內部需要判斷是否使用了new操作符(為什麼需要判斷,後面會講解到),如果使用則將apply的第一個引數設定為新建立的物件,如果沒有則設定為在呼叫bind()時所傳入的物件(不傳的話預設為window)。

需要注意的是怎麼去判斷是否使用了new操作符呢?在解決這個問題之前,我們先看使用new操作符時具體幹了些什麼,下面是new操作符的簡單實現過程:

//簡潔版的new操作符實現過程

function newFunc(constructor){
      //第一步:建立一個空物件obj 
        var obj = {};
       //第二步:將建構函式 constructor的原型物件賦給obj的原型
        obj.__proto__ = constructor.prototype;
      //第三步:將建構函式 constructor中的this指向obj,並立即執行建構函式內部的操作
        constructor.apply(obj);
      //第四步:返回這個物件
        return obj;
}

new操作符的一個過程相當於繼承,新建立的建構函式的例項可以訪問建構函式的原型鏈

在new操作符實現過程的第三步中,會將建構函式 constructor中的this指向obj,並立即執行建構函式內部的操作,那麼,當在執行函式內部的操作時,如果不進行判斷是否使用了new,就會導致 " 將建構函式 constructor中的this指向obj " 這一過程失效,具體原因請看下面的模仿實現bind()的程式碼:

Function.prototype.testBind = function(object){

          var that = this,
              args = Array.prototype.slice.call(arguments,1),
              bound = function(){
                    return that.apply(this instanceof fNOP?this:object||window,
                         args.concat.apply(Array.from(arguments)));
          };

        //建立一箇中轉函式fNOP,讓bound間接繼承目標函式的原型
          var fNOP =  function(){};
          fNOP.prototype= that.prototype;   
          bound.prototype= new fNOP();  
  
          return bound;
}

重點:建立一箇中轉函式fNOP,讓bound間接繼承目標函式的原型,一開始我想為什麼不直接讓 bound.prototype = that.prototype ,後來才發現直接賦值後,bound.prototype和that.prototype指向同一塊內容,如果改變bound.prototype就會直接影響that.prototype,使用一箇中轉函式, bound.prototype= new fNOP()將bound.prototype的__poro__指向fNOP.prototype,然後fNOP.prototype = that.prototype,所以此時改變bound.prototype並不會影響that.prototype。

另外:上面實現bind()的程式碼中使用apply的地方可以換成原生實現的程式碼