發表文章

JavaScript async/await 的奇淫技巧

圖片
JavaScript async/await 的奇淫技巧
話說,最新的 ECMAScript 已經引入了 async/await 語法,讓開發者可以更容易控制非同步的程式邏輯,換言之,我們可以減少許多 callback 的使用,讓 JavaScript 這種單線程、事件驅動的程式語言更易讀、好寫。

關於 async/await 的基礎使用,有興趣的人可以參考舊文「JavaScript 好用的 async 異步函數!」,而本文將探討更多實際使用上的小技巧。

另外,瀏覽器不一定有支援 async/await,你可以在新版的 Node.js 上面測試本文的內容。

呼叫 async 函數與一般的函數沒有差別
想像一下,async 函數就是一個在執行後會回傳 Promise 物件的「普通函數」,和一般常見的函數的使用差異,僅僅只是 async 函數在執行後「不是回傳函數執行結果」。這代表我們可以把 async 函數當作一般函數來呼叫使用,用法一模一樣。

async/await 與 Promise 是可以共通的
非常有趣,async 函數與 Promise 其實能夠共通,這代表我們可以玩一些特別的組合技。所以,若要把 async/await 玩得通透,建議你盡量熟悉 Promise 的各種用法。

實現 delay 函數
過去因為單線程和事件驅動的關係,JavaScript 不可能實現一個沒有嚴重副作用的 delay 函數,所以取而代之的是使用 setTimeout() 加上 callback 來實現「一定時間後執行什麼工作」的需要。

不過來到 async/await 的世界後,我們可以一行行描述程式邏輯,無論是不是同步(Synchronous)的程式碼,所以我們可以用 Promise 來包裝 setTimeout(),以實現一個在 async 函數裡可以跑的 delay 函數:

// 實現一個等待函數 const delay = (interval) => { return new Promise((resolve) => { setTimeout(resolve, interval); }); }; const main = async () => { console.log('Starting...…

自幹 JavaScript 的 Tail Call Optimization

圖片
ECMAScript 6 開始,規範中出現了一項被稱為「尾呼叫優化(Tail Call Optimization, TCO)」的優化技術,這讓開發者可以在函數的執行過程中,減少 Stack Frame 的數量,進而提升效能。TCO 尤其是在遞迴這種不停呼叫自己或新函數的工作上,能得到最大的優化效益,能提升遞迴的執行效能如同迴圈一樣。

只不過很可惜的是,截至本文發稿前,大多數瀏覽器及 JavaScript 引擎尚未支援這項技術。但我們還是可以自幹並模擬一個 TCO 的行為,雖然比起語言本身、編譯器(Compiler)及虛擬機(VM)層面的實現,效果差了些,但仍然可以減少 Stack Frame 的數量避免達到 Stack Frame 的數量上限。

什麼時候會啟用尾呼叫優化機制?
如果 JavaScript 引擎有支援,通常一個函數執行到最後一行 return 時,是回傳另一個函數的執行結果,就會啟用 TCO 機制,如:

