2024 年 1 月 17 日

字串

在 JavaScript 中,文字資料儲存在字串中。沒有單一字元的個別型態。

字串的內部格式永遠是 UTF-16,與頁面編碼無關。

引號

讓我們回顧一下引號的種類。

字串可以用單引號、雙引號或反引號括起來

let single = 'single-quoted';
let double = "double-quoted";

let backticks = `backticks`;

單引號和雙引號基本上是相同的。然而,反引號允許我們將任何表達式嵌入字串中,方法是將其包裝在 ${…}

function sum(a, b) {
  return a + b;
}

alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.

使用反引號的另一個優點是它們允許字串跨越多行

let guestList = `Guests:
 * John
 * Pete
 * Mary
`;

alert(guestList); // a list of guests, multiple lines

看起來很自然,對吧?但單引號或雙引號不這樣做。

如果我們使用它們並嘗試使用多行,將會出現錯誤

let guestList = "Guests: // Error: Unexpected token ILLEGAL
  * John";

單引號和雙引號來自語言創建的古代,當時沒有考慮到多行字串的需要。反引號出現得晚得多,因此更通用。

反引號還允許我們在第一個反引號之前指定一個「範本函數」。語法為:func`string`。函數 func 會自動呼叫,接收字串和嵌入式表達式,並可以處理它們。此功能稱為「標記範本」,很少見,但您可以在 MDN 中閱讀有關它的資訊:範本字面

特殊字元

仍然可以使用單引號和雙引號建立多行字串,方法是使用一個稱為「換行字元」的字元,寫成 \n,表示換行

let guestList = "Guests:\n * John\n * Pete\n * Mary";

alert(guestList); // a multiline list of guests, same as above

作為一個更簡單的範例,這兩行是相等的,只是寫法不同

let str1 = "Hello\nWorld"; // two lines using a "newline symbol"

// two lines using a normal newline and backticks
let str2 = `Hello
World`;

alert(str1 == str2); // true

還有其他較不常見的特殊字元

字元 說明
\n 換行
\r 在 Windows 文字檔案中,兩個字元 \r\n 的組合表示換行,而在非 Windows 作業系統中,它只是 \n。這是出於歷史原因,大多數 Windows 軟體也理解 \n
\'\"\` 引號
\\ 反斜線
\t 跳格
\b\f\v 退格、換頁、垂直跳格 - 提及以求完整,來自舊時代,現在不使用了(您可以立即忘記它們)。

如您所見,所有特殊字元都以反斜線字元 \ 開頭。它也稱為「跳脫字元」。

由於它很特別,如果我們需要在字串中顯示實際的反斜線 \,我們需要將它加倍

alert( `The backslash: \\` ); // The backslash: \

所謂的「跳脫」引號 \'\"\` 用於將引號插入到相同引號的字串中。

例如

alert( 'I\'m the Walrus!' ); // I'm the Walrus!

如您所見,我們必須在內部引號前加上反斜線 \',否則它將表示字串結束。

當然,只有與封閉引號相同的引號需要跳脫。因此,作為一個更優雅的解決方案,我們可以改用雙引號或反引號

alert( "I'm the Walrus!" ); // I'm the Walrus!

除了這些特殊字元之外,還有一個 Unicode 碼 \u… 的特殊表示法,它很少使用,並在關於 Unicode 的選用章節中介紹。

字串長度

length 屬性具有字串長度

alert( `My\n`.length ); // 3

請注意,\n 是單一的「特殊」字元,因此長度確實是 3

length 是屬性

具備其他語言背景的人們有時會誤植為呼叫 str.length(),而非僅呼叫 str.length。這無法運作。

請注意,str.length 是數字屬性,而非函式。無須在其後加上括號。非 .length(),而是 .length

存取字元

若要取得位置 pos 的字元,請使用方括號 [pos] 或呼叫方法 str.at(pos)。第一個字元從零位置開始

let str = `Hello`;

// the first character
alert( str[0] ); // H
alert( str.at(0) ); // H

// the last character
alert( str[str.length - 1] ); // o
alert( str.at(-1) );

如您所見,.at(pos) 方法有允許負位置的優點。如果 pos 為負數,則會從字串的結尾開始計算。

因此,.at(-1) 表示最後一個字元,而 .at(-2) 表示其前一個字元,依此類推。

方括號總是會傳回負數索引的 undefined,例如

let str = `Hello`;

alert( str[-2] ); // undefined
alert( str.at(-2) ); // l

