“睡服”面试官系列第四篇之字符串的扩展(建议收藏学习)
目錄
?
字符串的擴展
1. 字符的 Unicode 表示法
2. codePointAt()
3. String.fromCodePoint()
4. 字符串的遍歷器接口
5. at()
6. normalize()
7. includes(), startsWith(), endsWith()
8. repeat()
9. padStart(),padEnd()
10. 模板字符串
11. 實例:模板編譯
13. String.raw()
14. 模板字符串的限制
字符串的擴展
ES6 加強了對 Unicode 的支持,并且擴展了字符串對象。
1. 字符的 Unicode 表示法
JavaScript 允許采用 \uxxxx 形式表示一個字符,其中 xxxx 表示字符的 Unicode 碼點。
"\u0061" // "a" 但是,這種但是,這種表示法只限于碼點在 \u0000 ~ \uFFFF 之間的字符。超出這個范圍的字符,必須用兩個雙字節的形式表示
"\uD842\uDFB7" // "𠮷" "\u20BB7" // " 7"上面代碼表示,如果直接在 \u 后面跟上超過 0xFFFF 的數值(比如 \u20BB7 ),JavaScript 會理解成 \u20BB+7 。由于 \u20BB 是一個不可打印字符,所以
只會顯示一個空格,后面跟著一個 7 。
ES6 對這一點做出了改進,只要將碼點放入大括號,就能正確解讀該字符
上面代碼中,最后一個例子表明,大括號表示法與四字節的 UTF-16 編碼是等價的。
有了這種表示法之后,JavaScript 共有 6 種方法可以表示一個字符
2. codePointAt()
JavaScript 內部,字符以 UTF-16 的格式儲存,每個字符固定為 2 個字節。對于那些需要 4 個字節儲存的字符(Unicode 碼點大于 0xFFFF 的字符),
JavaScript 會認為它們是兩個字符。
上面代碼中,漢字“𠮷”(注意,這個字不是“吉祥”的“吉”)的碼點是 0x20BB7 ,UTF-16 編碼為 0xD842 0xDFB7 (十進制為 55362 57271 ),需要 4 個字
節儲存。對于這種 4 個字節的字符,JavaScript 不能正確處理,字符串長度會誤判為 2 ,而且 charAt 方法無法讀取整個字符, charCodeAt 方法只能分別
返回前兩個字節和后兩個字節的值。
ES6 提供了 codePointAt 方法,能夠正確處理 4 個字節儲存的字符,返回一個字符的碼點。
codePointAt 方法的參數,是字符在字符串中的位置(從 0 開始)。上面代碼中,JavaScript 將“𠮷a”視為三個字符,codePointAt 方法在第一個字符
上,正確地識別了“𠮷”,返回了它的十進制碼點 134071(即十六進制的 20BB7 )。在第二個字符(即“𠮷”的后兩個字節)和第三個字符“a”上
codePointAt 方法的結果與 charCodeAt 方法相同。
總之, codePointAt 方法會正確返回 32 位的 UTF-16 字符的碼點。對于那些兩個字節儲存的常規字符,它的返回結果與 charCodeAt 方法相同。
codePointAt 方法返回的是碼點的十進制值,如果想要十六進制的值,可以使用 toString 方法轉換一下。
你可能注意到了, codePointAt 方法的參數,仍然是不正確的。比如,上面代碼中,字符 a 在字符串 s 的正確位置序號應該是 1,但是必須向
codePointAt 方法傳入 2。解決這個問題的一個辦法是使用 for...of 循環,因為它會正確識別 32 位的 UTF-16 字符。
codePointAt 方法是測試一個字符由兩個字節還是由四個字節組成的最簡單方法
function is32Bit(c) { return c.codePointAt(0) > 0xFFFF; } is32Bit("𠮷") // true is32Bit("a") // false3. String.fromCodePoint()
ES5 提供 String.fromCharCode 方法,用于從碼點返回對應字符,但是這個方法不能識別 32 位的 UTF-16 字符(Unicode 編號大于 0xFFFF )
String.fromCharCode(0x20BB7) // "?上面代碼中, String.fromCharCode 不能識別大于 0xFFFF 的碼點,所以 0x20BB7 就發生了溢出,最高位 2 被舍棄了,最后返回碼點 U+0BB7 對應的字符,
而不是碼點 U+20BB7 對應的字符。
ES6 提供了 String.fromCodePoint 方法,可以識別大于 0xFFFF 的字符,彌補了 String.fromCharCode 方法的不足。在作用上,正好與 codePointAt 方
法相反。
上面代碼中,如果 String.fromCodePoint 方法有多個參數,則它們會被合并成一個字符串返回。
注意, fromCodePoint 方法定義在 String 對象上,而 codePointAt 方法定義在字符串的實例對象上。
4. 字符串的遍歷器接口
ES6 為字符串添加了遍歷器接口,使得字符串可以被 for...of 循環遍歷。
for (let codePoint of 'foo') { console.log(codePoint) } // "f" // "o" // "o"除了遍歷字符串,這個遍歷器最大的優點是可以識別大于 0xFFFF 的碼點,傳統的 for 循環無法識別這樣的碼點
let text = String.fromCodePoint(0x20BB7); for (let i = 0; i < text.length; i++) { console.log(text[i]); } // " " // " " for (let i of text) { console.log(i); } // "𠮷"上面代碼中,字符串 text 只有一個字符,但是 for 循環會認為它包含兩個字符(都不可打印),而 for...of 循環會正確識別出這一個字符。
5. at()
ES5 對字符串對象提供 charAt 方法,返回字符串給定位置的字符。該方法不能識別碼點大于 0xFFFF 的字符。
'abc'.charAt(0) // "a" '𠮷'.charAt(0) // "\uD842"上面代碼中, charAt 方法返回的是 UTF-16 編碼的第一個字節,實際上是無法顯示的。
目前,有一個提案,提出字符串實例的 at 方法,可以識別 Unicode 編號大于 0xFFFF 的字符,返回正確的字符。
6. normalize()
許多歐洲語言有語調符號和重音符號。為了表示它們,Unicode 提供了兩種方法。一種是直接提供帶重音符號的字符,比如 ǒ (\u01D1)。另一種是提
供合成符號(combining character),即原字符與重音符號的合成,兩個字符合成一個字符,比如 O (\u004F)和 ˇ (\u030C)合成
ǒ (\u004F\u030C)。
這兩種表示方法,在視覺和語義上都等價,但是 JavaScript 不能識別。
上面代碼表示,JavaScript 將合成字符視為兩個字符,導致兩種表示方法不相等。
ES6 提供字符串實例的 normalize() 方法,用來將字符的不同表示方法統一為同樣的形式,這稱為 Unicode 正規化
normalize 方法可以接受一個參數來指定 normalize 的方式,參數的四個可選值如下。
NFC ,默認參數,表示“標準等價合成”(Normalization Form Canonical Composition),返回多個簡單字符的合成字符。所謂“標準等價”指的
是視覺和語義上的等價。
NFD ,表示“標準等價分解”(Normalization Form Canonical Decomposition),即在標準等價的前提下,返回合成字符分解的多個簡單字符。
NFKC ,表示“兼容等價合成”(Normalization Form Compatibility Composition),返回合成字符。所謂“兼容等價”指的是語義上存在等價,但
視覺上不等價,比如“囍”和“喜喜”。(這只是用來舉例, normalize 方法不能識別中文。)
NFKD ,表示“兼容等價分解”(Normalization Form Compatibility Decomposition),即在兼容等價的前提下,返回合成字符分解的多個簡單
字符。
上面代碼表示, NFC 參數返回字符的合成形式, NFD 參數返回字符的分解形式。
不過, normalize 方法目前不能識別三個或三個以上字符的合成。這種情況下,還是只能使用正則表達式,通過 Unicode 編號區間判斷
7. includes(), startsWith(), endsWith()
傳統上,JavaScript 只有 indexOf 方法,可以用來確定一個字符串是否包含在另一個字符串中。ES6 又提供了三種新方法。
includes():返回布爾值,表示是否找到了參數字符串。
startsWith():返回布爾值,表示參數字符串是否在原字符串的頭部。
endsWith():返回布爾值,表示參數字符串是否在原字符串的尾部。
這三個方法都支持第二個參數,表示開始搜索的位置
let s = 'Hello world!'; s.startsWith('world', 6) // true s.endsWith('Hello', 5) // true s.includes('Hello', 6) // false上面代碼表示,使用第二個參數 n 時, endsWith 的行為與其他兩個方法有所不同。它針對前 n 個字符,而其他兩個方法針對從第 n 個位置直到字符串結
束。
8. repeat()
repeat 方法返回一個新字符串,表示將原字符串重復 n 次。
'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // ""參數如果是小數,會被取整。
'na'.repeat(2.9) // "nana"如果 repeat 的參數是負數或者 Infinity ,會報錯。
'na'.repeat(Infinity) // RangeError 'na'.repeat(-1) // RangeErro但是,如果參數是 0 到-1 之間的小數,則等同于 0,這是因為會先進行取整運算。0 到-1 之間的小數,取整以后等于 -0 , repeat 視同為 0
'na'.repeat(-0.9) // ""參數 NaN 等同于 0
'na'.repeat(NaN) // ""如果 repeat 的參數是字符串,則會先轉換成數字。
'na'.repeat('na') // "" 'na'.repeat('3') // "nanana"9. padStart(),padEnd()
ES2017 引入了字符串補全長度的功能。如果某個字符串不夠指定長度,會在頭部或尾部補全。 padStart() 用于頭部補全, padEnd() 用于尾部補全
'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba'上面代碼中, padStart 和 padEnd 一共接受兩個參數,第一個參數用來指定字符串的最小長度,第二個參數是用來補全的字符串。
如果原字符串的長度,等于或大于指定的最小長度,則返回原字符串。
如果用來補全的字符串與原字符串,兩者的長度之和超過了指定的最小長度,則會截去超出位數的補全字符串。
'abc'.padStart(10, '0123456789') // '0123456abc'如果省略第二個參數,默認使用空格補全長度。
'x'.padStart(4) // ' x' 'x'.padEnd(4) // 'xpadStart 的常見用途是為數值補全指定位數。下面代碼生成 10 位的數值字符串。
'1'.padStart(10, '0') // "0000000001" '12'.padStart(10, '0') // "0000000012" '123456'.padStart(10, '0') // "0000123456"另一個用途是提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12" '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"10. 模板字符串
傳統的 JavaScript 語言,輸出模板通常是這樣寫的。
$('#result').append( 'There are <b>' + basket.count + '</b> ' + 'items in your basket, ' + '<em>' + basket.onSale + '</em> are on sale!' );上面這種寫法相當繁瑣不方便,ES6 引入了模板字符串解決這個問題。
$('#result').append(` There are <b>${basket.count}</b> items in your basket, <em>${basket.onSale}</em> are on sale! `);模板字符串(template string)是增強版的字符串,用反引號(`)標識。它可以當作普通字符串使用,也可以用來定義多行字符串,或者在字符串中嵌
入變量
上面代碼中的模板字符串,都是用反引號表示。如果在模板字符串中需要使用反引號,則前面要用反斜杠轉義
let greeting = `\`Yo\` World!`;如果使用模板字符串表示多行字符串,所有的空格和縮進都會被保留在輸出之中
$('#list').html(` <ul> <li>first</li> <li>second</li> </ul> `);上面代碼中,所有模板字符串的空格和換行,都是被保留的,比如 <ul> 標簽前面會有一個換行。如果你不想要這個換行,可以使用 trim 方法消除它。
$('#list').html(` <ul> <li>first</li> <li>second</li> </ul> `.trim());模板字符串中嵌入變量,需要將變量名寫在 ${} 之中。
function authorize(user, action) { if (!user.hasPrivilege(action)) { throw new Error( // 傳統寫法為 // 'User ' // + user.name // + ' is not authorized to do ' // + action // + '.' `User ${user.name} is not authorized to do ${action}.`); } }大括號內部可以放入任意的 JavaScript 表達式,可以進行運算,以及引用對象屬性。
let x = 1; let y = 2; `${x} + ${y} = ${x + y}` // "1 + 2 = 3" `${x} + ${y * 2} = ${x + y * 2}` // "1 + 4 = 5" let obj = {x: 1, y: 2}; `${obj.x + obj.y}` // "3"模板字符串之中還能調用函數
function fn() { return "Hello World"; } `foo ${fn()} bar` // foo Hello World bar如果大括號中的值不是字符串,將按照一般的規則轉為字符串。比如,大括號中是一個對象,將默認調用對象的 toString 方法。
如果模板字符串中的變量沒有聲明,將報錯。
由于模板字符串的大括號內部,就是執行 JavaScript 代碼,因此如果大括號內部是一個字符串,將會原樣輸出。
`Hello ${'World'}` // "Hello World"模板字符串甚至還能嵌套。
const tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('')} </table> `;上面代碼中,模板字符串的變量之中,又嵌入了另一個模板字符串,使用方法如下
const data = [ { first: '<Jane>', last: 'Bond' }, { first: 'Lars', last: '<Croft>' }, ]; console.log(tmpl(data)); // <table> // // <tr><td><Jane></td></tr> // <tr><td>Bond</td></tr> // // <tr><td>Lars</td></tr> // <tr><td><Croft></td></tr> // // </table>如果需要引用模板字符串本身,在需要時執行,可以像下面這樣寫。
// 寫法一 let str = 'return ' + '`Hello ${name}!`'; let func = new Function('name', str); func('Jack') // "Hello Jack!" // 寫法二 let str = '(name) => `Hello ${name}!`'; let func = eval.call(null, str); func('Jack') // "Hello Jack!"11. 實例:模板編譯
下面,我們來看一個通過模板字符串,生成正式模板的實例。
let template = ` <ul> <% for(let i=0; i < data.supplies.length; i++) { %> <li><%= data.supplies[i] %></li> <% } %> </ul> `;上面代碼在模板字符串之中,放置了一個常規模板。該模板使用 <%...%> 放置 JavaScript 代碼,使用 <%= ... %> 輸出 JavaScript 表達式。
怎么編譯這個模板字符串呢?
一種思路是將其轉換為 JavaScript 表達式字符串。
這個轉換使用正則表達式就行了。
let evalExpr = /<%=(.+?)%>/g; let expr = /<%([\s\S]+?)%>/g; template = template .replace(evalExpr, '`); \n echo( $1 ); \n echo(`') .replace(expr, '`); \n $1 \n echo(`'); template = 'echo(`' + template + '`);';然后,將 template 封裝在一個函數里面返回,就可以了
let script = `(function parse(data){ let output = ""; function echo(html){ output += html; } ${ template } return output; })`; return script;將上面的內容拼裝成一個模板編譯函數 compile 。
function compile(template){ const evalExpr = /<%=(.+?)%>/g; const expr = /<%([\s\S]+?)%>/g; template = template .replace(evalExpr, '`); \n echo( $1 ); \n echo(`') .replace(expr, '`); \n $1 \n echo(`'); template = 'echo(`' + template + '`);'; let script = `(function parse(data){ let output = ""; function echo(html){ output += html; } ${ template } return output; })`; return script; }compile 函數的用法如下
let parse = eval(compile(template)); div.innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] }); // <ul> // <li>broom</li> // <li>mop</li> // <li>cleaner</li> // </ul>12. 標簽模板
模板字符串的功能,不僅僅是上面這些。它可以緊跟在一個函數名后面,該函數將被調用來處理這個模板字符串。這被稱為“標簽模板”功能(tagged
template)。
標簽模板其實不是模板,而是函數調用的一種特殊形式。“標簽”指的就是函數,緊跟在后面的模板字符串就是它的參數。
但是,如果模板字符里面有變量,就不是簡單的調用了,而是會將模板字符串先處理成多個參數,再調用函數
上面代碼中,模板字符串前面有一個標識名 tag ,它是一個函數。整個表達式的返回值,就是 tag 函數處理模板字符串后的返回值。
函數 tag 依次會接收到多個參數。
tag 函數的第一個參數是一個數組,該數組的成員是模板字符串中那些沒有變量替換的部分,也就是說,變量替換只發生在數組的第一個成員與第二個成
員之間、第二個成員與第三個成員之間,以此類推。
tag 函數的其他參數,都是模板字符串各個變量被替換后的值。由于本例中,模板字符串含有兩個變量,因此 tag 會接受到 value1 和 value2 兩個參數。
tag 函數所有參數的實際值如下
第一個參數: ['Hello ', ' world ', '']
第二個參數: 15
第三個參數:50
也就是說, tag 函數實際上以下面的形式調用。
我們可以按照需要編寫 tag 函數的代碼。下面是 tag 函數的一種寫法,以及運行結果
let a = 5; let b = 10; function tag(s, v1, v2) { console.log(s[0]); console.log(s[1]); console.log(s[2]); console.log(v1); console.log(v2); return "OK"; } tag`Hello ${ a + b } world ${ a * b}`; // "Hello " // " world " // "" // 15 // 50 // "OK下面是一個更復雜的例子。
let total = 30; let msg = passthru`The total is ${total} (${total*1.05} with tax)`; function passthru(literals) { let result = ''; let i = 0; while (i < literals.length) { result += literals[i++]; if (i < arguments.length) { result += arguments[i]; } } return result; } msg // "The total is 30 (31.5 with tax)"上面這個例子展示了,如何將各個參數按照原來的位置拼合回去。
passthru 函數采用 rest 參數的寫法如下
“標簽模板”的一個重要應用,就是過濾 HTML 字符串,防止用戶輸入惡意內容。
let message = SaferHTML`<p>${sender} has sent you a message.</p>`; function SaferHTML(templateData) { let s = templateData[0]; for (let i = 1; i < arguments.length; i++) { let arg = String(arguments[i]); // Escape special characters in the substitution. s += arg.replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">"); // Don't escape special characters in the template. s += templateData[i]; } return s; }上面代碼中, sender 變量往往是用戶提供的,經過 SaferHTML 函數處理,里面的特殊字符都會被轉義。
let sender = '<script>alert("abc")</script>'; // 惡意代碼 let message = SaferHTML`<p>${sender} has sent you a message.</p>`; message // <p><script>alert("abc")</script> has sent you a message.</p>標簽模板的另一個應用,就是多語言轉換(國際化處理)
13. String.raw()
ES6 還為原生的 String 對象,提供了一個 raw 方法。
String.raw 方法,往往用來充當模板字符串的處理函數,返回一個斜杠都被轉義(即斜杠前面再加一個斜杠)的字符串,對應于替換變量后的模板字符
串。
如果原字符串的斜杠已經轉義,那么 String.raw 不會做任何處理。
String.raw`Hi\\n` // "Hi\\n" String.raw 的代碼基本如下。 String.raw = function (strings, ...values) { let output = ""; let index; for (index = 0; index < values.length; index++) { output += strings.raw[index] + values[index]; } output += strings.raw[index] return output; }String.raw 方法可以作為處理模板字符串的基本方法,它會將所有變量替換,而且對斜杠進行轉義,方便下一步作為字符串來使用。
String.raw 方法也可以作為正常的函數使用。這時,它的第一個參數,應該是一個具有 raw 屬性的對象,且 raw 屬性的值應該是一個數組。
14. 模板字符串的限制
前面提到標簽模板里面,可以內嵌其他語言。但是,模板字符串默認會將字符串轉義,導致無法嵌入其他語言。
舉例來說,標簽模板里面可以嵌入 LaTEX 語言。
上面代碼中,變量 document 內嵌的模板字符串,對于 LaTEX 語言來說完全是合法的,但是 JavaScript 引擎會報錯。原因就在于字符串的轉義。
模板字符串會將 \u00FF 和 \u{42} 當作 Unicode 字符進行轉義,所以 \unicode 解析時報錯;而 \x56 會被當作十六進制字符串轉義,所以 \xerxes 會報
錯。也就是說, \u 和 \x 在 LaTEX 里面有特殊含義,但是 JavaScript 將它們轉義了。
為了解決這個問題,現在有一個提案,放松對標簽模板里面的字符串轉義的限制。如果遇到不合法的字符串轉義,就返回 undefined ,而不是報錯,并且
從 raw 屬性上面可以得到原始字符串。
上面代碼中,模板字符串原本是應該報錯的,但是由于放松了對字符串轉義的限制,所以不報錯了,JavaScript 引擎將第一個字符設置為 undefined ,但
是 raw 屬性依然可以得到原始字符串,因此 tag 函數還是可以對原字符串進行處理。
注意,這種對字符串轉義的放松,只在標簽模板解析字符串時生效,不是標簽模板的場合,依然會報錯
本博客源于本人閱讀相關書籍和視頻總結,創作不易,謝謝點贊支持。學到就是賺到。我是歌謠,勵志成為一名優秀的技術革新人員。
歡迎私信交流,一起學習,一起成長。
推薦鏈接 其他文件目錄參照
“睡服“面試官系列之各系列目錄匯總(建議學習收藏)
總結
以上是生活随笔為你收集整理的“睡服”面试官系列第四篇之字符串的扩展(建议收藏学习)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 快解析结合泛微OA
- 下一篇: 前端学习(1770):前端调试之如何参照