有學過Java的人應該會知道StringBuilder或是StringBuffer這兩個在java.lang套件下的類別,常被用來處理需要一直被改變內容的字串。由於Java程式語言一個字串(String)有著不可變物件(Immutable Object)的特性,如果直接使用String類別來進行字串的處理,在改變字串的過程中,每次字串內容的變化將會產生出新的String物件來表示,也導致其效能不是很好,需要使用內建的StringBuilder或是StringBuffer這兩個類別來專門處理可變的字串,處理完後再使用其toString()方法將最終結果建立為字串物件。那麼既然Java需要使用StringBuilder或是StringBuffer來處理字串,基於JavaScript的Node.js也需要嗎?



Node.js使用Chrome的V8 JavaScript引擎來執行JavaScript程式,而實際上V8實作的字串功能,效能也已經可以說是極好的了,所以官方也無內建StringBuilder或是StringBuffer之類的模組。也就是說,在Node.js中,可以不需要特別使用StringBuilder或是StringBuffer來提升字串效能。

然而,StringBuilder或是StringBuffer帶來的價值僅僅只是字串處理效能而已嗎?不!Java的StringBuilderStringBuffer皆自帶了常用的增、刪、改、查之方法,可以大幅度地減少重複撰寫的程式。

舉例來說,若要將某一段字串插入到某個字串中,如果不使用StringBuilderStringBuffer,寫出來的Java程式看起來會像這樣:

String text = "This is apple.";
String insertString = "an ";
text = text.substring(0, 8) + insertString + text.substring(8);
System.out.println(text); // This is an apple.

利用字串物件的substring方法將text字串變數所表示的字串切割成頭、尾兩個部份,再用+運算子將他們串接,並在兩者中間插入新字串。由這一系列的動作來完成我們所謂的「字串插入」。

但如果使用StringBuilderStringBuffer,這個Java程式會變得十分簡單,如下:

StringBuilder text = new StringBuilder("This is apple.");
String insertString = "an ";
text.insert(8, insertString);
System.out.println(text.toString());

直接使用StringBuilder物件的insert方法,將指定字串插入至指定位置。程式看起來也更容易閱讀。

當然,程式設計者也可以將使用substring方法來完成「字串插入」的動作自己另外寫成方法來呼叫,但這樣做的話,之後字串處理的過程中,如果遇到需要刪除或是取代的時候不也要再實作一次嗎?直接使用StringBuilderStringBuffer會省事很多。

從剛剛就一直在說StringBuilderStringBuffer,這兩者究竟差在哪裡呢?其實,這兩者的用法完全一樣,只差在StringBuilder類別在多執行緒的條件下是不安全的,而StringBuffer類別則是擁有thread-safe的特性。若無多執行緒需求的話,使用StringBuilder類型會比較好,因為不用花費效能去處理同步問題。

Java的內容講了一堆,回到Node.js的主題上吧!Node.js是單執行緒的結構,也就是說,同一時間不會有CPU同時執行某段程式的問題。若要在Node.js上實作如Java內建的StringBuilderStringBuffer,可以不必考慮thread-safe的問題,因此只需要有StringBuilder就好。

node-stringbuilder

node-stringbuilder是一個使用Node.js 8之後才支援的N-API所開發的模組,使用C語言和UTF-16編碼,在一段長度可變的記憶體內來處理字串的相關操作,支援stringBufferReadStreamnumberboolean和其他物件的輸入。同時它也內建字串搜尋的功能,使用「Boyer-Moore-MagicLen」演算法。且每個物件皆能複製出新的獨立物件來進行不一樣的字串處理。

npmjs.com

npm 安裝指令

npm install node-stringbuilder

使用方法

初始化

使用require函數來引入node-stringbuilder模組。

const StringBuilder = require("node-stringbuilder");

接著使用new運算子,或是使用模組的from方法來建立StringBuilder物件。

const sb1 = new StringBuilder();
// or 
const sb2 = StringBuilder.from();

建構參數可以傳入預設的文字內容和長度容量。

const sb = StringBuilder.from("First", 4096);

長度容量可以分成好幾個區塊,每個長度容量的區塊是128(個字元)。長度容量可以區塊為單位進行擴充或是縮小。

// To expand
const newCapacity = 65536;
sb.expandCapacity(newCapacity);
// To shrink
sb.shrinkCapacity();

當有文字加入至StringBuilder物件中時,StringBuilder會去檢查它的長度容量是否足夠容納這新進來的文字。如果不行,它就會自動進行擴展,當然,擴展長度容量的時候會有一些延遲,如果頻繁地執行擴展的動作,甚至會拖慢程式的運行速度。因此,如果程式設計者可以預測這個StringBuilder物件大概會存到多長的文字,建議在產生StringBuilder物件的階段就先行設定。

串接(Append)

串接文字至原本的文字之後。

sb.append("string").append(123).append(false).append(fs.createReadStream(path));

串接文字至原本的文字之後,並進行換行。

sb.appendLine("string");

串接文字至原本的文字之後,並重複進行數次。

sb.appendRepeat("string", 3);

非同步(asnyc)的方式串接檔案內容至原本的文字之後。

await sb.appendReadStream(fs.createReadStream(path));
插入(Insert)

插入文字至原本文字中的任意位置。

sb.insert("string"); // To the head.
sb.insert(5, "string");
取代(Replace)

取代原本文字中的指定位置範圍的某段文字。

sb.replace(4, 15, "string");

取代部份原本文字中已存在的子字串。

sb.replacePattern("old", "new");
sb.replacePattern(
    "old",
    "new",
    offset,
    limit
);

取代所有原本文字中已存在的子字串。

sb.replaceAll("old", "new");
刪除(Delete)

刪除原本文字中的指定位置範圍的某段文字。

sb.delete(4, 15);

刪除原本文字中指定索引位置的字元。

sb.deleteCharAt(4);

清空StringBuilder物件,但保留現有的長度容量。

sb.clear();
子字串(Substring)

僅保留原本文字中的指定位置範圍的某段文字。

sb.substring(1, 5); // input the start and end index 
sb.substr(1, 5); // input the start index and length 
反轉(Reverse)
sb.reverse();
大小寫轉換(Upper/Lower Case)

將文字轉換成大寫或是小寫。

sb.upperCase();
sb.lowerCase();
修剪(Trim)

移除文字前後所有空白、換行相關的字元。

sb.trim();
重複(Repeat)

將目前的文字重複數次。

sb.repeat(1);
擴展長度容量

擴展StringBuilder物件的長度容量。

sb.expandCapacity(4096).append("string");

擴展StringBuilder物件的長度容量,並取得擴展之後的容量。

const capacity = sb.expandCapacity(4096, true);
縮小長度容量

根據目前的文字縮小StringBuilder物件的長度容量。

sb.shrinkCapacity().clone().append("string");

根據目前的文字縮小StringBuilder物件的長度容量,並取得縮小之後的容量。

const capacity = sb.shrinkCapacity(true);
取得目前文字長度
const length = sb.length();
取得長度容量
const capacity = sb.capacity();
計算字數
const words = sb.count();
建立字串

StringBuilder物件內指定位置範圍的文字轉成字串。

const str = sb.toString(4, 10);

StringBuilder物件內指定位置範圍的文字轉成UTF-8編碼的Buffer

const buffer = sb.toBuffer(4, 10);

StringBuilder物件內的文字都轉成字串。

const str = sb.toString(4, 10);

StringBuilder物件內的文字都轉成UTF-8編碼的Buffer

const buffer = sb.toBuffer();

取得StringBuilder物件內指定索引位置的文字字元。

const c = sb.charAt(4);
字串搜尋

從文字前端開始搜尋子字串。

const indexArray = sb.indexOf("string");
const indexArray2 = sb.indexOf("string", offset, limit);

從文字前端開始搜尋符合正規表示式的子字串。

const indexArray = sb.indexOf(/string/g);

從文字尾端開始搜尋子字串。

const indexArray = sb.lastIndexOf("string");
字串比對

比對兩文字是否完全相等。

const equal = sb.equals("string");

比對兩文字是否在忽略大小寫的條件下相等。

const equal = sb.equalsIgnoreCase("string");

比對文字是否以某字串開頭或結尾。

const start = sb.startsWith("string");
const end = sb.endsWith("string");
複製StringBuilder物件
const newSB = sb.clone();

效能的影響

由於使用C語言這樣的底層程式實作模組的關係,node-stringbuilder的效能大概只有在「串接(Append)」和「字串比對(完全相等)」時會比JS程式還要來的差。雖說如此,這兩者卻也是平常開發產品時最常用的功能,因此如果不需要進行除了「串接(Append)」和「字串比對(完全相等)」之外,其他的字串操作的話,直接使用JavaScript程式原生的方式去產生字串是比較好的。