我們也可以使用 for..of 遍歷字元

for (let char of "Hello") {
  alert(char); // H,e,l,l,o (char becomes "H", then "e", then "l" etc)
}

字串是不可變的

字串無法在 JavaScript 中變更。無法變更字元。

讓我們嘗試看看它無法運作

let str = 'Hi';

str[0] = 'h'; // error
alert( str[0] ); // doesn't work

常見的解決方法是建立一個全新的字串,並將其指定給 str,而非舊字串。

例如

let str = 'Hi';

str = 'h' + str[1]; // replace the string

alert( str ); // hi

在以下各節中,我們將看到更多此類範例。

變更大小寫

方法 toLowerCase()toUpperCase() 會變更大小寫

alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface

或者,如果我們想要將單一字元轉換為小寫

alert( 'Interface'[0].toLowerCase() ); // 'i'

搜尋子字串

有多種方法可以在字串中尋找子字串。

str.indexOf

第一個方法是 str.indexOf(substr, pos)

它會從指定的 pos 位置開始,在 str 中尋找 substr,並傳回找到配對項的位置或 -1(如果找不到任何項目)。

例如

let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0, because 'Widget' is found at the beginning
alert( str.indexOf('widget') ); // -1, not found, the search is case-sensitive

alert( str.indexOf("id") ); // 1, "id" is found at the position 1 (..idget with id)

選擇性的第二個參數允許我們從指定的位置開始搜尋。

例如,"id" 的第一次出現位於 1 位置。若要尋找下一次出現,讓我們從位置 2 開始搜尋

let str = 'Widget with id';

alert( str.indexOf('id', 2) ) // 12

如果我們對所有出現感興趣,我們可以在迴圈中執行 indexOf。每次新的呼叫都會使用前一次配對項之後的位置

let str = 'As sly as a fox, as strong as an ox';

let target = 'as'; // let's look for it

let pos = 0;
while (true) {
  let foundPos = str.indexOf(target, pos);
  if (foundPos == -1) break;

  alert( `Found at ${foundPos}` );
  pos = foundPos + 1; // continue the search from the next position
}

可以將相同的演算法簡化

let str = "As sly as a fox, as strong as an ox";
let target = "as";

let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
  alert( pos );
}
str.lastIndexOf(substr, position)

還有一個類似的 str.lastIndexOf(substr, position) 方法,它會從字串的結尾開始搜尋到其開頭。

它會以相反的順序列出出現的項目。

indexOfif 測試中會造成一點小不便。我們不能像這樣將它放入 if

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("We found it"); // doesn't work!
}

上述範例中的 alert 沒有顯示,因為 str.indexOf("Widget") 傳回 0(表示它在起始位置找到匹配項)。沒錯,但 if0 視為 false

因此,我們實際上應該檢查 -1,如下所示

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("We found it"); // works now!
}

includes、startsWith、endsWith

較新的方法 str.includes(substr, pos) 會傳回 true/false,視 str 是否包含 substr

如果我們需要測試匹配項,但不需要其位置,這是正確的選擇

alert( "Widget with id".includes("Widget") ); // true

alert( "Hello".includes("Bye") ); // false

str.includes 的第二個選用引數是開始搜尋的位置

alert( "Widget".includes("id") ); // true
alert( "Widget".includes("id", 3) ); // false, from position 3 there is no "id"

方法 str.startsWithstr.endsWith 會執行它們所述的動作

alert( "Widget".startsWith("Wid") ); // true, "Widget" starts with "Wid"
alert( "Widget".endsWith("get") ); // true, "Widget" ends with "get"

取得子字串

JavaScript 中有 3 個方法可取得子字串:substringsubstrslice

str.slice(start [, end])

傳回從 start 到(但不包含)end 的字串部分。

例如

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin', the substring from 0 to 5 (not including 5)
alert( str.slice(0, 1) ); // 's', from 0 to 1, but not including 1, so only character at 0

如果沒有第二個引數,則 slice 會一直到字串的結尾

let str = "stringify";
alert( str.slice(2) ); // 'ringify', from the 2nd position till the end

start/end 也可能為負值。它們表示位置從字串結尾計算

let str = "stringify";

// start at the 4th position from the right, end at the 1st from the right
alert( str.slice(-4, -1) ); // 'gif'
str.substring(start [, end])

