在 JavaScript 中,文本數(shù)據(jù)被以字符串形式存儲(chǔ),單個(gè)字符沒(méi)有單獨(dú)的類(lèi)型。
字符串的內(nèi)部格式始終是 UTF-16,它不依賴(lài)于頁(yè)面編碼。
讓我們回憶一下引號(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: \
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)。
要獲取在 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)示例。
toLowerCase() 和 toUpperCase() 方法可以改變大小寫(xiě):
alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface
或者我們想要使一個(gè)字符變成小寫(xiě):
alert( 'Interface'[0].toLowerCase() ); // 'i'
在字符串中查找子字符串有很多種方法。
第一個(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)在工作了!
}
這里使用的一個(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)下文)。
更現(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ò),也有一些奇怪的地方。
alert( 'a' > 'Z' ); // true
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)的代碼。
?
? 的字母與主要字母表不同。這里,它的代碼比任何從 ?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ù)。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ī)則。
進(jìn)階內(nèi)容這部分會(huì)深入字符串內(nèi)部。如果你計(jì)劃處理 emoji、罕見(jiàn)的數(shù)學(xué)或象形文字或其他罕見(jiàn)的符號(hào),這些知識(shí)會(huì)對(duì)你有用。
如果你不打算支持它們,你可以跳過(guò)這一部分。
所有常用的字符都是一個(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)有什么足夠好的建議了。
在許多語(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)足夠了。
${…}
? 在字符串中嵌入表達(dá)式。\n
? 這樣的特殊字符或通過(guò)使用 ?\u...
? 來(lái)操作它們的 Unicode 進(jìn)行字符插入。[]
?。slice
?或 ?substring
?。toLowerCase/toUpperCase
?。indexOf
?或 ?includes/startsWith/endsWith
? 進(jìn)行簡(jiǎn)單檢查。localeCompare
?,否則將按字符代碼進(jìn)行比較。還有其他幾種有用的字符串方法:
str.trim()
? —— 刪除字符串前后的空格 (“trims”)。str.repeat(n)
? —— 重復(fù)字符串 ?n
? 次。字符串還具有使用正則表達(dá)式進(jìn)行搜索/替換的方法。但這個(gè)話(huà)題很大,因此我們將在本教程中單獨(dú)的 正則表達(dá)式 章節(jié)中進(jìn)行討論。
重要程度: 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ò)誤。
存在如下兩種變體:
str.charAt(0)
?,因?yàn)樗偸菚?huì)返回一個(gè)字符串(可能為空)。這是第二種變體:
function ucFirst(str) {
if (!str) return str;
return str[0].toUpperCase() + str.slice(1);
}
alert( ucFirst("john") ); // John
重要程度: 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") );
重要程度: 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);
}
更多建議: