Javascript 字符串

2023-02-17 10:44 更新

在 JavaScript 中,文本數(shù)據(jù)被以字符串形式存儲(chǔ),單個(gè)字符沒(méi)有單獨(dú)的類(lèi)型。

字符串的內(nèi)部格式始終是 UTF-16,它不依賴(lài)于頁(yè)面編碼。

引號(hào)(Quotes)

讓我們回憶一下引號(hào)的種類(lèi)。

字符串可以包含在單引號(hào)、雙引號(hào)或反引號(hào)中:

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

let backticks = `backticks`;

單引號(hào)和雙引號(hào)基本相同。但是,反引號(hào)允許我們通過(guò) ${…} 將任何表達(dá)式嵌入到字符串中:

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

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

使用反引號(hào)的另一個(gè)優(yōu)點(diǎn)是它們?cè)试S字符串跨行:

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

alert(guestList); // 客人清單,多行

看起來(lái)很自然,不是嗎?但是單引號(hào)和雙引號(hào)可不能這樣做。

如果我們使用單引號(hào)或雙引號(hào)來(lái)實(shí)現(xiàn)字符串跨行的話(huà),則會(huì)出現(xiàn)錯(cuò)誤:

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

單引號(hào)和雙引號(hào)來(lái)自語(yǔ)言創(chuàng)建的的古老時(shí)代,當(dāng)時(shí)沒(méi)有考慮到多行字符串的需要。反引號(hào)出現(xiàn)較晚,因此更通用。

反引號(hào)還允許我們?cè)诘谝粋€(gè)反引號(hào)之前指定一個(gè)“模版函數(shù)”。語(yǔ)法是:func`string`。函數(shù) func 被自動(dòng)調(diào)用,接收字符串和嵌入式表達(dá)式,并處理它們。你可以在 docs 中閱讀更多關(guān)于它們的信息。這叫做 “tagged templates”。此功能可以更輕松地將字符串包裝到自定義模版或其他函數(shù)中,但這很少使用。

特殊字符

我們?nèi)匀豢梢酝ㄟ^(guò)使用“換行符(newline character)”,以支持使用單引號(hào)和雙引號(hào)來(lái)創(chuàng)建跨行字符串。換行符寫(xiě)作 \n,用來(lái)表示換行:

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

alert(guestList); // 一個(gè)多行的客人列表

例如,這兩行描述的是一樣的,只是書(shū)寫(xiě)方式不同:

let str1 = "Hello\nWorld"; // 使用“換行符”創(chuàng)建的兩行字符串

// 使用反引號(hào)和普通的換行創(chuàng)建的兩行字符串
let str2 = `Hello
World`;

alert(str1 == str2); // true

還有其他不常見(jiàn)的“特殊”字符。

這是完整列表:

字符 描述
\n 換行
\r 在 Windows 文本文件中,兩個(gè)字符 \r\n 的組合代表一個(gè)換行。而在非 Windows 操作系統(tǒng)上,它就是 \n。這是歷史原因造成的,大多數(shù)的 Windows 軟件也理解 \n。
\'\" 引號(hào)
\\ 反斜線(xiàn)
\t 制表符
\b\f\v 退格,換頁(yè),垂直標(biāo)簽 —— 為了兼容性,現(xiàn)在已經(jīng)不使用了。
\xXX 具有給定十六進(jìn)制 Unicode XX 的 Unicode 字符,例如:'\x7A' 和 'z' 相同。
\uXXXX 以 UTF-16 編碼的十六進(jìn)制代碼 XXXX 的 Unicode 字符,例如 \u00A9 —— 是版權(quán)符號(hào) ? 的 Unicode。它必須正好是 4 個(gè)十六進(jìn)制數(shù)字。
\u{X…XXXXXX}(1 到 6 個(gè)十六進(jìn)制字符) 具有給定 UTF-32 編碼的 Unicode 符號(hào)。一些罕見(jiàn)的字符用兩個(gè) Unicode 符號(hào)編碼,占用 4 個(gè)字節(jié)。這樣我們就可以插入長(zhǎng)代碼了。

