iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 22
1
Modern Web

教練我想學 JavaScript 系列 第 22

Day 22 call()、apply() 與 bind()

當呼叫函數時執行環境會被丟進執行堆最上方執行,
函數的執行環境中我們已經知道也會有變數環境、外部環境、以及特殊變數 this ,

在全域環境中我們不管是透過函數陳述句或是函數表達式在函數內部取用變數 this 都會指向全域物件,
如果是物件中的方法(在物件中如果屬性的值是一個函數則此屬性稱作方法)則方法內的 this 會是物件本身,
物件方法中如果還有函數則 this 會指向全域物件,

那我們可以改變 this 指向哪個物件嗎?

因為一級函數(first class function)的概念,在 JavaScript 中函數是一種特殊類型的物件,
函數可以傳入另一個函數,函數也可以有屬性,

像是之前我們提過函數有兩個隱藏的屬性,
名稱屬性與程式屬性,名稱屬性就是函數名稱,可有可無,函數可以是匿名,
程式屬性是函數中的所有程式碼,

帶其實函數本身也有一些內建的方法如: bind()、call() 、apply(),

如下圖:

圖片來源:JavaScript 全攻略:克服 JS 的奇怪部分課程第 4 節講座 50 影片截圖

我們來看一段程式碼,
程式碼如下:

var person = {
  firstname: 'Jimmy',
  lastname: 'Huang',
  getFullName: function() {
    var fullname = this.firstname + ' ' + this.lastname;

    return fullname;
  }
};

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

logName('en', 'es');

我透過物件實體語法建立一個有兩個屬性一個方法的物件,注意我在裡面有使用 this 取得物件本身的屬性,
接著我透過函數表達式建立一個函數,在函數中我透過 this 想要呼叫 person 物件的 getFullNam 方法,
你覺得可以嗎?
我們到 Console 中來看結果:

因為現在指派給變數 logName 的匿名函數中的 this 指向全域物件 ,
在全域物件中我們沒有getFullNam 方法可以呼叫,所以會報錯,

那我們可以透過剛才提過的函數本身內建的 bind() 方法來改變 this 指向的物件,
在 bind() 方法中帶入的第一個參數就是要改變 this 指向哪個物件,
程式碼如下:

var person = {
  firstname: 'Jimmy',
  lastname: 'Huang',
  getFullName: function() {
    var fullname = this.firstname + ' ' + this.lastname;

    return fullname;
  }
};

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

var logPersonName = logName.bind(person);

logPersonName('en', 'es');

函數的 bind 方法除了可以改變 this 指向哪個物件外,還可以拷貝函數,
只是在執行時將 this 指向我們傳入的物件,
變數 logPersonName 的值 logName 我們沒有透過括號呼叫,因為呼叫會回傳值,
只要像在用點(成員取用)運算子一樣取用指派給變數 logName 的函數本身的 bind 方法即可,
記得要給函數 bind 方法你想讓 this 指向的物件,
呼叫指派給變數 logPersonName 的函數時,
因為現在的 this 指向 person 物件,所以可以取用到 person 物件的 getFullName 方法,
呼叫指派給變數 logPersonName 的函數時我一樣可以傳入參數,

在 Console 中的結果如下:

接著來看函數本身內建的 call() 方法,
call 方法不像 bind 會拷貝函數,但 call 方法會執行函數,第一個參數一樣是 this 想要指向哪個物件,
之後可以依據函數的需要傳入對應參數數量的參數,
程式碼如下:

var person = {
  firstname: 'Jimmy',
  lastname: 'Huang',
  getFullName: function() {
    var fullname = this.firstname + ' ' + this.lastname;

    return fullname;
  }
};

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

logName.call(person, 'jp', 'kr');

Console 中的結果如下:

與 bind 方法不同我不用在另外將 logName 指派給一個變數後再去呼叫,
透過指派給變數 logName 的函數本身的內建方法 call 除了能夠改變 this 指向的物件外,
還能馬上呼叫執行函數與傳入參數,

在來我們看看函數內建方法 apply(),
apply 方法一樣不會像 bind 一樣拷貝函數,
但與函數本身的 call 方法類似除了能夠改變 this 指向的物件外還會執行函數,
需要注意的是與 call 方法的差異是在傳入參數時 apply 方法必須的是一個陣列,不能分開傳入否則會報錯,
程式碼如下:

var person = {
  firstname: 'Jimmy',
  lastname: 'Huang',
  getFullName: function() {
    var fullname = this.firstname + ' ' + this.lastname;

    return fullname;
  }
};

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

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

在 Console 中的結果如下:

與 bind 方法不同我不用在另外將 logName 指派給一個變數後再去呼叫,
透過指派給變數 logName 的函數本身的內建方法 apply 除了能夠改變 this 指向的物件外,
還能馬上呼叫執行函數與傳入參數(apply 只接受一個陣列當作參數),

除此之外函數本身的apply 、bind 、call 方法很常用在以下按例,
你可能會像別的物件借用物件裡的方法,這稱作函數借用(function borrowing),
我新增另外一個物件,但物件內沒有自己的 getFullName 方法,
程式碼如下

var person = {
  firstname: 'Jimmy',
  lastname: 'Huang',
  getFullName: function() {
    var fullname = this.firstname + ' ' + this.lastname;

    return fullname;
  }
};

//function borrowing
var person2 = {
  firstname: 'John',
  lastname: 'Doe'
};

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

在 Console 中的結果如下:

如果我想在 person2 物件中也使用 person 物件內的 getFullName 方法,
只要在呼叫 person 物件的 getFullName 方法時,
透過 getFullName 方法本身的 apply 方法來改變 this 指向的物件,
現在 this 指向 person2,所以 this 取用到的屬性會是在 物件 person 中的屬性,
使用 call 方法也可以達到此效果,

另外一個例子是在呼叫函數時設置預設值( function currying),
程式碼如下:

//function currying
function multiply(a, b) {
  return a * b;
}
multiply().bind(this, 2);

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

在呼叫函數 multiply 時透過 bind 來設定參數 a 的固定值,
這種作法就像在函數 multiplyTwo 中將 2 指派給參(變數) a 一樣,
因為我們沒有要改變 this 指向的物件,所以 bind 方法的第一個參數傳入 this 就好,
這樣就不會給變 this 指向的物件,

接著我將這個透過 bind 方法拷貝的函數指派給變數 multiplyByTwo,
透過呼叫指派給變數 multiplyByTwo 的新拷貝的函數(目前參數 a 有預設值),
程式碼如下:

//function currying
function multiply(a, b) {
  return a * b;
}

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


console.log(multiplyByTwo(4));

在 Console 中的結果如下:

在呼叫函數 multiplyByTwo 時,我傳入 4 ,這個 4 會 傳給參數 b 的,因為我們在拷貝函數時已經替參數 a 設定固定值,現在參數 a 是 2 ,函數 b 是 4,相乘後得到 8 ,你以可以在呼叫函數 multiplyByTwo 時傳入不同值玩玩看,

我也可以透過 bind 方法將兩個參數都給固定值,
程式碼如下:

//function currying
function multiply(a, b) {
  return a * b;
}

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


console.log(multiplyByTwo(5));

Console 中的結果:

如果我們在透過 bind 方法拷貝函數時,給兩個參數固定執,
就算我們在呼叫將拷貝後的函數時傳入不同的參數,輸出的結果不會被影響,

如果我在拷貝函數時不給參數則在呼叫拷貝完成後的函數時,我可以傳入不同的參數,
程式碼如下:

//function currying
function multiply(a, b) {
  return a * b;
}

var multiplyByTwo = multiply.bind(this);

console.log(multiplyByTwo(2, 5));

在 Console 中的結果如下:

這樣輸出的結果就會是我們在呼叫新的函數時傳入的參數所計算完回傳的結果了,

也可以透過函數的 bind 方法拷貝很多個新的函數給變數,設置不同的參數固定值,
程式碼如下:

//function currying
function multiply(a, b) {
  return a * b;
}

var multiplyByTwo = multiply.bind(this, 2);
console.log(multiplyByTwo(4));

var multiplyByThree = multiply.bind(this, 3);
console.log(multiplyByThree(4));

在 Console 中的結果如下:

當呼叫這些變數時,其實也就是在呼叫每次透過 bind 方法拷貝的新函數,呼叫時我們傳入參數 b 的值,
因為每個參數 a 都被設定成不同的固定值,所以計算完後會得到不同的回傳結果,

function currying 可以在透過函數本身的 bind 方法拷貝函數時,設置參數的固定值,
這在數學計算時很有用也常用在資料庫數學計算。


上一篇
Day 21 Function Factories、閉包與回呼
下一篇
Day 23 函數程式設計
系列文
教練我想學 JavaScript 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言