const f = () => { return 999; }; const g = () => { // 執行並直接回傳 f 函數的執行結果:會啟用尾呼叫優化機制 return f(); }; g();
但要注意的是,回傳的「必定為函數的直接回傳值」,所以下面這些寫法不會啟用 TCO 機制:

// 不會啟用 TCO 機制的設計 const g = () => { return f() + 1; }; // 不會啟用 TCO 機制的設計 const g = () => { let ret = f(); return ret; };
創造一個跑不完的函數
首先我們先創造一個肯定跑不完的遞迴,然後改善它:

const func = (x) => { // 讓他跑 10000000 次 if (x === 10000000) return x; return func(x + 1); }; let ret = func(0);
理論上,如果你直接執行上述程式碼,會得到 stack size 超過上限的錯誤訊息:

RangeError: Maximum call stack size exceeded
簡單模擬實現自己的 TCO 效果
簡單…

實現 JavaScript 的 Memoization

圖片
函數式程式設計(Functional Programming)是近年來越來越被軟體開發者常提及的話題,許多人討論它時,不外乎說其就是在程式設計中引入了數學方法,彷彿有神奇又高深的理論加持一般。事實上,對於一般開發者而言,函數式程式設計比較通俗且直接的好處,就是讓開發者可以在「函數」的層面和維度,進行邏輯或是效能上的優化。所以說,比起命令化的執行程式、管理物件,怎麼去設計和管理函數這件事,就是函數式程式設計所關心的重點。

JavaScript 這語言在設計上,天生就支援 first-class function,這代表函數在 JavaScript 是一種資料型態,可以被當成普通物件傳遞、處理,這讓開發者在使用 JavaScript 時可以時不時引入 Functional Programming 的技巧和概念。

本文將介紹 Functional Programming 中大量被使用的 Memoization 機制,然後我們如何在 JavaScript 中引入並實地使用它,無論你會不會 Functional Programming,這都是一個可以常用於日常開發中的優化技巧。

Memoization 是什麼?
用一般程式開發的說法就是快取(Cache)機制,只不過 Memoization 是針對「函數」進行快取。快取的好處在於我們只要執行過一次工作後,之後在執行相同工作前,就能提前知道執行結果為何,所以我們可以不用「真正的」去執行工作,而直接取用執行結果就好,可大量提升程式執行的效能。

同樣快取概念套用在函數上,若我們給予特定「輸入(Input)」到一個函數中,而該函數會回傳一個特定的「輸出(Output)」,理論上函數執行一次後,下次再使用這個函數時,只要「輸入」和過去一樣,我們就能提前知道結果。

而這樣對函數進行的快取機制,就是所謂的 Memoization。

只能使用在純函數
你可能會聽一些人說,只有「純函數」才能引入快取機制,然後開始討論數學上所謂函數的定義,然後你就聽到昏了,後面在講什麼你就都聽不進去了。

但如果撇除函數的數學定義,若白話來說,能被快取的東西,就是能被預測的東西,這代表函數的執行結果也要能被預測,也就是一樣的輸入值,就會有一樣的輸出結果。

所以,如果一個函數每次執行,代入的輸入值一樣,但回傳結果卻是可能不一樣,這就不是一個純函數,像是取亂數的函數就屬…

Koa 2 起手式!

圖片
在 Node.js 的世界裡,說到今天最潮的 Web Framework,肯定就是 Koa!其用了最新的 JavaScript 語法和特性,來改善 Web Framework 的設計。只不過,Koa 雖然相對於其他舊的 Web Framework 來說有相當多的進步,但很多人卻相當討厭 Koa 的 Generator 設計,尤其是那些「*」符號,那不知所謂的 yield 也讓很多人不舒服。所以至今仍然有許多人在使用 express 來當作自己的 Web Framework,寧可繼續使用那老派的 callback 設計,而不肯嘗試 Koa。

隨著 ECMAScript 標準的進步,Koa 才剛被開發出來沒多久,原本的開發團隊就立即著手打造 Koa 2 ,開始更進一步採用更新的 JavaScript 特性,以 async/await 語法重新打造了全新且更簡潔的框架。可惜的是,由於 async/await 語法一直遲遲沒有被 JavaScript 引擎原生支援,因此總需要靠 babel 編譯打包程式碼後,才能正常跑在 Node.js 之上。這讓 Koa 2 一直無限期處於非穩定版,讓原開發者從開發的一開始,就打算等到 V8 和 Node.js 開始原生支援 async/await 後,才會被以穩定版(stable)的姿態釋出。

所以,即使 Koa 2 到了今天已經相當穩定,也開始有不少人使用在自己的線上服務,卻一直無限期處於非穩定版的狀態。

另外,由於 Koa 2 大量使用 Async/Await,如果你還對 Async/Await 的使用還不熟悉,建議在閱讀本文之前,先閱讀舊文「JavaScript 好用的 async 異步函數!」來學習如何使用。

學習 Koa 的好時機來囉
總算,日前 Node.js v7.6.0 釋出後已經正式宣布原生支援了 async/await 語法,而且不需要額外的參數選項。伴隨著這個消息,Koa 2.0 也隨即正式釋出了!

Node.js 內建支援 ES7 的 async/await 真的是非常棒的消息!過去我們使用 async/await,都還需要 babel 的協助才能正常跑在舊版的 Node.js,不但開發上相當麻煩,非原生的各種 ES7 特性也浪費不少額外的記憶體和效能,這樣的問題在斤斤計較效能的 Server 環境下,更是讓…

Node.js 也可以使用 Protocol Buffers!

圖片
Protocol Buffers (protobuf)」是一套 Google 所提出的結構化資料的包裝技術,讓資料便於網路傳輸或交換,如同常見的 JSON 和 XML 等技術一般。但相對於其他常見技術,protobuf 設計上更易於用來包裝二進位資料,應用在串流(Streaming)技術上,在資料包裝上也更為節省空間,在包裝或解析上也更有效率。

註一:若採用 JSON,由於原本的設計上並無法處理二進位資料,所以如果要包裝二進位資料,傳統做法會將資料轉換成 base64 的格式,再以字串(String)的格式儲存。因為這等於二次包裝資料,導致處理上非常沒有效率。

註二:與 Google Protocol Buffers 類似的技術還有 MessagePack 及 Facebook 採用的 Apache Thrift,有興趣的人可以自行參考比較。

跨語言的優點
另外,Protocol Buffers 最大的優點,就是擁有跨程式語言的設計,提供了一個標準通用的 .proto 定義方法,讓我們定義資料結構和格式。只需要載入這些我們事先準備好的資料定義,就可以輕易生成給不同語言(如:C++、C#、Go、Java、Objective-C 或 Python)用的資料解析器、包裝方法,讓我們可以在不同的語言之間,解析或包裝相同的結構資料。

Protocol Buffers 的使用場景?
若在純粹的 Web 應用下,大多數情況,我們不需要處理二進位資料,或是需要非常精準的資料格式,也不會進行單筆高流量的資料交換,所以使用 JSON 或 XML 已經足以。但若你的應用有串流、二進位資料的需求,Protocol Buffers 就是你可以考慮的選擇。

像是筆者在一些公司專案中,會運用 Message Queuing 進行各種訊息資料傳遞,以達成各種資料處理需求。但由於訊息資料內可能有大大小小等各種資料形式和資料型態需求,導致 JSON 包裝已經完全不敷使用,甚至有效能上的疑慮,這時就會採用 Prorocol Buffers 來打包資料。

安裝 ProtoBuf.js
Google 官方其實並沒有實作 JavaScript 版本的 Protocol Buffers 支援,但還好廣大的社群已經有高手開發出 JavaScript 的模組「ProtoBuf.js」,除了在 Node.js 上可…

上手使用 JavaScript 的 Map、Reduce 吧!

圖片
雖然有些概念類似甚至可以相通,但這裡並不是指常聽到的「MapReduce」,本文目的不是要討論如何運用 MapReduce 這樣的架構去處理大資料庫。這裡真正要討論的是,如何使用 JavaScript 裡陣列(Array)中的 .map() 和 .reduce() 方法,並把一些常見的使用方法和情境描述出來大家進行參考。

很多人對這兩個方法不習慣,原因不外乎是這兩種方法本來就不是一個非常直覺的東西,在大多數 JavaScript 語言的開發情境中,其實也沒有非得使用的理由。但不得不說,習慣了這兩個對陣列操作的方法,程式碼會變得簡潔,也更容易能處理一整批的資料。有時也能順便學習到一些「Functional Programming」會用到的概念,無論是在改善程式品質,還是投資自己的角度上,都有相當好處。

從最簡單的遍歷陣列開始
面對一個陣列裡的一堆資料,我們一定是從遍歷開始,一一處理裡面的每一筆資料。你也許已經非常熟悉如何遍歷陣列,最常見的不外乎就是兩種做法。

使用 for-loop:

var myArr = [ 1, 2, 3 ]; for (var index in myArr) { console.log(myArr[index]); }
使用陣列內建的 forEach 方法:

var myArr = [ 1, 2, 3 ]; myArr.forEach(function(element) { console.log(element); });
使用 .map() 對每個陣列元素加工
有些時候,我們想對每個陣列元素(Element)進行加工處理,於是最土法煉鋼的方法大概就是這樣:

幫每個元素加一:

var myArr = [ 1, 2, 3 ]; for (var index in myArr) { myArr[index] = myArr[index] + 1; } // [ 2, 3, 4 ] console.log(myArr);
這時你可以使用 .map() 方法來達成同樣目的:

var myArr = [ 1, 2, 3 ]; var newArr = myArr.map(function(element) { return element + 1; }); // [ 2, 3, 4 ] console.log(new…

Node.js 小密技:以 Readline 核心模組一行行讀取檔案內容

圖片
最近參與了一些關於資料處理的專案,處理了很多各式各樣的原始資料(Raw Data)或各種不同格式的資料,於是使用到了 Node.js 上的一些小技巧。像是一行行讀取檔案內容這件事,就隱藏了一些技巧。

對很多人來說,處理的檔案內容都不大,如果用 Node.js 來一行行讀取檔案內容,不外乎就是將整個檔案讀出後再進行切割,做法大致上如下:
var fs = require('fs'); fs.readFile('example.txt', function(err, data) { // 以換行字元作為切割點,將內容切成一個大陣列 var lines = data.split('\n'); lines.forEach(function(line) { // 一行行處理 }); }); 但有些時候,由於檔案並不小,若又牽涉到運算,不可能整個檔案都讀出到記憶體上才進行切割,這時就得用到 Stream(資料流)機制,將檔案一段段讀出來進行處理。然後,為了進行一行行的切割,我們會自己做這樣的機制,先將一段段讀取出來的檔案內容放到緩衝區(Buffer),然後找到換行字元進行切斷取出,然後再繼續讀取檔案,重複這樣的過程直到檔案結尾。

的確,實做這樣的機制有點麻煩,所以其實能利用 Node.js 現成內建的核心模組 Readline 來做到切割資料流中一行字串的工作。因為常見的 Readline 用法都是拿來做終端機字元模式下的命令列操作,所以許多人沒有想到可以這樣使用 Readline。作法其實很簡單,就把 Readline 的 input 從標準輸入(Standard Input)換成我們的檔案讀取資料流就可以。

完整做法如下:
var fs = require('fs'); var readline = require('readline'); // 建立檔案讀取資料流 var inputStream = fs.createReadStream('example.txt'); // 將讀取資料流導入 Readline 進行處理 var lineReader = readline.createInterface({ input:…