JavaScript中bind、call、apply函式用法詳解

菜鳥浮出水發表於2015-03-13

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

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

javascript中bind、call、apply函式的用法

為了介紹 bind、call、apply 這三個函式的用法,不得不介紹 js 裡函式的一些設定。關於這部分推薦通讀 《javascript程式設計精粹》 的第四章,這裡我所說的在書裡都能找到。

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

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

js 裡函式呼叫有 4 種模式:方法呼叫、正常函式呼叫、構造器函式呼叫、apply/call 呼叫。

同時,無論哪種函式呼叫除了你宣告時定義的形參外,還會自動新增 2 個形參,分別是 this 和arguments。

arguments 不涉及到上述 3 個函式,所以這裡只談 this。this 的值,在上面 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,而這個原型自帶有好幾個屬性和方法,其中就有這裡困惑的 bind、call、apply 方法。先說 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 開頭的語句被認為是函式語句,而函式語句後面是肯定不會帶 () 的,所以才報錯,於是聰明的人想出來,加上一對括號就可以了。於是就變成了這樣:

1(function jam() {}());

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

相關文章