Unicode 示例:

alert( "\u00A9" ); // ?
alert( "\u{20331}" ); // 佫,罕見(jiàn)的中國(guó)象形文字(長(zhǎng) Unicode)
alert( "\u{1F60D}" ); // ,笑臉?lè)?hào)(另一個(gè)長(zhǎng) Unicode)

所有的特殊字符都以反斜杠字符 \ 開(kāi)始。它也被稱(chēng)為“轉(zhuǎn)義字符”。

如果我們想要在字符串中插入一個(gè)引號(hào),我們也會(huì)使用它。

例如:

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

正如你所看到的,我們必須在內(nèi)部引號(hào)前加上反斜杠 \',否則它將表示字符串結(jié)束。

當(dāng)然,只有與外部閉合引號(hào)相同的引號(hào)才需要轉(zhuǎn)義。因此,作為一個(gè)更優(yōu)雅的解決方案,我們可以改用雙引號(hào)或者反引號(hào):

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

注意反斜杠 \ 在 JavaScript 中用于正確讀取字符串,然后消失。內(nèi)存中的字符串沒(méi)有 \。你從上述示例中的 alert 可以清楚地看到這一點(diǎn)。

但是如果我們需要在字符串中顯示一個(gè)實(shí)際的反斜杠 \ 應(yīng)該怎么做?

我們可以這樣做,只需要將其書(shū)寫(xiě)兩次 \\

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

字符串長(zhǎng)度

length 屬性表示字符串長(zhǎng)度:

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

注意 \n 是一個(gè)單獨(dú)的“特殊”字符,所以長(zhǎng)度確實(shí)是 3。

?length ?是一個(gè)屬性

掌握其他編程語(yǔ)言的人,有時(shí)會(huì)錯(cuò)誤地調(diào)用 str.length() 而不是 str.length。這是行不通的。

請(qǐng)注意 str.length 是一個(gè)數(shù)字屬性,而不是函數(shù)。后面不需要添加括號(hào)。

訪問(wèn)字符

要獲取在 pos 位置的一個(gè)字符,可以使用方括號(hào) [pos] 或者調(diào)用 str.charAt(pos) 方法。第一個(gè)字符從零位置開(kāi)始:

let str = `Hello`;

// 第一個(gè)字符
alert( str[0] ); // H
alert( str.charAt(0) ); // H

// 最后一個(gè)字符
alert( str[str.length - 1] ); // o

方括號(hào)是獲取字符的一種現(xiàn)代化方法,而 charAt 是歷史原因才存在的。

它們之間的唯一區(qū)別是,如果沒(méi)有找到字符,[] 返回 undefined,而 charAt 返回一個(gè)空字符串:

let str = `Hello`;

alert( str[1000] ); // undefined
alert( str.charAt(1000) ); // ''(空字符串)

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

for (let char of "Hello") {
  alert(char); // H,e,l,l,o(char 變?yōu)?"H",然后是 "e",然后是 "l" 等)
}

字符串是不可變的

在 JavaScript 中,字符串不可更改。改變字符是不可能的。

我們證明一下為什么不可能:

let str = 'Hi';

str[0] = 'h'; // error
alert( str[0] ); // 無(wú)法運(yùn)行

通常的解決方法是創(chuàng)建一個(gè)新的字符串,并將其分配給 str 而不是以前的字符串。

例如:

let str = 'Hi';

str = 'h' + str[1];  // 替換字符串

alert( str ); // hi

在接下來(lái)的章節(jié),我們將看到更多相關(guān)示例。

改變大小寫(xiě)

toLowerCase() 和 toUpperCase() 方法可以改變大小寫(xiě):

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

或者我們想要使一個(gè)字符變成小寫(xiě):

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

