[JS奇怪的世界]No.42 call()、apply()與 bind()

前言

這節課將來討論 JavaScript 中的三個方法 call()apple()bind(),而這三個東西與我們前面所講的東西有關係。

在探討這三個方法之前必須先瞭解什麼一級函數,一開始我們已經知道一級函數是特殊形態

  • 它可以有名稱及沒有名稱 (也就是匿名函數)
  • 它有程式區塊,裡面可以撰寫程式碼,然後程式是可以被呼叫
  • 函數就是物件,所以本身也可以有屬性及方法
    • 所有函數都包含著 call()apply()bind(),而這三個都與 this 以及我們傳入的參數有關。

一級函數

call()、apply()與 bind()

接下來我們試著連跟著課程範例了解。

1
2
3
4
5
6
7
8
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

那 call()、apply()與 bind() 該怎麼使用呢?假設我們有一個 logName 匿名函數

1
2
3
4
5
var logName = function (lang1, lang2) {
console.log('Logged' + this.getFullName());
}

logName();

課程來看,其實我們大概都可以知道這樣會出現錯誤,因為 this 指向的是 window,而 window 下並沒有一個函數叫 getFullName

bind

但是如果今天我們可以控制 this 指向誰的話呢?所以這邊就要試著使用看看 bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var logName = function (lang1, lang2) {
console.log('Logged' + this.getFullName());
}

var logPersonName = logName.bind(person) // 把函數當作物件使用

logPersonName();

bind 這個方法會回傳新的函數,當 JavaScript 看到 bind 將會重新指向 this 到傳入 bind 方法的東西。

所以 bind 創造任何我們呼叫的函數拷貝,不論我們傳入函數還是物件都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var logName = function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2)
console.log('-----------');
}

var logPersonName = logName.bind(person) // 把函數當作物件使用

logPersonName();
logPersonName('en');

call

接下來看 call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var logName = function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2)
console.log('-----------');
}

logName.call(person) // 也可以用這種方式呼叫。

call 與 bind 一樣可以傳入要拷貝的函數,但是可以傳入參數並執行他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var logName = function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2)
console.log('-----------');
}

logName.call(person,'en','es')

bind 只是單純拷貝並沒有執行函數,但 call 可以拷貝並執行函數。

apply

apply 呢?與 call 非常像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var logName = function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2)
console.log('-----------');
}

logName.apply(person,'en','es');

但是如果我們試著跟 call 一樣傳入參數就會出現錯誤。

所以應該怎麼使用 apply?,apply 。主要接受的參數是陣列,而不是普通的字串、數值等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var logName = function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2);
console.log('-----------');
}

logName.apply(person, ['en', 'es']);

我們可以從上面發現 apply 非常強大,在傳入參數後會自動幫我們對應載入相對應的參數。

所以我們這邊也可以透過匿名函數 + apply or call 立刻執行呼叫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

(function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2);
console.log('-----------');
}).apply(person, ['en', 'es']);

(function (lang1, lang2) {
console.log('Logged: ' + this.getFullName());
console.log('Arguments: ' + lang1 + ' ' + lang2);
console.log('-----------');
}).call(person,'en','es');

真實例子

那我們該如何套用這些到真實例子上?

函數借用 (function borrowing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var person2 = {
fistname: 'Jack',
lastname: 'Mary',
};

person.getFullName.apply(person2);

在這個例子我們可以將原有 person 底下的函數 (getFullName) this 轉指到 person2

而 call 也是一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = {
fistname: 'John',
lastname: 'Doe',
getFullName: function() {
var fullname = this.fistname + ' ' + this.lastname;
return fullname;
},
};

var person2 = {
fistname: 'Jack',
lastname: 'Mary',
};

person.getFullName.call(person2);

function currying

這個例子與 bind 有關,在前面使用 call 及 apply 傳入參數就只是傳入參數,但是 bind 可以創造函數拷貝。

1
2
3
4
5
6
function multiply(a, b) {
return a * b;
};

var multpleByTwo = multiply.bind(this, 2);
multpleByTwo();

注意 bind 只會拷貝並不會執行,但給他的參數將會被設定成拷貝函數的永久參數值,所以當我們第一個參數放 2 代表永遠都是 2 , a 永遠都是 2。

此時你可能會問怎麼是 NaN?試著思考一下變數的預設值是什麼 undefined,當 2 * undefined 那當然會是 NaN,這並不是一個數值,所以我們可以在執行函數時傳入數字。

1
2
3
4
5
6
function multiply(a, b) {
return a * b;
};

var multpleByTwo = multiply.bind(this, 2);
multpleByTwo(3);

當然我們可以看一下 a 跟 b 在還沒傳入參數與傳入參數後的狀況是怎樣。

1
2
3
4
5
6
7
8
function multiply(a, b) {
console.log('a: ' + a);
console.log('b: ' + b);
return a * b;
};

var multpleByTwo = multiply.bind(this, 2);
multpleByTwo(3);

1
2
3
4
5
6
7
8
function multiply(a, b) {
console.log('a: ' + a);
console.log('b: ' + b);
return a * b;
};

var multpleByTwo = multiply.bind(this, 2);
multpleByTwo();

所以如果我們將 bind 傳入兩個參數,那你就知道了 :D

function currying 建立一個函數拷貝,並設定預設的參數。

圖源

JavaScript 全攻略:克服 JS 奇怪的部分

0%