ES5新增函數之二: Function.prototype.bind();

在上一篇文章裏我們分析了ES5對幾個常用類新增的函數,今天就重點來講解一下Function中的bind函數。

簡單來說,bind函數用於將當前函數和指定對象綁定,返回一個新的函數,當新函數被調用時,代碼會在指定對象的上下文中執行。

這就涉及到JavaScript程序執行上下文Context的知識了,在JavaScript中函數內部如果存在與Context有關的代碼,如果我們在調用之前改變其Context,那麼執行結果就不同,這一點我們可以用一個最基本的例子來說明:

var name = 'Global';

var student = {
  name: 'John'
};

var person = {
  name: 'Scott',
  getName: function() {
    return this.name;
  }
};

console.log(person.getName());      // Scott

var getName = person.getName;
console.log(getName());             // Global
console.log(getName.call(student)); // John

如上所示,我們在全局聲明一個name變量,然後聲明student和person對象,分別都有name屬性,其中person包含一個getName函數,用於返回所在對象的name屬性。第一步我們直接調用person的getName函數,返回Scott;然後我們先去到person的getName函數引用,之後直接調用,注意,這次調用跟第一步是不同的,它的執行環境是全局,所以函數內部的this.name會指向我們全局聲明的name,所以結果會返回Global;最後我們使用call方法將getName函數在student對象的上下文中執行,結果會返回student的name屬性,即John。

針對這個問題,我們可以使用bind函數將getName綁定person對象,返回一個帶有固定作用域的新函數,以後不管在哪調用這個新函數,都不用擔心作用域的問題:

var getName = person.getName.bind(person);  //使用bind函數綁定person對象
console.log(getName());               //Scott
console.log(getName.call(student));   //Scott

注意最後一個調用雖然使用了call函數,但因爲getName使用bind綁定了person對象,所以不會再被call函數更改作用域了,打印結果仍然會是person對象的name。

另外一個例子是調用setTimeout或setInterval,當我們在函數內部調用setTimeout時需要特別小心,因爲setTimeout函數是在全局Context中執行的,我們來看下面這段代碼及運行的結果:

var name = 'John';

var person = {
  name: 'Scott',
  showMyName: function() {
    setTimeout(this.printName, 1000);
  },
  printName: function() {
    console.log('in person object, there is a name: ', this.name);
  }
};

person.showMyName();   //in person object, there is a name: John


我們本希望在調用showMyName後延時1秒然後打印person的name屬性,可是結果並不是如我們期望的那樣,而是打印出了全局變量的值John,這是因爲setTimeout把person的printName函數放到了全局執行了,那printName裏面的this自然也指向了全局中的name變量。對於這個問題我們同樣可以使用bind函數將作用域綁定爲person對象,進而將printName的this總是指向person對象:

setTimeout(this.printName.bind(this), 1000);   //使用bind綁定當前對象person

這樣一來結果就會如我們期望打印出person的name屬性,即Scott了:


上面介紹了這麼多,想必大家已經對bind的作用有所瞭解了,下面來詳細介紹一下bind函數的簽名:

func.bind(context, [arg1, [arg2, [...]]]);

函數的第一個參數是執行環境的上下文對象,後面的參數列表是預設參數,我們知道,調用bind函數會返回一個新函數,當這個新函數調用時,上面參數列表裏的預設函數也會附帶作爲實參傳入。在上面兩個例子中我們使用bind函數時並沒有預設參數,下面我們舉個例子來說明一下如何使用預設參數:

var listDrinks = function() {
  var drinks = Array.prototype.slice.call(arguments);
  console.log(drinks.join(', '));
};

listDrinks('tea', 'coffee');  //tea, coffee

var listDrinksOfRestaurant = listDrinks.bind(null, 'this restaurant serves: water');

listDrinksOfRestaurant('tea', 'coffee', 'milk');  //this restaurant serves: water, tea, coffee, milk

打印結果如下:


上面的例子很簡單,listDrinks函數用於列出所有的飲料,然後我們使用bind函數返回了一個新的函數listDrinksOfRestaurant,它專門用於列出一個餐館提供的所有飲料,因爲每個餐館都得提供水,所以這是一個基本的要求,我們就把它作爲預設參數在bind時傳遞進去,這樣不管listDrinksOfRestaurant如何調用,water會一直存在的。另外,我們注意到上面使用bind函數時第一個參數是null,這是因爲我們的listDrinks函數內部沒有任何與上下文有關的代碼,所以不需要傳遞上下文對象即可。

bind函數在ES5規範中是一個很重要的特性,給開發者帶來了極大的便利,但它在低版本的瀏覽器中是無法使用的,所以我們很有必要實現我們自己的bind函數:

if (!Function.prototpe.bind) {
    Function.prototype.bind = function(context) {
        var self = this, 
        	args = Array.prototype.slice.call(arguments);
            
        return function() {
            return self.apply(context, args.slice(1).concat(arguments));    
        }
    };
}
以上就是bind函數的全部內容,因爲它涉及到一些作用域的概念及運行機制,所以需要細細體會,瞭解其中的奧妙之處,謝謝大家。

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