查找子字符串

在字符串中查找子字符串有很多種方法。

str.indexOf

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

它從給定位置 pos 開(kāi)始,在 str 中查找 substr,如果沒(méi)有找到,則返回 -1,否則返回匹配成功的位置。

例如:

let str = 'Widget with id';

alert( str.indexOf('Widget') ); // 0,因?yàn)?'Widget' 一開(kāi)始就被找到
alert( str.indexOf('widget') ); // -1,沒(méi)有找到,檢索是大小寫(xiě)敏感的

alert( str.indexOf("id") ); // 1,"id" 在位置 1 處(……idget 和 id)

可選的第二個(gè)參數(shù)允許我們從一個(gè)給定的位置開(kāi)始檢索。

例如,"id" 第一次出現(xiàn)的位置是 1。查詢(xún)下一個(gè)存在位置時(shí),我們從 2 開(kāi)始檢索:

let str = 'Widget with id';

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

如果我們對(duì)所有存在位置都感興趣,可以在一個(gè)循環(huán)中使用 indexOf。每一次新的調(diào)用都發(fā)生在上一匹配位置之后:

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

let target = 'as'; // 這是我們要查找的目標(biāo)

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

  alert( `Found at ${foundPos}` );
  pos = foundPos + 1; // 繼續(xù)從下一個(gè)位置查找
}

相同的算法可以簡(jiǎn)寫(xiě):

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, pos)

還有一個(gè)類(lèi)似的方法 str.lastIndexOf(substr, position),它從字符串的末尾開(kāi)始搜索到開(kāi)頭。

它會(huì)以相反的順序列出這些事件。

在 if 測(cè)試中 indexOf 有一點(diǎn)不方便。我們不能像這樣把它放在 if 中:

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("We found it"); // 不工作!
}

上述示例中的 alert 不會(huì)顯示,因?yàn)?nbsp;str.indexOf("Widget") 返回 0(意思是它在起始位置就查找到了匹配項(xiàng))。是的,但是 if 認(rèn)為 0 表示 false。

因此我們應(yīng)該檢查 -1,像這樣:

let str = "Widget with id";

if (str.indexOf("Widget") != -1) {
    alert("We found it"); // 現(xiàn)在工作了!
}

按位(bitwise)NOT 技巧

這里使用的一個(gè)老技巧是 bitwise NOT ~ 運(yùn)算符。它將數(shù)字轉(zhuǎn)換為 32-bit 整數(shù)(如果存在小數(shù)部分,則刪除小數(shù)部分),然后對(duì)其二進(jìn)制表示形式中的所有位均取反。

實(shí)際上,這意味著一件很簡(jiǎn)單的事兒:對(duì)于 32-bit 整數(shù),~n 等于 -(n+1)。

例如:

alert( ~2 ); // -3,和 -(2+1) 相同
alert( ~1 ); // -2,和 -(1+1) 相同
alert( ~0 ); // -1,和 -(0+1) 相同
alert( ~-1 ); // 0,和 -(-1+1) 相同

正如我們看到這樣,只有當(dāng) n == -1 時(shí),~n 才為零(適用于任何 32-bit 帶符號(hào)的整數(shù) n)。

因此,僅當(dāng) indexOf 的結(jié)果不是 -1 時(shí),檢查 if ( ~str.indexOf("...") ) 才為真。換句話(huà)說(shuō),當(dāng)有匹配時(shí)。

人們用它來(lái)簡(jiǎn)寫(xiě) indexOf 檢查:

let str = "Widget";

if (~str.indexOf("Widget")) {
  alert( 'Found it!' ); // 正常運(yùn)行
}

通常不建議以非顯而易見(jiàn)的方式使用語(yǔ)言特性,但這種特殊技巧在舊代碼中仍被廣泛使用,所以我們應(yīng)該理解它。

只要記?。?code>if (~str.indexOf(...)) 讀作 “if found”。

