Web Development 網站開發 (8) - Javascript 非同步:Function and Callback

Callback 是 Javascript 的一個特色。簡單來說就是把一個 function 當作另一個 function 的參數傳進去。其他的程式語言也有類似的方法,但在 Javascript 裡應該是使用最多的。我想原因應該是來自於其使用的場景,Javascript 原先就是用來開發網頁的,需要監聽來自於使用者的各種操作並作出即時的響應,非同步搭配 Callback 的設計能很好的執行任務。

Function

在 Javascript 裡的 function 有各式各樣的寫法。有基本的、匿名的還有 arrow function 等,可以直接看一下例子:

// 最基本的 function
function add(a, b) {
    return a + b;
}

add(1, 2)

// javascript 裡的 function 是 object 的一種,可以被 assign 給變數
// 因為這個 function 立刻就會被 assign 給 add1,那我們不用定義 function name。
// 當然因為他被 assign 給 add1,所以你終究還是會得到一個 add 的 function
const add1 = function(a, b) {
    return a + b;
}

add1(2, 3)

// 在 object 底下定義 object 有以下方法:
const obj = {
    add(a, b) {
        return a + b;
    }
}

obj.add(1, 2);

// assign 給 obj1 裡的 add 成員,跟上面一般的 assign 一樣也可以是匿名
const obj1 = {
    add: function(a, b) {
        return a + b;
    }
}

obj1.add(1, 2);

// 這個是另一種 function 的寫法,我們稱之為 arrow function,除了語法上的不同外大部分的地方都是一樣的,有一個差別是 `this` 在 arrow function 裡的意義跟一般的 functino 不太一樣,這邊先不提。 
const add2 = (a, b) => {
    return a + b;
}

add2(1, 2);

// arrow function 的 inline 格式,變得很簡潔
const add3 = (a, b) => a + b;

add3(2, 3);

// arrow function 也可以 assign 在 object 裡
const obj2 = {
	add: (a, b) => a + b
}

obj2.add(1, 2);
function

以上我們知道了 function 是一個 object 所以可以被 assign 進一個變數裡。那既然變數可以被當作參數傳進 function 裡,代表說我們可以把一個 function 當成參數傳進另一個 function 裡!

function a() {
    console.log('I am a');
}

function b(func) {
    func();
    console.log('I am b');
}

// a 這個 function 被當作參數傳給 b,也就是第五行的 `func`
// 第六行 call 了 func(),也就是 call 了 a,這時就會印出 I am a
// 接著執行第七行,印出 I am b
// 所以執行這個 functino 會先印出 I am a 然後 I am b
b(a); 
function as a parameter

Callback

以上 function call function 就是 Callback 的核心概念,如果要問為什麼 Callback 在 Javascript 的世界裡這麼重要的話,大概就是因為 Javascript 是一個 single threaded 的語言。

大部分的語言要同時執行多件事都是開 multi threads,讓不同的任務在不同的 threads 裡執行。Javascript 只有一個 thread ,如果要在相近的時間內執行多個任務,就必須要用 callback 的方式。這邊做一個完後 callback 回去做另一個,再搭配 event loop 來達成這種 “非同步” 的行為。 (event loop的話題也很有趣,可能需要另外一個篇幅來解釋)

// setInterval 是瀏覽器內建給你的 function,使用方式是傳進一個 function 給他
// 並指定秒數(1000 = 1 sec),他會每隔一段你指定的時間執行你定義的 function
// 這個 case 他會每格一秒把 counter 加一然後印出來
// 其實他在做的事情就是你先call它,然後他每一秒callback回來。你的程式不會被 setTimeout 卡住,它會繼續往下執行(非同步)。
let counter = 0;

setInterval(function () {
    console.log(++counter);
}, 1000);


// Callback 不是一定只能用在非同步的場景,很多內建的函式要求你傳callback進去來完成任務,更像是挖格子填內容的概念。

// Case1: forEach
// array 內建有 forEach,它會試著去 iterate 每一個 array 裡的元素,並且用當下的元素當作參數塞你定義的函數去執行。
const arr = [1, 2, 3];
arr.forEach(function(item) {
    console.log(item); // 會依序列印 1, 2, 3
});

// Case2: map
// array 內建 map,它會試著去 iterate 每一個 array 裡的元素,並且用當下的元素當作參數塞你定義的函數去執行,你的 function 必須要回傳一個結果,它會搜集你回傳的每個結果,再組出一個新的 array。
// 除了基本的 function 以外,這些內建函數都允許你塞 arrow function
// 我們在 map 把傳進來的數 +1 再回傳,也就是說以下做的事情是把當下 array 的元素全部加1組成新的 array。
const arr1 = [1, 2, 3];
const arr2 = arr1.map(item => item + 1); 

console.log(arr2); // arr2 現在是 [2, 3, 4]
callback

可以看看一個有 forEach 功能的 function 是怎麼實現出來的:

// Bonus: 如何實現一個 forEach?
Array.prototype.myForEach = function(fn) {
    for (let i = 0; i < this.length; i++) {
        fn(this[i]);
    }
}


const arr = [1, 2, 3];
arr.myForEach(function(item) {
    console.log(item); // 會依序列印 1, 2, 3
});

// 解釋:
// 用 prototype 可以幫內建的 array 型態新增一個字定義的成員
// Array.prototype.myForEach 定義好以後,你就可以對一個 array 互叫 myForEach
// myForEach 的參數是 function 所以我們在 line2 定義了 `fn`
// 用 arr.myForEach 時,myForEach 裡的 this 就是指 arr
// line 3-5 就是 iterate 這個 this(array),然後把 array 的每個元素依照順序當成參數傳到傳進來的 function fn裡執行。
implement forEach

最後面不得不提到最常用的事件監聽,用戶的操作可以在任何時間點發生,我以我們的事件監聽 Function 理所當然是非同步,需要給一個 Callback 去處理事件。

const button = document.getElementById('button1');

// 這裡的 addEventListenr 帶了兩個參數 (第三個後省略),第一個是監聽的事件,
// 第二個是一個匿名的 callback function。
// 當這個按鈕點擊時,callback function 會被呼叫,所以執行 line7 的視窗彈跳。
button.addEventListener('click', function() {
    alert('button1 is clicked');
})

const input = document.getElementById('username_input');

// keypress 是鍵盤按下的事件偵測,假如今天有人在 username_input 這個 input 
// 裡打字了那這裡的 callback function 就會被觸發
input.addEventListener('keypress', function(event) {
    // do something
});

// 事件有非常多種,滑鼠滾動、滑鼠移動、圖片加載完成、頁面加載完成等各式各樣的事件
// 可以參考這裡 https://developer.mozilla.org/en-US/docs/Web/API/Event/type