[JavaScriptWeird]No.38 call()、apply()、bind()

前言

這篇文章我們要討論三個函式 (call、apply、bind),這是用來控制特殊關鍵字 this 指向的函式。

call()、apply()、bind()

在前面的篇章提到,在執行環境中有變數環境、外部參考,還有 JavaScript 幫我們設定好的 this 變數。

我們已經看過 this 預設指向全域物件、也可以指向包含函式的物件,然而之前我們提到控制 this 的方式,是使用一個變數來儲存 this 指向的位址。

而這一篇提到的三個函式將幫助我們更有效率的控制 this

函式是特殊形態的物件,它具有

  • 名稱屬性 - 可匿名的名稱屬性
  • 程式屬性 - 可包含程式碼,而程式屬性是可以被呼叫的,所以可以被執行。

函式就是物件,所以函式可以有屬性和方法,而且所有函式都有 call()、apply()、bind() 方法

bind()

我們先建立一個初始的範例,如下:

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

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

  • 我們建立了 person 物件,並且在裡面建立了 getFullName 方法。
  • 方法內的 this 指向 person 物件,並回傳 fullName
  • 宣告了 logName 並指向一個匿名函式,但函式內的 this 此時是指向全域物件 windows ,因此執行 logName 函式後得到 undefined

這時就是 bind() 出場的時候了,將程式碼改成以下:

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

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

var logPersonName = logName.bind(person);
logPersonName();

我宣告了 logPersonName 並且取用 logNamebind 方法,然後傳入想要 this 變數指向的物件,接著 bind 方法會回傳一個新的函式,它會複製 logName 函式,並且設定為一個新的函式物件。

所以當呼叫 logPersonName 函式時,因為 this 已經指向 person 物件,所以輸出就如同預期。

甚至也可以寫得更簡潔,像是這樣:

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

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

logName();

bind 方法會產生 logName 函式的拷貝,並且將 this 指向為我們指定的物件。

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

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

logName('es','en');

bind 方法會將函式完整的拷貝下來。

這就是 bind 的作用,創造拷貝函式,然後將 this 指向到某個物件。

call()

接續上面的範例

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

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

var logPersonName = logName.bind(person);
logPersonName('es','en');
logName.call(person,'en','es');

call 方法可以讓我們決定 this 要指向哪個物件、也可以傳入參數,而第一個參數就是決定 this 要指向的物件。基本上就跟 () 用法一樣,但 call 方法允許控制 this 的值。

bind 方法不同的地方是,call 方法並不是創造函式的拷貝並等待呼叫,而是直接執行並且改變 this 的指向

apply()

接續上面的範例

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

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

var logPersonName = logName.bind(person);
logPersonName('es','en');
logName.call(person,'en','es');
logName.apply(person, ['es','en']);

apply 方法與 call 方法雷同,只是控制 this 之後的傳入參數,部分略有不同。

call 方法允許傳入各種型別的值,但是 apply 方法只接受陣列作為參數。

所以這兩個方法可以根據函式的情況來使用。

不使用括號來達成立即執行函式

看過了 apply 方法與 call 方法,我們可以這麼寫:

1
2
3
4
5
6
7
8
9
10
11
(function(lang1, lang2){  
console.log('Logged: ' + this.getFullName());
console.log('Argument: ' + lang1 + ' ' +lang2);
console.log('---------------');
}).call(person,'es','en');

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

所有的函式都可以使用 apply、call、bind 方法。

實際上的使用

這些方法可以如何地在實際開發上使用呢?

函式借用 (function borrow)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = {  
firstName: 'John',
lastName: 'Doe',
getFullName: function(){
var fullName = this.firstName + ' ' + this.lastName;
return fullName;
}
}
// function borrow
var person2 = {
firstName: 'Jane',
lastName: 'Doe',
}

console.log(person.getFullName.call(person2));

我們創造了兩個物件,一個 person 物件、一個 person2 物件,但是在 person2 物件內沒有 getFullName 方法。

可以透過 callapply 方法達成函式借用,即使 person2 物件內沒有 getFullName 方法,透過改變 this 的指向,也可以輸出 person2 的物件內容。

函式柯里化 (function curring)

這個部分跟 bind 方法的特性有關,如果我們傳入參數給 bind,會有不太一樣的事情發生,讓我們來觀察。

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

var mulipleByTwo = multply.bind(this, 2);
console.log(mulipleByTwo(4));

我寫了一個會回傳 a * bmultply 函式,並且使用 bind 方法拷貝一份新的函式給 mulipleByTwo ,在此 this 的指向不重要,重要的是傳入的參數。

bind 方法會將傳入的參數設定為一個定值。

以例子來說 multply.bind(this, 2) 就好比這樣:

1
2
3
4
5
6
7
8
9
10
11
function multply(a, b){  
return a*b;
}

function mulipleByTwo(a, b){
var a = 2;
return a*b;
}

var mulipleByTwo = multply.bind(this, 2);
console.log(mulipleByTwo(4));

而在 bind 內設定了傳入的參數後,我們呼叫的函式所帶入的參數就會變成另一個,以本例來說就是 b

因此輸出就是 2 * 4 = 8

如果 bind 填入了所有參數呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function multply(a, b){  
return a\*b;
}

function mulipleByTwo(a, b){
var a = 2;
return a\*b;
}

var mulipleByTwo = multply.bind(this, 2);
console.log(mulipleByTwo(4)); // 8

var mulipleByThree = multply.bind(this, 3, 4);
console.log(mulipleByThree(1,7)); // 12

像這樣,使用 bind 方法拷貝一份新的函式給 mulipleByThree ,並且填入 ab 參數。

當我們呼叫 mulipleByThree 帶入參數時,受到 bind 影響,所以無論帶入什麼,輸出的結果都是 12 。

於是 函式柯里化 (function curring) 的意思就是建立一個函式的拷貝,並設定不可變的預設參數。

函式柯里化主要用於數學的運算,可以寫個基本的函式,然後根據這個函式放入預設參數,用以減少需要填入的參數,這是一種 bind 的用法。

0%