確切地說(shuō),由于 ~ 運(yùn)算符將大數(shù)字截?cái)酁?32 位,因此存在給出 0 的其他數(shù)字,最小的數(shù)字是 ~4294967295=0。這使得這種檢查只有在字符串沒(méi)有那么長(zhǎng)的情況下才是正確的。

現(xiàn)在我們只會(huì)在舊的代碼中看到這個(gè)技巧,因?yàn)楝F(xiàn)代 JavaScript 提供了 .includes 方法(見(jiàn)下文)。

includes,startsWith,endsWith

更現(xiàn)代的方法 str.includes(substr, pos) 根據(jù) str 中是否包含 substr 來(lái)返回 true/false。

如果我們需要檢測(cè)匹配,但不需要它的位置,那么這是正確的選擇:

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

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

str.includes 的第二個(gè)可選參數(shù)是開(kāi)始搜索的起始位置:

alert( "Widget".includes("id") ); // true
alert( "Widget".includes("id", 3) ); // false, 從位置 3 開(kāi)始沒(méi)有 "id"

方法 str.startsWith 和 str.endsWith 的功能與其名稱(chēng)所表示的意思相同:

alert( "Widget".startsWith("Wid") ); // true,"Widget" 以 "Wid" 開(kāi)始
alert( "Widget".endsWith("get") ); // true,"Widget" 以 "get" 結(jié)束

獲取子字符串

JavaScript 中有三種獲取字符串的方法:substring、substr 和 slice。

?str.slice(start [, end])
?

返回字符串從 start 到(但不包括)end 的部分。

例如:

let str = "stringify";
alert( str.slice(0, 5) ); // 'strin',從 0 到 5 的子字符串(不包括 5)
alert( str.slice(0, 1) ); // 's',從 0 到 1,但不包括 1,所以只有在 0 處的字符

如果沒(méi)有第二個(gè)參數(shù),slice 會(huì)一直運(yùn)行到字符串末尾:

let str = "stringify";
alert( str.slice(2) ); // 從第二個(gè)位置直到結(jié)束

start/end 也有可能是負(fù)值。它們的意思是起始位置從字符串結(jié)尾計(jì)算:

let str = "stringify";

// 從右邊的第四個(gè)位置開(kāi)始,在右邊的第一個(gè)位置結(jié)束
alert( str.slice(-4, -1) ); // 'gif'

?str.substring(start [, end])
?

返回字符串從 start 到(但不包括)end 的部分。

這與 slice 幾乎相同,但它允許 start 大于 end。

例如:

let str = "stringify";

// 這些對(duì)于 substring 是相同的
alert( str.substring(2, 6) ); // "ring"
alert( str.substring(6, 2) ); // "ring"

// ……但對(duì) slice 是不同的:
alert( str.slice(2, 6) ); // "ring"(一樣)
alert( str.slice(6, 2) ); // ""(空字符串)

不支持負(fù)參數(shù)(不像 slice),它們被視為 0。

?str.substr(start [, length])
?

返回字符串從 start 開(kāi)始的給定 length 的部分。

與以前的方法相比,這個(gè)允許我們指定 length 而不是結(jié)束位置:

let str = "stringify";
alert( str.substr(2, 4) ); // 'ring',從位置 2 開(kāi)始,獲取 4 個(gè)字符

第一個(gè)參數(shù)可能是負(fù)數(shù),從結(jié)尾算起:

let str = "stringify";
alert( str.substr(-4, 2) ); // 'gi',從第 4 位獲取 2 個(gè)字符

我們回顧一下這些方法,以免混淆:

