使用另一種方式實現js中Function的調用(call/apply/bind)


在JavaScript中函數的調用可以有多種方式,但更經典的莫過於call和apply。call跟apply都綁定在函數上,他們兩個的第一個參數意義相同,傳入一個對象,他作為函數的執行環境(實質上是為了改變函數的Execution Context執行上下文),也就是this的指向;而第二個參數兩者只是類型的不同,call傳的是arguments,而apply傳的是array。廢話不多說,先上一個最基礎的例子:

function add(c,d){
  return this.a + this.b + c + d;
}
var o = {
  a: 1,
  b: 2        
}
add.call(o,3,4)          // 10
add.apply(o,[3,4])    // 10

 

再比如我之前看過的Twitter上的關於call和apply的一個面試題:定義一個函數log,傳入任意數量參數,讓它模擬console.log的方法,形如log('hello','world');

function log(){
  console.log.apply(console,arguments);
}

  

當執行這個log函數時,該函數的執行上下文為console對象,arguments為傳入的實參。

然后此題又有需求,如果要給每個log信息加一個(app)前綴,比如 '(app) hello world';

這時我們應該想到一點,那就是我們傳入的實參,也就是arguments並不是一個數組,它實質上只是一個類數組的對象,我們在這里可以用 instanceof 來判斷自定義對象。

instanceof的意思就是看左邊對象的原型鏈上是否有右邊構造器的prototype屬性。由上圖可以看出arguments是一個類數組的對象,並且可以看出arguments沒有array的方法。

所以回到剛才那個題,我們必須讓傳入的實參變為Array類型才可以調用數組的unshift方法(在它的前面加上app)。

function log(){
  var args = Array.prototype.slice.call(arguments);
  args.unshift('(app)');  
  console.log.apply(console,args);        
}

log('hello','world');  // (app) hello world

在OOP面向對象中此方法用的更加多一些,當我們不想為另一個對象創建方法的時候,可以用call調用foo的方法。

function foo () {}
foo.prototype.name = 'james';
foo.prototype.sayHello = function(){
    console.log(this.name);
}
var obj1 = new foo();
obj1.sayHello();   //james
var obj2 = {
    name: 'bond'
}
obj1.sayHello.call(obj2);  //bond

再比如如果要想調用一些不能直接調用的方法,比如Object.prototype.toString(),我們也可以用call,它的本質在於將內部的變量改為包裝對象。

 這個對象原型的方法指向window,也就是this,但是如果把log函數里call指向的對象改為window的話,就會輸出global。因為在執行上下文中的全局對象為Global Context,

比方說全局的Math,String,window都存在於[[global]]變量對象中。

window對象依附於Global全局變量對象,雖然權威上來說在NodeJS里全局是Global對象,但是在全局執行上下文中window指向global,請看下圖。

function log(){
console.log(this===window); // true
return Object.prototype.toString.call(this); } log.call(5) // [Object Number] log.call(true) // [Object Boolean]

對於js中的bind方法,他跟apply和call基本一樣,里面傳遞的參數也是改變this指向的,比方下面一個關於執行上下文的代碼,通過bind改變this的指向。

var User = {
  count: 1,
  getCount: function(){
    return this.count;
  }
}
var func = User.getCount;
console.log(func());  //undefined
var func1 = User.getCount.bind(User) 
console.log(func1());  // 1
 

上面的答案是undefined,因為func是在全局作用域window中,window里面沒有count屬性,所以我們為了讓this指向User對象,我們便想到了使用bind。但是bind是es5才有的方法,不兼容老版本瀏覽器,那如何解決這個問題呢?下面是我從火狐的MDN上拿下來的bind模擬。

if(!Function.prototype.bind){
    Function.prototype.bind = function(oThis){
        if(typeof this !== 'function'){
            throw new TypeError('What is trying to be bound is not callable');
        }
        var args = Array.prototype.slice.call(arguments,1),
            fTobind = this,  //指向調用bind的函數
            fNOP = function(){},  //創建一個空函數,為了下面的繼承
            fBound = function(){
                return fTobind.apply(this instanceof fNOP ? this : oThis,  //改變this的指向
                    args.concat(Array.prototype.slice.call(arguments)));  //將通過bind傳遞的參數與調用時傳的參數合並
            };
            fNOP.prototype = this.prototype;  //將目標函數的原型傳遞到新函數中
            fBound.prototype = new fNOP;  //這兩條相當於Object.create的作用
            return fBound;
    }
}

我們通過下面這個例子來分析它吧:

function foo(){
  this.b = 100;
  return this.a;
};
var func = foo.bind({a:1});
func() // 1
new func()  // {b:100}

fBound指向新函數func,因為foo調用的bind方法,所以fToBind指向目標函數foo,在func()里面this指向傳進去的對象,也就是bind模擬里面的oThis,通過this instanceof fNOP來判斷this的指向,因為func()是window調用下的,所以this指向window,所以this instanceof fNOP 返回false,所以oThis也就是傳入的{a:1}為當前的執行上下文,所以彈出1。但如果是對象調用(  new Func()  )的話,this instanceof fNOP中的this會指向一個空對象,空對象的原型會指向構造器的prototype屬性,即func的prototype屬性,這里注意一點,因為foo 中 return的不是對象,所以忽略return。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM