js中bind、call、apply函數的用法

最近一直在用 js 寫遊戲服務器,我也接觸 js 時間不長,大學的時候用 js 做過一個 H3C 的 web 的項目,然後在騰訊實習的時候用 js 寫過一些奇怪的程序,自己也用 js 寫過幾個的網站。但真正大規模的使用 js 這還是第一次。我也是初生牛犢不怕虎,這次服務器居然拋棄 C++lua 的正統搭配,而嘗試用 nodejs 來寫遊戲服務器,折騰的自己要死要活的我也是醉了。

在給我們項目組的其他程序介紹 js 的時候,我準備了很多的內容,但看起來效果不大,果然光講還是不行的,必須動手。前幾天有人問我關於代碼裏 call() 函數的用法,我讓他去看書,這裏推薦用 js 寫服務器的程序猿看 《javascript編程精粹》 這本書,crockford大神果然不是蓋的。之後我在 segmentfault 上又看到了類似的問題,那邊解答之後乾脆這裏記一筆。


首先,關於 js 定義類或對象的方法,請參看 w3school 的這裏,寫的非常詳細和清晰,我不再贅言了。


爲了介紹 bindcallapply 這三個函數的用法,不得不介紹 js 裏函數的一些設定。關於這部分推薦通讀 《javascript編程精粹》 的第四章,這裏我所說的在書裏都能找到。

關於這三個函數的詳細介紹,可以參看 MDN 的文檔:bindcallapply


下面開始搬磚,修改自我之前在 segmentfault 上的答案:

js 裏函數調用有 4 種模式:方法調用正常函數調用構造器函數調用apply/call 調用
同時,無論哪種函數調用除了你聲明時定義的形參外,還會自動添加 2 個形參,分別是 thisarguments
arguments 不涉及到上述 3 個函數,所以這裏只談 thisthis 的值,在上面 4 中調用模式下,分別會綁定不同的值。分別來說一說:
方法調用
這個很好理解,函數是一個對象的屬性,比如

var a = {    
    v : 0,    
    f : function(xx) {                
        this.v = xx;    
    }
}
a.f(5);

這個時候,上面函數裏的 this 就綁定的是這個對象 a。所以 this.v 可以取到對象 a 的屬性 v

正常函數調用:
依然看代碼

function f(xx) {        
    this.x = xx;
}
f(5);

這個時候,函數 f 裏的 this 綁定的是全局對象,如果是在瀏覽器運行的解釋器中,一般來說是 window 對象。所以這裏 this.x 訪問的其實是 window.x ,當然,如果 window 沒有 x 屬性,那麼你這麼一寫,按照 js 的坑爹語法,就是給 window 對象添加了一個 x 屬性,同時賦值。

構造器函數調用
構造函數一直是我認爲是 js 裏最坑爹的部分,因爲它和 js 最初設計的基於原型的面向對象實現方式格格不入,就好像是特意爲了迎合大家已經被其他基於類的面相對象實現給慣壞了的習慣。
如果你在一個函數前面帶上 new 關鍵字來調用,那麼 js 會創建一個 prototype 屬性是此函數的一個新對象,同時在調用這個函數的時候,把 this 綁定到這個新對象上。當然 new 關鍵字也會改變 return 語句的行爲,不過這裏就不談了。看代碼

function a(xx) {        
    this.m = xx;
}
var b = new a(5);

上面這個函數和正常調用的函數寫法上沒什麼區別,只不過在調用的時候函數名前面加了關鍵字 new 罷了,這麼一來,this 綁定的就不再是前面講到的全局對象了,而是這裏說的創建的新對象,所以說這種方式其實很危險,因爲光看函數,你不會知道這個函數到底是準備拿來當構造函數用的,還是一般函數用的。所以我們可以看到,在 jslint 裏,它會要求你寫的所有構造函數,也就是一旦它發現你用了 new 關鍵字,那麼後面那個函數的首字母必須大寫,這樣通過函數首字母大寫的方式來區分,我個人只有一個看法:坑爹:)

apply/call 調用:
我們知道,在 js 裏,函數其實也是一個對象,那麼函數自然也可以擁有它自己的方法,有點繞,在 js 裏,每個函數都有一個公共的 prototype —— Function,而這個原型自帶有好幾個屬性和方法,其中就有這裏困惑的 bindcallapply 方法。先說 apply 方法,它讓我們構造一個參數數組傳遞給函數,同時可以自己來設置 this 的值,這就是它最強大的地方,上面的 3 種函數調用方式,你可以看到,this 都是自動綁定的,沒辦法由你來設,當你想設的時候,就可以用 apply() 了。apply 函數接收 2 個參數,第一個是傳遞給這個函數用來綁定 this 的值,第二個是一個參數數組。看代碼

function a(xx) {        
    this.b = xx;
}
var o = {};
a.apply(o, [5]);
alert(a.b);    // undefined
alert(o.b);    // 5

是不是很神奇,函數 a 居然可以給 o 加屬性值。當然,如果你 apply 的第一個參數傳遞 null,那麼在函數 a 裏面 this 指針依然會綁定全局對象。

call() 方法和 apply() 方法很類似,它們的存在都是爲了改變 this 的綁定,那 call()apply() 有什麼區別呢?就我個人看來,沒啥鳥區別。。。開玩笑!剛剛說了,上面 apply() 接收兩個參數,第一個是綁定 this 的值,第二個是一個參數數組,注意它是一個數組,你想傳遞給這個函數的所有參數都放在數組裏,然後 apply() 函數會在調用函數時自動幫你把數組展開。而 call() 呢,它的第一個參數也是綁定給 this 的值,但是後面接受的是不定參數,而不再是一個數組,也就是說你可以像平時給函數傳參那樣把這些參數一個一個傳遞。所以如果一定要說有什麼區別的話,看起來是這樣的

function a(xx, yy) {    
    alert(xx, yy);    
    alert(this);    
    alert(arguments);
}
a.apply(null, [5, 55]);
a.call(null, 5, 55);

僅此而已。

最後再來說 bind() 函數,上面講的無論是 call() 也好, apply() 也好,都是立馬就調用了對應的函數,而 bind() 不會, bind() 會生成一個新的函數,bind() 函數的參數跟 call() 一致,第一個參數也是綁定 this 的值,後面接受傳遞給函數的不定參數。 bind() 生成的新函數返回後,你想什麼時候調就什麼時候調,看下代碼就明白了

var m = {	
    "x" : 1
};
function foo(y) {	
    alert(this.x + y);
}
foo.apply(m, [5]);
foo.call(m, 5);
var foo1 = foo.bind(m, 5);
foo1();

末了來個吐槽,你在 js 裏想定義一個函數,於是你會這麼寫:

function jam() {};

其實這是 js 裏的一種語法糖,它等價於:

var jam = function() {};

然後你想執行這個函數,腦洞大開的你會這麼寫:

function jam() {}();

但是這麼寫就報錯了,其實這種寫法也不算錯,因爲它確實是 js 支持的函數表達式,但是同時 js 又規定以 function 開頭的語句被認爲是函數語句,而函數語句後面是肯定不會帶 () 的,所以才報錯,於是聰明的人想出來,加上一對括號就可以了。於是就變成了這樣:

(function jam() {}());

這樣就定義了一個函數同時也執行它,詳情參見 ECMAScript 的 Expression Statement 章節


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