方法 選擇方式…… 負(fù)值參數(shù)
slice(start, end) 從 start 到 end(不含 end 允許
substring(start, end) 從 start 到 end(不含 end 負(fù)值被視為 0
substr(start, length) 從 start 開(kāi)始獲取長(zhǎng)為 length 的字符串 允許 start 為負(fù)數(shù)

使用哪一個(gè)?

它們都可用于獲取子字符串。正式一點(diǎn)來(lái)講,substr 有一個(gè)小缺點(diǎn):它不是在 JavaScript 核心規(guī)范中描述的,而是在附錄 B 中。附錄 B 的內(nèi)容主要是描述因歷史原因而遺留下來(lái)的僅瀏覽器特性。因此,理論上非瀏覽器環(huán)境可能無(wú)法支持 substr,但實(shí)際上它在別的地方也都能用。

相較于其他兩個(gè)變體,slice 稍微靈活一些,它允許以負(fù)值作為參數(shù)并且寫(xiě)法更簡(jiǎn)短。因此僅僅記住這三種方法中的 slice 就足夠了。

比較字符串

正如我們從 值的比較 一章中了解到的,字符串按字母順序逐字比較。

不過(guò),也有一些奇怪的地方。

  1. 小寫(xiě)字母總是大于大寫(xiě)字母:
  2. alert( 'a' > 'Z' ); // true
  3. 帶變音符號(hào)的字母存在“亂序”的情況:
  4. alert( '?sterreich' > 'Zealand' ); // true

    如果我們對(duì)這些國(guó)家名進(jìn)行排序,可能會(huì)導(dǎo)致奇怪的結(jié)果。通常,人們會(huì)期望 Zealand 在名單中的 ?sterreich 之后出現(xiàn)。

為了明白發(fā)生了什么,我們回顧一下在 JavaScript 中字符串的內(nèi)部表示。

所有的字符串都使用 UTF-16 編碼。即:每個(gè)字符都有對(duì)應(yīng)的數(shù)字代碼。有特殊的方法可以獲取代碼表示的字符,以及字符對(duì)應(yīng)的代碼。

?str.codePointAt(pos)
?

返回在 pos 位置的字符代碼 :

// 不同的字母有不同的代碼
alert( "z".codePointAt(0) ); // 122
alert( "Z".codePointAt(0) ); // 90

?String.fromCodePoint(code)
?

通過(guò)數(shù)字 code 創(chuàng)建字符

alert( String.fromCodePoint(90) ); // Z

我們還可以用 \u 后跟十六進(jìn)制代碼,通過(guò)這些代碼添加 Unicode 字符:

// 在十六進(jìn)制系統(tǒng)中 90 為 5a
alert( '\u005a' ); // Z

現(xiàn)在我們看一下代碼為 65..220 的字符(拉丁字母和一些額外的字符),方法是創(chuàng)建一個(gè)字符串:

let str = '';

for (let i = 65; i <= 220; i++) {
  str += String.fromCodePoint(i);
}
alert( str );
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~?????
// ?¢£¤¥|§¨?a??-?ˉ°±23′μ?·?1o?????àá??????èéê?ìí??D?òó???×?ùú?ü

看到?jīng)]?先是大寫(xiě)字符,然后是一些特殊字符,然后是小寫(xiě)字符,而 ? 幾乎是最后輸出。

現(xiàn)在很明顯為什么 a > Z。

字符通過(guò)數(shù)字代碼進(jìn)行比較。越大的代碼意味著字符越大。a(97)的代碼大于 Z(90)的代碼。

  • 所有小寫(xiě)字母追隨在大寫(xiě)字母之后,因?yàn)樗鼈兊拇a更大。
  • 一些像 ??? 的字母與主要字母表不同。這里,它的代碼比任何從 ?a? 到 ?z? 的代碼都要大。

正確的比較

執(zhí)行字符串比較的“正確”算法比看起來(lái)更復(fù)雜,因?yàn)椴煌Z(yǔ)言的字母都不相同。

因此瀏覽器需要知道要比較的語(yǔ)言。

幸運(yùn)的是,所有現(xiàn)代瀏覽器(IE10- 需要額外的庫(kù) Intl.JS) 都支持國(guó)際化標(biāo)準(zhǔn) ECMA-402

它提供了一種特殊的方法來(lái)比較不同語(yǔ)言的字符串,遵循它們的規(guī)則。

調(diào)用 str.localeCompare(str2) 會(huì)根據(jù)語(yǔ)言規(guī)則返回一個(gè)整數(shù),這個(gè)整數(shù)能指示字符串 str 在排序順序中排在字符串 str2 前面、后面、還是相同:

  • 如果 ?str ?排在 ?str2 ?前面,則返回負(fù)數(shù)。
  • 如果 ?str ?排在 ?str2 ?后面,則返回正數(shù)。
  • 如果它們?cè)谙嗤恢?,則返回 ?0?。

例如:

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

這個(gè)方法實(shí)際上在 文檔 中指定了兩個(gè)額外的參數(shù),這兩個(gè)參數(shù)允許它指定語(yǔ)言(默認(rèn)語(yǔ)言從環(huán)境中獲取,字符順序視語(yǔ)言不同而不同)并設(shè)置諸如區(qū)分大小寫(xiě),或應(yīng)該將 "a" 和 "a?" 作相同處理等附加的規(guī)則。

內(nèi)部,Unicode

進(jìn)階內(nèi)容

這部分會(huì)深入字符串內(nèi)部。如果你計(jì)劃處理 emoji、罕見(jiàn)的數(shù)學(xué)或象形文字或其他罕見(jiàn)的符號(hào),這些知識(shí)會(huì)對(duì)你有用。

如果你不打算支持它們,你可以跳過(guò)這一部分。

代理對(duì)

所有常用的字符都是一個(gè) 2 字節(jié)的代碼。大多數(shù)歐洲語(yǔ)言,數(shù)字甚至大多數(shù)象形文字中的字母都有 2 字節(jié)的表示形式。

但 2 字節(jié)只允許 65536 個(gè)組合,這對(duì)于表示每個(gè)可能的符號(hào)是不夠的。所以稀有的符號(hào)被稱(chēng)為“代理對(duì)”的一對(duì) 2 字節(jié)的符號(hào)編碼。

這些符號(hào)的長(zhǎng)度是 ?2?:

alert( ''.length ); // 2,大寫(xiě)數(shù)學(xué)符號(hào) X
alert( ''.length ); // 2,笑哭表情
alert( ''.length ); // 2,罕見(jiàn)的中國(guó)象形文字

注意,代理對(duì)在 JavaScript 被創(chuàng)建時(shí)并不存在,因此無(wú)法被編程語(yǔ)言正確處理!

我們實(shí)際上在上面的每個(gè)字符串中都有一個(gè)符號(hào),但 length 顯示長(zhǎng)度為 2。

String.fromCodePoint 和 str.codePointAt 是幾種處理代理對(duì)的少數(shù)方法。它們最近才出現(xiàn)在編程語(yǔ)言中。在它們之前,只有 String.fromCharCode 和  str.charCodeAt。這些方法實(shí)際上與 fromCodePoint/codePointAt 相同,但是不適用于代理對(duì)。

獲取符號(hào)可能會(huì)非常麻煩,因?yàn)榇韺?duì)被認(rèn)為是兩個(gè)字符:

alert( ''[0] ); // 奇怪的符號(hào)……
alert( ''[1] ); // ……代理對(duì)的一塊

請(qǐng)注意,代理對(duì)的各部分沒(méi)有任何意義。因此,上述示例中的 alert 顯示的實(shí)際上是垃圾信息。

技術(shù)角度來(lái)說(shuō),代理對(duì)也是可以通過(guò)它們的代碼檢測(cè)到的:如果一個(gè)字符的代碼在 0xd800..0xdbff 范圍內(nèi),那么它是代理對(duì)的第一部分。下一個(gè)字符(第二部分)必須在 0xdc00..0xdfff 范圍中。這些范圍是按照標(biāo)準(zhǔn)專(zhuān)門(mén)為代理對(duì)保留的。

在上述示例中:

// charCodeAt 不理解代理對(duì),所以它給出了代理對(duì)的代碼

alert( ''.charCodeAt(0).toString(16) ); // d835,在 0xd800 和 0xdbff 之間
alert( ''.charCodeAt(1).toString(16) ); // dcb3, 在 0xdc00 和 0xdfff 之間

本章節(jié)后面的 Iterable object(可迭代對(duì)象) 章節(jié)中,你可以找到更多處理代理對(duì)的方法??赡苡袑?zhuān)門(mén)的庫(kù),這里沒(méi)有什么足夠好的建議了。

變音符號(hào)與規(guī)范化

在許多語(yǔ)言中,都有一些由基本字符組成的符號(hào),在其上方/下方有一個(gè)標(biāo)記。

例如,字母 a 可以是 àáa???ā 的基本字符。最常見(jiàn)的“復(fù)合”字符在 UTF-16 表中都有自己的代碼。但不是全部,因?yàn)榭赡艿慕M合太多。

為了支持任意組合,UTF-16 允許我們使用多個(gè) Unicode 字符:基本字符緊跟“裝飾”它的一個(gè)或多個(gè)“標(biāo)記”字符。

例如,如果我們 S 后跟有特殊的 “dot above” 字符(代碼 \u0307),則顯示 S?。

alert( 'S\u0307' ); // S?

如果我們需要在字母上方(或下方)添加額外的標(biāo)記 —— 沒(méi)問(wèn)題,只需要添加必要的標(biāo)記字符即可。

例如,如果我們追加一個(gè)字符 “dot below”(代碼 \u0323),那么我們將得到“S 上面和下面都有點(diǎn)”的字符:S??。

例如:

alert( 'S\u0307\u0323' ); // S??

這在提供良好靈活性的同時(shí),也存在一個(gè)有趣的問(wèn)題:兩個(gè)視覺(jué)上看起來(lái)相同的字符,可以用不同的 Unicode 組合表示。

例如:

let s1 = 'S\u0307\u0323'; // S??,S + 上點(diǎn) + 下點(diǎn)
let s2 = 'S\u0323\u0307'; // S??,S + 下點(diǎn) + 上點(diǎn)

alert( `s1: ${s1}, s2: ${s2}` );

alert( s1 == s2 ); // false,盡管字符看起來(lái)相同(?!)

為了解決這個(gè)問(wèn)題,有一個(gè) “Unicode 規(guī)范化”算法,它將每個(gè)字符串都轉(zhuǎn)化成單個(gè)“通用”格式。

它由 str.normalize() 實(shí)現(xiàn)。

alert( "S\u0307\u0323".normalize() == "S\u0323\u0307".normalize() ); // true

有趣的是,在實(shí)際情況下,normalize() 實(shí)際上將一個(gè)由 3 個(gè)字符組成的序列合并為一個(gè):\u1e68(S 有兩個(gè)點(diǎn))。

alert( "S\u0307\u0323".normalize().length ); // 1

alert( "S\u0307\u0323".normalize() == "\u1e68" ); // true

事實(shí)上,情況并非總是如此,因?yàn)榉?hào) S?? 是“常用”的,所以 UTF-16 創(chuàng)建者把它包含在主表中并給它了對(duì)應(yīng)的代碼。

如果你想了解更多關(guān)于規(guī)范化規(guī)則和變體的信息 —— 它們?cè)?Unicode 標(biāo)準(zhǔn)附錄中有詳細(xì)描述:Unicode 規(guī)范化形式,但對(duì)于大多數(shù)實(shí)際目的來(lái)說(shuō),本文的內(nèi)容就已經(jīng)足夠了。

