把玩Javascript中的bind

前言

今天閒着無聊隨便逛了逛MDN,忽而看到一個方法Function.prototype.bind(),忽然發現除了使用這個方法以外都沒有仔細琢磨過這個方法。因而乎,找到了kill time的事情-寫博客。javascript

基礎知識簡介

隨便看看資料發現這玩意其實不簡單,理解起來須要很多基礎知識,在這裏羅列一些,也算是一個總結和複習。java

函數

下面這段話來自《JavaScript語言精粹》,名副其實地描述了函數的精髓。git

調用一個函數會暫停當前函數的執行,傳遞控制權和參數給新函數。除了聲明時定義的形式參數,每一個函數還接收兩個附加的參數:this和arguments。參數this在面向對象編程中很是重要,他的值取決於調用的模式。在JavaScript裏面一共有四種調用模式:方法調用模式、函數調用模式、構造器調用模式和apply調用模式。這些模式在如何初始化關鍵參數this上面存在差別。github

方法調用模式

當一個函數被保存爲對象的一個屬性時,咱們稱它爲一個方法。當一個方法被調用的時候,this被綁定到該對象。編程

var info = {
    name: 'yuanzm',
    sayName: function() {
        console.log(this.name);
    }
}
info.sayName();    //yuanzm
函數調用模式

當一個函數並不是爲一個對象的屬性的時候,他就是被當作一個函數來調用的。此模式調用函數的時候,this被綁定到全局對象。這是語言設計上的一個錯誤。假若語言設計正確,this應該是綁定到外部函數的this變量。數組

var name = "yuanzm"
var sayName = function() {
    console.log(this.name);
}
sayName();// yuanzm
構造器調用模式

若是在一個函數前面帶上new來調用,那麼背地裏將會建立一個鏈接到該函數的prototype成員的新對象,同時this會被綁定到新對象上。(JavaScript原型相關知識這裏不作贅述)app

function Info(name) {
    this.name = name;
}
Info.prototype.sayName = function() {
    console.log(this.name);
}
var info = new Info('yuanzm');
info.sayName();//yuanzm
Apply調用模式

根據MDN的定義ide

The apply() method calls a function with a given this value and arguments provided as an array (or an array-like object).函數

var numbers = [5, 6, 2, 3, 7];
var max = Math.max.apply(null, numbers);

類數組

一個類數組被定義爲:ui

  • 具備:指向對象元素的數字索引下標以及 length 屬性告訴咱們對象的元素個數
  • 不具備:諸如 push 、 forEach 以及 indexOf 等數組對象具備的方法

符合上述定義的類數組是長下面這樣子的:

var my_object = {
    '0': 'zero',
    '1': 'one',
    '2': 'two',
    '3': 'three',
    '4': 'four',
    length: 5
};

前面提到的arguments也是類數組。不少時候,處理類數組最省事的方法就是將它轉化成數組。那麼就涉及到一個很是有意思的話題:將類數組轉換成數組。
將類數組轉換成數組很是簡單,調用Array自帶的方法便可:

Array.prototype.slice.call(arguments);

其中call換成apply也是同樣的。
簡單解釋一下,slice方法常規的調用方式爲array.slice(start, end),會對array中的一段作淺複製,首先複製array[start], 一直複製到array[end]。前面提到過apply(或call)會切換一個函數調用的上下文,也就是Array.prototype.slice.call(arguments);手動綁定了須要操做的array爲arguments,因爲arguments和數組同樣具備下標,因此這個方法也是可行的,於是產生了一個新的數組,這個數組具備普通數組的全部方法,同時具備arguments每個下標對應的值。

bind

簡介

前面說了這麼多,主角終於登場了!不過爲了凸顯它的做用,仍是須要拋出一段達不到咱們需求的代碼。