傳回 startend(不包含 end之間的字串部分。

這幾乎與 slice 相同,但它允許 start 大於 end(在這種情況下,它只會交換 startend 的值)。

例如

let str = "stringify";

// these are same for substring
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// ...but not for slice:
alert( str.slice(2, 6) ); // "ring" (the same)
alert( str.slice(6, 2) ); // "" (an empty string)

slice 不同,不支援負引數,它們會視為 0

str.substr(start [, length])

傳回從 start 開始,具有指定 length 的字串部分。

與前述方法不同,此方法允許我們指定 length,而不是結束位置

let str = "stringify";
alert( str.substr(2, 4) ); // 'ring', from the 2nd position get 4 characters

第一個引數可以為負值,以從結尾計算

let str = "stringify";
alert( str.substr(-4, 2) ); // 'gi', from the 4th position get 2 characters

此方法位於語言規範的 附錄 B 中。這表示只有瀏覽器託管的 Javascript 引擎應該支援它,不建議使用它。實際上,它在各處都受到支援。

讓我們回顧這些方法,以避免混淆

方法 選擇… 負數
slice(start, end) startend(不包含 end 允許負數
substring(start, end) startend 之間(不包含 end 負值表示 0
substr(start, length) start 取得 length 個字元 允許負 start
選擇哪一個?

所有這些都可以完成工作。正式來說,substr 有個小缺點:它不是在 JavaScript 核心規格中描述,而是在附錄 B 中,其中涵蓋了主要出於歷史原因而存在的僅瀏覽器功能。因此,非瀏覽器環境可能無法支援它。但在實務上,它在所有地方都可以使用。

在其他兩個變體中,slice 稍微靈活一些,它允許負數引數,而且寫起來較短。

因此,對於實際用途,只要記住 slice 就足夠了。

比較字串

正如我們從章節 比較 中所知,字串會按照字母順序逐字元比較。

儘管如此,還是有些奇怪的地方。

  1. 小寫字母永遠大於大寫字母

    alert( 'a' > 'Z' ); // true
  2. 帶有變音符號的字母「不按順序」

    alert( 'Österreich' > 'Zealand' ); // true

    如果我們對這些國家名稱進行排序,這可能會導致奇怪的結果。通常人們會希望 Zealand 在清單中出現在 Österreich 之後。

要了解發生了什麼事,我們應該知道 Javascript 中的字串使用 UTF-16 編碼。也就是說:每個字元都有對應的數字代碼。

有特殊的方法可以取得代碼的字元,反之亦然

str.codePointAt(pos)

傳回一個十進位數字,代表位置 pos 的字元代碼

// different case letters have different codes
alert( "Z".codePointAt(0) ); // 90
alert( "z".codePointAt(0) ); // 122
alert( "z".codePointAt(0).toString(16) ); // 7a (if we need a hexadecimal value)
String.fromCodePoint(code)

根據數字 code 建立一個字元

alert( String.fromCodePoint(90) ); // Z
alert( String.fromCodePoint(0x5a) ); // Z (we can also use a hex value as an argument)

現在讓我們透過建立一個字串來檢視代碼為 65..220 的字元(拉丁字母和一些額外的字元)

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// Output:
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„
// ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜ

看到了嗎?大寫字元排在前面,然後是幾個特殊字元,再接著是小寫字元,而 Ö 則在輸出的尾端。

現在很明顯為什麼 a > Z 了。

字元是根據其數字代碼進行比較。較大的代碼表示較大的字元。a 的代碼(97)大於 Z 的代碼(90)。

  • 所有小寫字母都出現在大寫字母之後,因為它們的代碼較大。
  • Ö 這樣的某些字母與主字母表不同。在此,它的代碼大於 az 之間的任何代碼。

正確的比較

執行字串比較的「正確」演算法比看起來複雜,因為不同的語言有不同的字母表。

因此,瀏覽器需要知道要比較的語言。

幸運的是,現代瀏覽器支援國際化標準 ECMA-402

它提供了一個特殊的方法來比較不同語言的字串,遵循它們的規則。

呼叫 str.localeCompare(str2) 會傳回一個整數,表示根據語言規則,str 是小於、等於或大於 str2

  • 如果 str 小於 str2,則傳回負數。
  • 如果 str 大於 str2,則傳回正數。
  • 如果它們相等,則傳回 0

例如

alert( 'Österreich'.localeCompare('Zealand') ); // -1

此方法實際上還有兩個額外的參數,在 文件 中指定,它允許指定語言(預設從環境中取得,字母順序取決於語言)並設定其他規則,例如大小寫敏感或 "a""á" 是否應視為相同等。

摘要

  • 有 3 種引號。反引號允許字串跨越多行並內嵌表達式 ${…}
  • 我們可以使用特殊字元,例如換行 \n
  • 要取得字元,請使用:[]at 方法。
  • 要取得子字串,請使用:slicesubstring
  • 要將字串轉換為小寫/大寫,請使用:toLowerCase/toUpperCase
  • 要尋找子字串,請使用:indexOf,或 includes/startsWith/endsWith 進行簡單檢查。
  • 要根據語言比較字串,請使用:localeCompare,否則它們將按字元碼比較。

字串中有許多其他有用的方法

  • str.trim() – 從字串的開頭和結尾移除(“修剪”)空格。
  • str.repeat(n) – 重複字串 n 次。
  • …更多內容請參閱 手冊

字串也有使用正規表示法進行搜尋/取代的方法。但那是個大主題,因此在單獨的教學單元 正規表示法 中說明。

此外,現在重要的是要知道字串是基於 Unicode 編碼,因此比較會有問題。章節 Unicode、字串內部 中有更多關於 Unicode 的資訊。

任務

重要性:5

撰寫一個函式 `ucFirst(str)`,回傳字串 `str`,且第一個字元為大寫,例如

ucFirst("john") == "John";

開啟一個沙盒進行測試。

我們無法「取代」第一個字元,因為 JavaScript 中的字串是不可變的。

但是,我們可以根據現有的字串建立一個新的字串,且第一個字元為大寫

let newStr = str[0].toUpperCase() + str.slice(1);

不過,有一個小問題。如果 `str` 為空,則 `str[0]` 為 `undefined`,且由於 `undefined` 沒有 `toUpperCase()` 方法,因此我們會收到錯誤訊息。

最簡單的解決方法是針對空字串新增測試,如下所示

function ucFirst(str) {
  if (!str) return str;

  return str[0].toUpperCase() + str.slice(1);
}

alert( ucFirst("john") ); // John

在沙盒中開啟包含測試的解法。

重要性:5

撰寫一個函式 `checkSpam(str)`,如果 `str` 包含「viagra」或「XXX」,則回傳 `true`,否則回傳 `false`。

此函式必須不區分大小寫

checkSpam('buy ViAgRA now') == true
checkSpam('free xxxxx') == true
checkSpam("innocent rabbit") == false

開啟一個沙盒進行測試。

若要讓搜尋不區分大小寫,我們將字串轉換為小寫,然後再進行搜尋

function checkSpam(str) {
  let lowerStr = str.toLowerCase();

  return lowerStr.includes('viagra') || lowerStr.includes('xxx');
}

alert( checkSpam('buy ViAgRA now') );
alert( checkSpam('free xxxxx') );
alert( checkSpam("innocent rabbit") );

在沙盒中開啟包含測試的解法。

重要性:5

建立一個函式 `truncate(str, maxlength)`,檢查 `str` 的長度,如果超過 `maxlength`,則將 `str` 的結尾取代為省略號字元「…」以使其長度等於 `maxlength`。

函式的結果應為已截斷(如果需要)的字串。

例如

truncate("What I'd like to tell on this topic is:", 20) == "What I'd like to te…"

truncate("Hi everyone!", 20) == "Hi everyone!"

開啟一個沙盒進行測試。

最大長度必須為 `maxlength`,因此我們需要將其縮短一點,以留出省略號的空間。

請注意,省略號實際上只有一個 Unicode 字元。這不是三個點。

function truncate(str, maxlength) {
  return (str.length > maxlength) ?
    str.slice(0, maxlength - 1) + '…' : str;
}

在沙盒中開啟包含測試的解法。

重要性:4

我們有一個成本,格式為「$120」。也就是說:美元符號在前,然後是數字。

建立一個函式 `extractCurrencyValue(str)`,從此類字串中擷取數字值並回傳。

範例

alert( extractCurrencyValue('$120') === 120 ); // true

開啟一個沙盒進行測試。

function extractCurrencyValue(str) {
  return +str.slice(1);
}

在沙盒中開啟包含測試的解法。

教學課程地圖

留言

留言前請先閱讀此內容…
  • 如果您有改善建議,請 提交 GitHub 議題 或發起 pull request,而不要留言。
  • 如果您無法理解文章中的內容,請詳細說明。
  • 若要插入幾行程式碼,請使用 `<code>` 標籤,若要插入多行,請將其包覆在 `<pre>` 標籤中,若要插入超過 10 行,請使用沙盒(plnkrjsbincodepen…)