要理解call,apply和bind,那得先知道JavaScript裏的this指針。JavaScript裏任何函數的執行都有一個上下文(context),也就是JavaScript裏常常說到的this指針,例如:javascript
var obj = { a: 3, foo: function() { console.log(this.a); } } obj.foo(); // 執行結果:打印出a的值3
這裏foo的執行context就是obj,即this指針的指向。固然函數也能夠直接定義,不加任何容器:java
var bar = function() { console.log(this.b); } bar(); // 等價於global.bar()
若是咱們調用bar(),那就至關於調用global.bar(),也就是調用在了一個全局的object上。在不一樣的環境裏這個全局object不同,在瀏覽器裏是window,在node裏則是global,這都是JavaScript執行環境預設的。實際上咱們定義的bar函數自己就是定義在了這個global上,即等價於:node
global { bar: function() { console.log(this.b) } }
因此直接執行bar的時候,context固然是global了。這和上面的obj和foo的例子是同樣的。編程
其實context的概念對於任何面向對象的語言都是同樣的,好比Java和C++,它們的Class的成員函數在調用的時候第一個參數永遠是一個隱式的this指針,而後纔是真正的參數。那個this指針必須指向一個具體的這個Class的實例object。數組
然而Java和C++這樣的語言和JavaScript不同的地方就在於,它們對函數的調用更嚴格。它們有明確的Class和成員函數的定義,只有Class的實例object才能調用這個Class的成員函數。而JavaScript做爲一門函數式編程語言則比較自由,函數的調用能夠任意指定上下文context。例如上面的foo和bar,咱們能夠並不調用在obj和global上,而是自行指定,這就須要用到call和apply。瀏覽器
JavaScript裏用call和apply來指定函數調用的context,即this指針的指向。call和apply都是定義在Function的prototype上,因此任何函數都繼承了它們,能直接使用,例如咱們定義函數func:閉包
function func(v1, v2) { console.log(this.a); console.log(v1); console.log(v2); }
而後調用func:app
var x = { a: 5 } func.call(x,7, 8); // 執行結果: // 打印出x.a的值5 // 打印出v1的值7 // 打印出v2的值8
這裏咱們用call來調用func函數,call的第一個參數便是要綁定的上下文context,因此函數裏的this.a就成了x.a,而x後面傳入的參數就是func函數自己的參數v1和v2。編程語言
apply的用法也是類似的,只不過apply把x後面的參數組織成一個數組:函數式編程
func.apply(x,[7, 8]);
這裏正由於咱們用call和apply指定了func調用的context爲x,所以它才能夠打印出this.a的值,由於x裏定義了a。若是直接調用func(),它就會調用在global上,此時a並無定義,則會打印出undefined。
有時候咱們想指定函數調用的context,但並不想當即調用,而是做爲callback傳給別人,這在JavaScript裏司空見慣,由於處處都會有須要給監聽事件綁定callbak函數,這時候call和apply就不能用了,須要用到bind。
var funcBound = func.bind(x); funcBound(7, 8); // 等價於: // func.call(x, 7, 8)
一樣是上面的func函數,正如bind的字面意思,此次咱們將x綁定到了func上,獲得一個新的函數funcBound,此時funcBound的上下文就已經被指定爲了x,而後它再直接調用參數7和8,那麼效果就至關於以前的func.call(x, 7, 8)了。
bind也能夠傳入多個參數,此時第一個參數綁定爲this,後面的參數則綁定爲正常參數,例如:
var funcBound = func.bind(x, 7); funcBound(8); // 等價於: // func.call(x, 7, 8)
與bind做爲比較,在JavaScript裏爲了控制函數的調用者,一種很方便的作法就是用閉包(Closure),由於閉包會保存任何外部的被使用到的變量的reference。不過閉包並無改變函數執行的上下文,也就是說this指針並無改變,它能改變的只是函數體裏某些具體的變量的指向,這其實是一種強耦合,須要你在寫函數的時候自己就用到外部定義好的一些變量,這是一種不可擴展的函數定義方式。而若是是一個很通用的函數,要想實現this指針的任意綁定,像call和apply那樣,則必須用bind。
bind在C++ 11裏也有相似的用法,也是C++實現callback的一種主要方式,只不過就像以前說的,C++對類型的檢查更嚴格,綁定的不論是this仍是其它參數都必須嚴格符合這個函數定義的形參的類型。而對於JavaScript,你能夠隨意給任何函數綁定任何參數,並且這樣的綁定常常是隱式的,這也是形成JavaScript裏this指針很容易混淆的緣由,由於它的函數調用比較自由,並且每每不少時候底層調用的細節咱們是不知道的。
其實咱們很容易用call和apply實現一個簡單的bind,來加深對bind的理解。這裏假定你對JavaScript的Function,Array以及閉包有必定了解。
function myBind() { // bind的調用者,即本來的函數。 var func = this; // 獲取bind的參數列表。 var args = Array.prototype.slice.call(arguments); if (args.length === 0) { // 若是未傳入任何參數,那就直接返回本來的函數。 return func; } // 第一個參數是執行上下文context。 var context = args[0]; // 後面的參數是正常的調用傳參,從第二個參數開始。 var boundArgs = args.slice(1); // 定義bound後的函數,這裏用到了Closure。 var boundFunc = function() { // 獲取實際調用時傳入的參數,而且拼接在以前的boundArgs後面,成爲真正的完整參數列表。 var args = Array.prototype.slice.call(arguments); var allArgs = boundArgs.concat(args); // 這裏用apply來調用原始的函數func,執行上下文指向context,並傳入完整參數列表。 return func.apply(context, allArgs); } // 返回bound函數。 return boundFunc; }
這樣就完成了一個簡單版本的bind,它已經能夠實現bind的基本功能,即綁定context和參數列表。具體的原理都在每一步的註釋裏寫出來了,我就不作過多解釋了。若是把它加到Function的prototype裏,那一個正常的函數例如foo就可使用foo.myBind(...)來實現bind相似的功能。
-------
有一個有趣的問題,就是若是對一個函數進行屢次bind會發生什麼事情?會不會context被不斷更新?答案是不會,只有第一次bind的context會做爲最後調用的實際的this指針。關於這一點,只要對照上面的實現就能理解,無論bind多少次,最裏層的apply永遠只做用在第一次的傳入的context,也就是說原始函數func的調用對象只能是第一次的那個context。
一樣,對於一個bind後的函數使用call或者apply,也沒法改變它的執行context,原理和上面是同樣的。在實際編程中也不會有人這樣寫,可是初學者有時候可能會不當心犯下這樣的錯誤。實際上我就是初學者。。。也是在這裏記錄一下我學JavaScript過程當中的一些問題和想法。