總結(jié)

  • 有 3 種類(lèi)型的引號(hào)。反引號(hào)允許字符串跨越多行并可以使用 ?${…}? 在字符串中嵌入表達(dá)式。
  • JavaScript 中的字符串使用的是 UTF-16 編碼。
  • 我們可以使用像 ?\n? 這樣的特殊字符或通過(guò)使用 ?\u...? 來(lái)操作它們的 Unicode 進(jìn)行字符插入。
  • 獲取字符時(shí),使用 ?[]?。
  • 獲取子字符串,使用 ?slice ?或 ?substring?。
  • 字符串的大/小寫(xiě)轉(zhuǎn)換,使用:?toLowerCase/toUpperCase?。
  • 查找子字符串時(shí),使用 ?indexOf ?或 ?includes/startsWith/endsWith? 進(jìn)行簡(jiǎn)單檢查。
  • 根據(jù)語(yǔ)言比較字符串時(shí)使用 ?localeCompare?,否則將按字符代碼進(jìn)行比較。

還有其他幾種有用的字符串方法:

  • ?str.trim()? —— 刪除字符串前后的空格 (“trims”)。
  • ?str.repeat(n)? —— 重復(fù)字符串 ?n? 次。
  • ……更多內(nèi)容細(xì)節(jié)請(qǐng)參見(jiàn) 手冊(cè)。

字符串還具有使用正則表達(dá)式進(jìn)行搜索/替換的方法。但這個(gè)話(huà)題很大,因此我們將在本教程中單獨(dú)的 正則表達(dá)式 章節(jié)中進(jìn)行討論。

任務(wù)


首字母大寫(xiě)

重要程度: 5

寫(xiě)一個(gè)函數(shù) ucFirst(str),并返回首字母大寫(xiě)的字符串 str,例如:

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

解決方案

我們不能“替換”第一個(gè)字符,因?yàn)樵?JavaScript 中字符串是不可變的。

但是我們可以根據(jù)已有字符串創(chuàng)建一個(gè)首字母大寫(xiě)的新字符串:

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

這里存在一個(gè)小問(wèn)題。如果 str 是空的,那么 str[0] 就是 undefined,但由于 undefined 并沒(méi)有 toUpperCase() 方法,因此我們會(huì)得到一個(gè)錯(cuò)誤。

存在如下兩種變體:

  1. 使用 ?str.charAt(0)?,因?yàn)樗偸菚?huì)返回一個(gè)字符串(可能為空)。
  2. 為空字符添加測(cè)試。

這是第二種變體:

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

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

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

檢查 spam

重要程度: 5

寫(xiě)一個(gè)函數(shù) checkSpam(str),如果 str 包含 viagra 或 XXX 就返回 true,否則返回 false。

函數(shù)必須不區(qū)分大小寫(xiě):

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

解決方案

為了使搜索不區(qū)分大小寫(xiě),我們將字符串改為小寫(xiě),然后搜索:

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") );

截?cái)辔谋?/h3>

重要程度: 5

創(chuàng)建函數(shù) truncate(str, maxlength) 來(lái)檢查 str 的長(zhǎng)度,如果超過(guò) maxlength —— 應(yīng)使用 "…" 來(lái)代替 str 的結(jié)尾部分,長(zhǎng)度仍然等于 maxlength。

函數(shù)的結(jié)果應(yīng)該是截?cái)嗪蟮奈谋荆ㄈ绻枰脑?huà))。

例如:

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

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

解決方案

最大長(zhǎng)度必須是 maxlength,因此為了給省略號(hào)留空間我們需要縮短它。

請(qǐng)注意,省略號(hào)實(shí)際上有一個(gè)單獨(dú)的 Unicode 字符,而不是三個(gè)點(diǎn)。

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

提取貨幣

重要程度: 4

我們有以 "$120" 這樣的格式表示的花銷(xiāo)。意味著:先是美元符號(hào),然后才是數(shù)值。

創(chuàng)建函數(shù) extractCurrencyValue(str) 從字符串中提取數(shù)值并返回。

例如:

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

解決方案

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


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)