var name = 'yuan'
var info = {
    name: "yuanzm",
    sayName: function() {
        console.log(this.name);
    }
}
var sayName = info.sayName;
// 咱們自己是但願獲得yuanzm的,可是確獲得了yuan這個結果。爲何會獲得這個結果,前面的長篇大論起做用了。
// 若是是採用info.sayName()這種調用方式,符合函數的方法調用模式,函數內部的this是info對象;
// 若是令一個變量sayName爲info.sayName,這個時候再調用函數,就是普通的函數調用模式了,結果天然爲yuan。
sayName();    // yuan

那麼咱們使用bind就是但願最後獲得的結果爲yuanzm

如今咱們能夠好好介紹bind了。根據MDN的定義:

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

翻譯過來就是,bind()方法會建立一個新函數,稱爲綁定函數.當調用這個綁定函數時,綁定函數會以建立它時傳入 bind()方法的第一個參數做爲 this,傳入 bind() 方法的第二個以及之後的參數加上綁定函數運行時自己的參數按照順序做爲原函數的參數來調用原函數。
他的語法是:

fun.bind(thisArg[, arg1[, arg2[, ...]]]);

解決問題

有了bind,上述問題咱們就可以獲得想要的結果了:

var name = 'yuan'
var info = {
    name: "yuanzm",
    sayName: function() {
        console.log(this.name);
    }
}
var sayName = info.sayName.bind(info);
sayName();    // yuanzm

bind的用法在MDN上面描述得很詳細,本文的目的也不是爲了照搬MDN,因此這裏有關bind使用的部分就到這兒,接下來咱們去看看Javascript庫中bind的實現

Prototype中的bind

好久以前Prototype的bind寫法是下面這樣的(註釋爲本人所加):

Function.prototype.bind = function(){ 
    // bind做爲Function的prototype屬性,每個函數都具備bind方法,其中的this指向調用該方法的函數(也即一個函數對象實例)
    var fn = this;
    // 前面說過,這個方法是爲了將類數組轉換成數組
    var args = Array.prototype.slice.call(arguments);
    // 數組的shift方法會移除數組的第一個元素,在bind方法的參數裏面,第一個參數就是須要綁定this的對象
    var object = args.shift();
    return function(){ 
        return fn.apply(object,
        // 如今的args是移除了最初參數中第一個參數後的數組,也就是新函數的預設的初始參數。
        // array.concat(items...)方法產生一個新數組,它包含一份array的淺複製,並把一個或者多個參數item附加在其後。
        // 若是參數item是一個數組,那麼它的元素會被分別添加。
        // 因而這一行代碼產生了一個參數數組,這個數組由預設的初始參數(若是存在)和新傳遞的參數組成。
        args.concat(Array.prototype.slice.call(arguments))); 
    }; 
};

在最新的版本1.7.2(2015.06.23查閱官網)中https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/function.js#L115,bind是下面這樣子的:

function bind(context) {
    if (arguments.length < 2 && Object.isUndefined(arguments[0]))
        return this;

    if (!Object.isFunction(this))
        throw new TypeError("The object is not callable.");
    
    var nop = function() {};
    var __method = this, args = slice.call(arguments, 1);

    var bound = function() {
        var a = merge(args, arguments);
        // Ignore the supplied context when the bound function is called with
        // the "new" keyword.
        var c = this instanceof bound ? this : context;
        return __method.apply(c, a);
    };
    
    nop.prototype   = this.prototype;
    bound.prototype = new nop();

    return bound;
}

可見,除了加了一些異常狀況判斷,核心代碼和以前並沒有大差異。

結語

總得來講apply、call和bind都是爲了手動綁定this對象,目的上沒有什麼區別。可是bind和另外二者的明顯的區別是,bind會產生一個新的函數,這個函數還能夠有預設的參數,這在不少時候會比apply和call更加好用。合理利用apply、call和bind會使得javaScript代碼更加優雅。

參考資料

Function.prototype.apply()
JavaScript 的怪癖 8:「類數組對象」
how does Array.prototype.slice.call() work?
JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals

相關文章
相關標籤/搜索