5.3 原始值包裝類型
????????為了方便操作原始值,ECMAScript提供了3種特殊的引用類型:Boolean、Number和String。這些類型具有本章介紹的其他引用類型一樣的特點(diǎn),但也具有與各自原始類型對應(yīng)的特殊行為。每當(dāng)用到某個(gè)原始值的方法或?qū)傩詴r(shí),后臺都會創(chuàng)建一個(gè)相應(yīng)原始包裝類型的對象,從而暴露出操作原始值的各種方法。來看下面的例子:
let s1 = "some text";
let s2 = s1.substring(2);
????????在這里,s1是一個(gè)包含字符串的變量,它是一個(gè)原始值。第二行緊接著在s1上調(diào)用了substring()方法,并把結(jié)果保存在s2中。我們知道,原始值本身不是對象,因此邏輯上不應(yīng)該有方法。而實(shí)際上這個(gè)例子又確實(shí)按照預(yù)期運(yùn)行了。這是因?yàn)楹笈_進(jìn)行了很多處理,從而實(shí)現(xiàn)了上述操
作。具體來說,當(dāng)?shù)诙性L問s1時(shí),是以讀模式訪問的,也就是要從內(nèi)存中讀取變量保存的值。在以讀模式訪問字符串值的任何時(shí)候,后臺都會執(zhí)行以下3步:?
(1) 創(chuàng)建一個(gè)String類型的實(shí)例;
(2) 調(diào)用實(shí)例上的特定方法;
(3) 銷毀實(shí)例。
可以把這3步想象成執(zhí)行了如下3行ECMAScript代碼:
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;
????????這種行為可以讓原始值擁有對象的行為。對布爾值和數(shù)值而言,以上3步也會在后臺發(fā)生,只不過使用的是Boolean和Number包裝類型而已。?
????????引用類型與原始值包裝類型的主要區(qū)別在于對象的生命周期。在通過new實(shí)例化引用類型后,得到的實(shí)例會在離開作用域時(shí)被銷毀,而自動創(chuàng)建的原始值包裝對象則只存在于訪問它的那行代碼執(zhí)行期間。這意味著不能在運(yùn)行時(shí)給原始值添加屬性和方法。比如下面的例子:
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined
?????????這里的第二行代碼嘗試給字符串s1添加了一個(gè)color屬性??墒牵谌写a訪問color屬性時(shí),它卻不見了。原因就是第二行代碼運(yùn)行時(shí)會臨時(shí)創(chuàng)建一個(gè)String對象,而當(dāng)?shù)谌写a執(zhí)行時(shí),這個(gè)對象已經(jīng)被銷毀了。實(shí)際上,第三行代碼在這里創(chuàng)建了自己的String對象,但這個(gè)對象沒有color屬性。
????????可以顯式地使用Boolean、Number和String構(gòu)造函數(shù)創(chuàng)建原始值包裝對象。不過應(yīng)該在確實(shí)必要時(shí)再這么做,否則容易讓開發(fā)者疑惑,分不清它們到底是原始值還是引用值。在原始值包裝類型的實(shí)例上調(diào)用typeof會返回"object",所有原始值包裝對象都會轉(zhuǎn)換為布爾值true。
????????另外,Object構(gòu)造函數(shù)作為一個(gè)工廠方法,能夠根據(jù)傳入值的類型返回相應(yīng)原始值包裝類型的實(shí)例。比如:
let obj = new Object("some text");
console.log(obj instanceof String); // true
?????????如果傳給Object的是字符串,則會創(chuàng)建一個(gè)String的實(shí)例。如果是數(shù)值,則會創(chuàng)建Number的實(shí)例。布爾值則會得到Boolean的實(shí)例。
????????注意,使用new調(diào)用原始值包裝類型的構(gòu)造函數(shù),與調(diào)用同名的轉(zhuǎn)型函數(shù)并不一樣。例如:
let value = "25";
let number = Number(value); // 轉(zhuǎn)型函數(shù)
console.log(typeof number); // "number"
let obj = new Number(value); // 構(gòu)造函數(shù)
console.log(typeof obj); // "object"
????????在這個(gè)例子中,變量number中保存的是一個(gè)值為25的原始數(shù)值,而變量obj中保存的是一個(gè)Number的實(shí)例。?
????????雖然不推薦顯式創(chuàng)建原始值包裝類型的實(shí)例,但它們對于操作原始值的功能是很重要的。每個(gè)原始值包裝類型都有相應(yīng)的一套方法來方便數(shù)據(jù)操作。
5.3.1 Boolean
????????Boolean是對應(yīng)布爾值的引用類型。要創(chuàng)建一個(gè)Boolean對象,就使用Boolean構(gòu)造函數(shù)并傳入true或false,如下例所示:
let booleanObject = new Boolean(true);
????????Boolean的實(shí)例會重寫valueOf()方法,返回一個(gè)原始值true或false。toString()方法被調(diào)用時(shí)也會被覆蓋,返回字符串"true"或"false"。不過,Boolean對象在ECMAScript中用得很少。不僅如此,它們還容易引起誤會,尤其是在布爾表達(dá)式中使用Boolean對象時(shí),比如:
let falseObject = new Boolean(false);
let result = falseObject && true;
console.log(result); // true
let falseValue = false;
result = falseValue && true;
console.log(result); // false
?????????在這段代碼中,我們創(chuàng)建一個(gè)值為false的Boolean對象。然后,在一個(gè)布爾表達(dá)式中通過&&操作將這個(gè)對象與一個(gè)原始值true組合起來。在布爾算術(shù)中,false && true等于false??墒?,這個(gè)表達(dá)式是對falseObject對象而不是對它表示的值(false)求值。前面剛剛說過,所有對象在布爾表達(dá)式中都會自動轉(zhuǎn)換為true,因此falseObject在這個(gè)表達(dá)式里實(shí)際上表示一個(gè)true值。那么true && true當(dāng)然是true。
????????除此之外,原始值和引用值(Boolean對象)還有幾個(gè)區(qū)別。首先,typeof操作符對原始值返回"boolean",但對引用值返回"object"。同樣,Boolean對象是Boolean類型的實(shí)例,在使用instaceof操作符時(shí)返回true,但對原始值則返回false,如下所示:
console.log(typeof falseObject); // object
console.log(typeof falseValue); // boolean
console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean); // false
?????????理解原始布爾值和Boolean對象之間的區(qū)別非常重要,強(qiáng)烈建議永遠(yuǎn)不要使用后者。
5.3.2 Number
????????Number是對應(yīng)數(shù)值的引用類型。要創(chuàng)建一個(gè)Number對象,就使用Number構(gòu)造函數(shù)并傳入一個(gè)數(shù)值,如下例所示:
let numberObject = new Number(10);
????????與Boolean類型一樣,Number類型重寫了valueOf()、toLocaleString()和toString()方法。valueOf()方法返回Number對象表示的原始數(shù)值,另外兩個(gè)方法返回?cái)?shù)值字符串。toString()方法可選地接收一個(gè)表示基數(shù)的參數(shù),并返回相應(yīng)基數(shù)形式的數(shù)值字符串,如下所示:
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
????????除了繼承的方法,Number類型還提供了幾個(gè)用于將數(shù)值格式化為字符串的方法。
????????toFixed()方法返回包含指定小數(shù)點(diǎn)位數(shù)的數(shù)值字符串,如:
let num = 10;
console.log(num.toFixed(2)); // "10.00"
????????這里的toFixed()方法接收了參數(shù)2,表示返回的數(shù)值字符串要包含兩位小數(shù)。結(jié)果返回值為"10.00",小數(shù)位填充了0。如果數(shù)值本身的小數(shù)位超過了參數(shù)指定的位數(shù),則四舍五入到最接近的小數(shù)位:
let num = 10.005;
console.log(num.toFixed(2)); // "10.01"
????????toFixed()自動舍入的特點(diǎn)可以用于處理貨幣。不過要注意的是,多個(gè)浮點(diǎn)數(shù)值的數(shù)學(xué)計(jì)算不一定得到精確的結(jié)果。比如,0.1 + 0.2 =0.30000000000000004。
????????另一個(gè)用于格式化數(shù)值的方法是toExponential(),返回以科學(xué)記數(shù)法(也稱為指數(shù)記數(shù)法)表示的數(shù)值字符串。與toFixed()一樣,toExponential()也接收一個(gè)參數(shù),表示結(jié)果中小數(shù)的位數(shù)。來看下面的例子:
let num = 10;
console.log(num.toExponential(1)); // "1.0e+1"
????????這段代碼的輸出為"1.0e+1"。一般來說,這么小的數(shù)不用表示為科學(xué)記數(shù)法形式。如果想得到數(shù)值最適當(dāng)?shù)男问?,那么可以使用toPrecision()。
????????toPrecision()方法會根據(jù)情況返回最合理的輸出結(jié)果,可能是固定長度,也可能是科學(xué)記數(shù)法形式。這個(gè)方法接收一個(gè)參數(shù),表示結(jié)果中數(shù)字的總位數(shù)(不包含指數(shù))。來看幾個(gè)例子:
let num = 99;
console.log(num.toPrecision(1)); // "1e+2"
console.log(num.toPrecision(2)); // "99"
console.log(num.toPrecision(3)); // "99.0"
????????在這個(gè)例子中,首先要用1位數(shù)字表示數(shù)值99,得到"1e+2",也就是100。因?yàn)?9不能只用1位數(shù)字來精確表示,所以這個(gè)方法就將它舍入為100,這樣就可以只用1位數(shù)字(及其科學(xué)記數(shù)法形式)來表示了。用2位數(shù)字表示99得到"99",用3位數(shù)字則是"99.0"。本質(zhì)上,toPrecision()方法會根據(jù)數(shù)值和精度來決定調(diào)用toFixed()還是toExponential()。為了以正確的小數(shù)位精確表示數(shù)值,這3個(gè)方法都會向上或向下舍入。
????????與Boolean對象類似,Number對象也為數(shù)值提供了重要能力。但是,考慮到兩者存在同樣的潛在問題,因此并不建議直接實(shí)例化Number對象。在處理原始數(shù)值和引用數(shù)值時(shí),typeof和instacnceof操作符會返回不同的結(jié)果,如下所示:
let numberObject = new Number(10);
let numberValue = 10;
console.log(typeof numberObject); // "object"
console.log(typeof numberValue); // "number"
console.log(numberObject instanceof Number); // true
console.log(numberValue instanceof Number); // false
????????原始數(shù)值在調(diào)用typeof時(shí)始終返回"number",而Number對象則返回"object"。類似地,Number對象是Number類型的實(shí)例,而原始數(shù)值不是。
????????isInteger()方法與安全整數(shù)
????????ES6新增了Number.isInteger()方法,用于辨別一個(gè)數(shù)值是否保存為整數(shù)。有時(shí)候,小數(shù)位的0可能會讓人誤以為數(shù)值是一個(gè)浮點(diǎn)值:
console.log(Number.isInteger(1)); // true
console.log(Number.isInteger(1.00)); // true
console.log(Number.isInteger(1.01)); // false
5.3.3 string
????????String是對應(yīng)字符串的引用類型。要創(chuàng)建一個(gè)String對象,使用String構(gòu)造函數(shù)并傳入一個(gè)數(shù)值,如下例所示:
let stringObject = new String("hello world");
????????String對象的方法可以在所有字符串原始值上調(diào)用。3個(gè)繼承的方法valueOf()、toLocaleString()和toString()都返回對象的原始字符串值。
????????每個(gè)String對象都有一個(gè)length屬性,表示字符串中字符的數(shù)量。來看下面的例子:
let stringValue = "hello world";
console.log(stringValue.length); // "11"
????????這個(gè)例子輸出了字符串"hello world"中包含的字符數(shù)量:11。注意,即使字符串中包含雙字節(jié)字符(而不是單字節(jié)的ASCII字符),也仍然會按單字符來計(jì)數(shù)。
????????String類型提供了很多方法來解析和操作字符串。
1、JavaScript字符
????????JavaScript字符串由16位碼元(code unit)組成。對多數(shù)字符來說,每16位碼元對應(yīng)一個(gè)字符。換句話說,字符串的length屬性表示字符串包含多少16位碼元:
let message = "abcde";
console.log(message.length); // 5
????????此外,charAt()方法返回給定索引位置的字符,由傳給方法的整數(shù)參數(shù)指定。具體來說,這個(gè)方法查找指定索引位置的16位碼元,并返回該碼元對應(yīng)的字符:
let message = "abcde";
console.log(message.charAt(2)); // "c"
????????JavaScript字符串使用了兩種Unicode編碼混合的策略:UCS-2和UTF-16。對于可以采用16位編碼的字符(U+0000~U+FFFF),這兩種編碼實(shí)際上是一樣的。
????????使用charCodeAt()方法可以查看指定碼元的字符編碼。這個(gè)方法返回指定索引位置的碼元值,索引以整數(shù)指定。比如:
let message = "abcde";
// Unicode "Latin small letter C"的編碼是U+0063
console.log(message.charCodeAt(2)); // 99
// 十進(jìn)制99等于十六進(jìn)制63
console.log(99 === 0x63); // true
?????????fromCharCode()方法用于根據(jù)給定的UTF-16碼元創(chuàng)建字符串中的字符。這個(gè)方法可以接受任意多個(gè)數(shù)值,并返回將所有數(shù)值對應(yīng)的字符拼接起來的字符串:
// Unicode "Latin small letter A"的編碼是U+0061
// Unicode "Latin small letter B"的編碼是U+0062
// Unicode "Latin small letter C"的編碼是U+0063
// Unicode "Latin small letter D"的編碼是U+0064
// Unicode "Latin small letter E"的編碼是U+0065
console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65)); // "abcde"
// 0x0061 === 97
// 0x0062 === 98
// 0x0063 === 99
// 0x0064 === 100
// 0x0065 === 101
console.log(String.fromCharCode(97, 98, 99, 100, 101)); // "abcde"
?????????對于U+0000~U+FFFF范圍內(nèi)的字符,length、charAt()、charCodeAt()和fromCharCode()返回的結(jié)果都跟預(yù)期是一樣的。這是因?yàn)樵谶@個(gè)范圍內(nèi),每個(gè)字符都是用16位表示的,而這幾個(gè)方法也都基于16位碼元完成操作。只要字符編碼大小與碼元大小一一對應(yīng),這些方法就能如期工作。
????????這個(gè)對應(yīng)關(guān)系在擴(kuò)展到Unicode增補(bǔ)字符平面時(shí)就不成立了。問題很簡單,即16位只能唯一表示65 536個(gè)字符。這對于大多數(shù)語言字符集是足夠了,在Unicode中稱為基本多語言平面(BMP)。為了表示更多的字符,Unicode采用了一個(gè)策略,即每個(gè)字符使用另外16位去選擇一個(gè)增補(bǔ)平面。這種每個(gè)字符使用兩個(gè)16位碼元的策略稱為代理對。
????????在涉及增補(bǔ)平面的字符時(shí),前面討論的字符串方法就會出問題。比如,下面的例子中使用了一個(gè)笑臉表情符號,也就是一個(gè)使用代理對編碼的字符:
// "smiling face with smiling eyes" 表情符號的編碼是U+1F60A
// 0x1F60A === 128522
let message = "ab?de";
console.log(message.length); // 6
console.log(message.charAt(1)); // b
console.log(message.charAt(2)); // <?>
console.log(message.charAt(3)); // <?>
console.log(message.charAt(4)); // d
console.log(message.charCodeAt(1)); // 98
console.log(message.charCodeAt(2)); // 55357
console.log(message.charCodeAt(3)); // 56842
console.log(message.charCodeAt(4)); // 100
console.log(String.fromCodePoint(0x1F60A)); // ?
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab?de
????????這些方法仍然將16位碼元當(dāng)作一個(gè)字符,事實(shí)上索引2和索引3對應(yīng)的碼元應(yīng)該被看成一個(gè)代理對,只對應(yīng)一個(gè)字符。fromCharCode()方法仍然返回正確的結(jié)果,因?yàn)樗鼘?shí)際上是基于提供的二進(jìn)制表示直接組合成字符串。瀏覽器可以正確解析代理對(由兩個(gè)碼元構(gòu)成),并正確地將其識別為一個(gè)Unicode笑臉字符。
????????為正確解析既包含單碼元字符又包含代理對字符的字符串,可以使用codePointAt()來代替charCodeAt()。跟使用charCodeAt()時(shí)類似,codePointAt()接收16位碼元的索引并返回該索引位置上的碼點(diǎn)(code point)。碼點(diǎn)是Unicode中一個(gè)字符的完整標(biāo)識。比如,"c"的碼點(diǎn)是0x0063,而"?"的碼點(diǎn)是0x1F60A。碼點(diǎn)可能是16位,也可能是32位,而codePointAt()方法可以從指定碼元位置識別完整的碼點(diǎn)。
let message = "ab?de";
console.log(message.codePointAt(1)); // 98
console.log(message.codePointAt(2)); // 128522
console.log(message.codePointAt(3)); // 56842
console.log(message.codePointAt(4)); // 100
????????注意,如果傳入的碼元索引并非代理對的開頭,就會返回錯(cuò)誤的碼點(diǎn)。這種錯(cuò)誤只有檢測單個(gè)字符的時(shí)候才會出現(xiàn),可以通過從左到右按正確的碼元數(shù)遍歷字符串來規(guī)避。迭代字符串可以智能地識別代理對的碼點(diǎn):
console.log([..."ab?de"]); // ["a", "b", "?", "d", "e"]
????????與charCodeAt()有對應(yīng)的codePointAt()一樣,fromCharCode()也有一個(gè)對應(yīng)的fromCodePoint()。這個(gè)方法接收任意數(shù)量的碼點(diǎn),返回對應(yīng)字符拼接起來的字符串:
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab?de
console.log(String.fromCodePoint(97, 98, 128522, 100, 101)); // ab?de
2、normalize()方法
????????某些Unicode字符可以有多種編碼方式。有的字符既可以通過一個(gè)BMP字符表示,也可以通過一個(gè)代理對表示。比如:
// U+00C5:上面帶圓圈的大寫拉丁字母A
console.log(String.fromCharCode(0x00C5)); // ?
// U+212B:長度單位“?!?console.log(String.fromCharCode(0x212B)); // ?
// U+004:大寫拉丁字母A
// U+030A:上面加個(gè)圓圈
console.log(String.fromCharCode(0x0041, 0x030A)); // ?
?????????比較操作符不在乎字符看起來是什么樣的,因此這3個(gè)字符互不相等。
let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1, a2, a3); // ?, ?, ?
console.log(a1 === a2); // false
console.log(a1 === a3); // false
console.log(a2 === a3); // false
?????????為解決這個(gè)問題,Unicode提供了4種規(guī)范化形式,可以將類似上面的字符規(guī)范化為一致的格式,無論底層字符的代碼是什么。這4種規(guī)范化形式是:NFD(Normalization Form D)、
NFC(Normalization Form C)、NFKD(Normalization Form KD)和NFKC(Normalization Form KC)??梢允褂胣ormalize()方法對字符串應(yīng)用上述規(guī)范化形式,使用時(shí)需要傳入表示哪種形式的字符串:"NFD"、"NFC"、"NFKD"或"NFKC"。
????????通過比較字符串與其調(diào)用normalize()的返回值,就可以知道該字符串是否已經(jīng)規(guī)范化了:
let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
// U+00C5是對0+212B進(jìn)行NFC/NFKC規(guī)范化之后的結(jié)果
console.log(a1 === a1.normalize("NFD")); // false
console.log(a1 === a1.normalize("NFC")); // true
console.log(a1 === a1.normalize("NFKD")); // false
console.log(a1 === a1.normalize("NFKC")); // true
// U+212B是未規(guī)范化的
console.log(a2 === a2.normalize("NFD")); // false
console.log(a2 === a2.normalize("NFC")); // false
console.log(a2 === a2.normalize("NFKD")); // false
console.log(a2 === a2.normalize("NFKC")); // false
// U+0041/U+030A是對0+212B進(jìn)行NFD/NFKD規(guī)范化之后的結(jié)果
console.log(a3 === a3.normalize("NFD")); // true
console.log(a3 === a3.normalize("NFC")); // false
console.log(a3 === a3.normalize("NFKD")); // true
console.log(a3 === a3.normalize("NFKC")); // false
????????選擇同一種規(guī)范化形式可以讓比較操作符返回正確的結(jié)果:?
let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1.normalize("NFD") === a2.normalize("NFD")); // true
console.log(a2.normalize("NFKC") === a3.normalize("NFKC")); // true
console.log(a1.normalize("NFC") === a3.normalize("NFC")); // true
3、字符串操作方法
????????本節(jié)介紹幾個(gè)操作字符串值的方法。首先是concat(),用于將一個(gè)或多個(gè)字符串拼接成一個(gè)新字符串。來看下面的例子:
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"
????????在這個(gè)例子中,對stringValue調(diào)用concat()方法的結(jié)果是得到"hello world",但stringValue的值保持不變。concat()方法可以接收任意多個(gè)參數(shù),因此可以一次性拼接多個(gè)字符串,如下所示:
let stringValue = "hello ";
let result = stringValue.concat("world", "!");
console.log(result); // "hello world!"
console.log(stringValue); // "hello"
????????這個(gè)修改后的例子將字符串"world"和"!"追加到了"hello "后面。雖然concat()方法可以拼接字符串,但更常用的方式是使用加號操作符(+)。而且多數(shù)情況下,對于拼接多個(gè)字符串來說,使用加號更方便。
????????ECMAScript提供了3個(gè)從字符串中提取子字符串的方法:slice()、substr()和substring()。這3個(gè)方法都返回調(diào)用它們的字符串的一個(gè)子字符串,而且都接收一或兩個(gè)參數(shù)。第一個(gè)參數(shù)表示子字符串開始的位置,第二個(gè)參數(shù)表示子字符串結(jié)束的位置。對slice()和substring()而言,第二個(gè)參數(shù)是提取結(jié)束的位置(即該位置之前的字符會被提取出來)。對substr()而言,第二個(gè)參數(shù)表示返回的子字符串?dāng)?shù)量。任何情況下,省略第二個(gè)參數(shù)都意味著提取到字符串末尾。與concat()方法一樣,slice()、substr()和substring()也不會修改調(diào)用它們的字符串,而只會返回提取到的原始新字符串值。來看下面的例子:
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
????????在這個(gè)例子中,slice()、substr()和substring()是以相同方式被調(diào)用的,而且多數(shù)情況下返回的值也相同。如果只傳一個(gè)參數(shù)3,則所有方法都將返回"lo world",因?yàn)?hello"中"l"位置為3。如果
傳入兩個(gè)參數(shù)3和7,則slice()和substring()返回"lo w"(因?yàn)?world"中"o"在位置7,不包含),而substr()返回"lo worl",因?yàn)榈诙€(gè)參數(shù)對它而言表示返回的字符數(shù)。
????????當(dāng)某個(gè)參數(shù)是負(fù)值時(shí),這3個(gè)方法的行為又有不同。比如,slice()方法將所有負(fù)值參數(shù)都當(dāng)成字符串長度加上負(fù)參數(shù)值。
????????而substr()方法將第一個(gè)負(fù)參數(shù)值當(dāng)成字符串長度加上該值,將第二個(gè)負(fù)參數(shù)值轉(zhuǎn)換為0。substring()方法會將所有負(fù)參數(shù)值都轉(zhuǎn)換為0。看下面的例子:
let stringValue = "hello world";
console.log(stringValue.slice(-3)); // "rld"
console.log(stringValue.substring(-3)); // "hello world"
console.log(stringValue.substr(-3)); // "rld"
console.log(stringValue.slice(3, -4)); // "lo w"
console.log(stringValue.substring(3, -4)); // "hel"
console.log(stringValue.substr(3, -4)); // "" (empty string)
????????這個(gè)例子明確演示了3個(gè)方法的差異。在給slice()和substr()傳入負(fù)參數(shù)時(shí),它們的返回結(jié)果相同。這是因?yàn)?3會被轉(zhuǎn)換為8(長度加上負(fù)參數(shù)),實(shí)際上調(diào)用的是slice(8)和substr(8)。而substring()方法返回整個(gè)字符串,因?yàn)?3會轉(zhuǎn)換為0。
????????在第二個(gè)參數(shù)是負(fù)值時(shí),這3個(gè)方法各不相同。slice()方法將第二個(gè)參數(shù)轉(zhuǎn)換為7,實(shí)際上相當(dāng)于調(diào)用slice(3, 7),因此返回"low"。而substring()方法會將第二個(gè)參數(shù)轉(zhuǎn)換為0,相當(dāng)于調(diào)
用substring(3, 0),等價(jià)于substring(0, 3),這是因?yàn)檫@個(gè)方法會將較小的參數(shù)作為起點(diǎn),將較大的參數(shù)作為終點(diǎn)。對substr()來說,第二個(gè)參數(shù)會被轉(zhuǎn)換為0,意味著返回的字符串包含零個(gè)字符,因而會返回一個(gè)空字符串。
4、字符串位置方法
????????有兩個(gè)方法用于在字符串中定位子字符串:indexOf()和lastIndexOf()。這兩個(gè)方法從字符串中搜索傳入的字符串,并返回位置(如果沒找到,則返回-1)。兩者的區(qū)別在于,indexOf()方法從字符串開頭開始查找子字符串,而lastIndexOf()方法從字符串末尾開始查找子字符串。來看下面的例子:
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4
console.log(stringValue.lastIndexOf("o")); // 7
????????這里,字符串中第一個(gè)"o"的位置是4,即"hello"中的"o"。最后一個(gè)"o"的位置是7,即"world"中的"o"。如果字符串中只有一個(gè)"o",則indexOf()和lastIndexOf()返回同一個(gè)位置。
????????這兩個(gè)方法都可以接收可選的第二個(gè)參數(shù),表示開始搜索的位置。這意味著,indexOf()會從這個(gè)參數(shù)指定的位置開始向字符串末尾搜索,忽略該位置之前的字符;lastIndexOf()則會從這個(gè)參數(shù)指定的位置開始向字符串開頭搜索,忽略該位置之后直到字符串末尾的字符。下面看一個(gè)例子:
let stringValue = "hello world";
console.log(stringValue.indexOf("o", 6)); // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4
????????在傳入第二個(gè)參數(shù)6以后,結(jié)果跟前面的例子恰好相反。這一次,indexOf()返回7,因?yàn)樗鼜奈恢?(字符"w")開始向后搜索字符串,在位置7找到了"o"。而lastIndexOf()返回4,因?yàn)樗鼜奈恢?開始反向搜索至字符串開頭,因此找到了"hello"中的"o"。像這樣使用第二個(gè)參數(shù)并循環(huán)調(diào)用indexOf()或lastIndexOf(),就可以在字符串中找到所有的目標(biāo)子字符串,如下所示:
let stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
let positions = new Array();
let pos = stringValue.indexOf("e");
while(pos > -1) {
positions.push(pos);
pos = stringValue.indexOf("e", pos + 1);
}
console.log(positions); // [3,24,32,35,52]
????????這個(gè)例子逐步增大開始搜索的位置,通過indexOf()遍歷了整個(gè)字符串。首先取得第一個(gè)"e"的位置,然后進(jìn)入循環(huán),將上一次的位置加1再傳給indexOf(),確保搜索到最后一個(gè)子字符串實(shí)例之后。每個(gè)位置都保存在positions數(shù)組中,可供以后使用。
5、字符串包含方法
????????ECMAScript 6增加了3個(gè)用于判斷字符串中是否包含另一個(gè)字符串的方法:startsWith()、endsWith()和includes()。這些方法都會從字符串中搜索傳入的字符串,并返回一個(gè)表示是否包含的布爾值。它們的區(qū)別在于,startsWith()檢查開始于索引0的匹配項(xiàng),endsWith()檢查開始于索引(string.length -substring.length)的匹配項(xiàng),而includes()檢查整個(gè)字符串:
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.endsWith("baz")); // true
console.log(message.endsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false
????????startsWith()和includes()方法接收可選的第二個(gè)參數(shù),表示開始搜索的位置。如果傳入第二個(gè)參數(shù),則意味著這兩個(gè)方法會從指定位置向著字符串末尾搜索,忽略該位置之前的所有字符。下面是一個(gè)例子:
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("foo", 1)); // false
console.log(message.includes("bar")); // true
console.log(message.includes("bar", 4)); // false
?????????endsWith()方法接收可選的第二個(gè)參數(shù),表示應(yīng)該當(dāng)作字符串末尾的位置。如果不提供這個(gè)參數(shù),那么默認(rèn)就是字符串長度。如果提供這個(gè)參數(shù),那么就好像字符串只有那么多字符一樣:
let message = "foobarbaz";
console.log(message.endsWith("bar")); // false
console.log(message.endsWith("bar", 6)); // true
6、trim()方法
????????ECMAScript在所有字符串上都提供了trim()方法。這個(gè)方法會創(chuàng)建字符串的一個(gè)副本,刪除前、后所有空格符,再返回結(jié)果。比如:
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
????????由于trim()返回的是字符串的副本,因此原始字符串不受影響,即原本的前、后空格符都會保留。
?????????另外,trimLeft()和trimRight()方法分別用于從字符串開始和末尾清理空格符。
7、repeat()方法
????????ECMAScript在所有字符串上都提供了repeat()方法。這個(gè)方法接收一個(gè)整數(shù)參數(shù),表示要將字符串復(fù)制多少次,然后返回拼接所有副本后的結(jié)果。
let stringValue = "na ";
console.log(stringValue.repeat(16) + "batman");
// na na na na na na na na na na na na na na na na batman
8、padStart()和padEnd()方法
????????padStart()和padEnd()方法會復(fù)制字符串,如果小于指定長度,則在相應(yīng)一邊填充字符,直至滿足長度條件。這兩個(gè)方法的第一個(gè)參數(shù)是長度,第二個(gè)參數(shù)是可選的填充字符串,默認(rèn)為空格
(U+0020)。
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
console.log(stringValue.padEnd(6)); // "foo "
console.log(stringValue.padEnd(9, ".")); // "foo......"
????????可選的第二個(gè)參數(shù)并不限于一個(gè)字符。如果提供了多個(gè)字符的字符串,則會將其拼接并截?cái)嘁云ヅ渲付ㄩL度。此外,如果長度小于或等于字符串長度,則會返回原始字符串。
let stringValue = "foo";
console.log(stringValue.padStart(8, "bar")); // "barbafoo"
console.log(stringValue.padStart(2)); // "foo"
console.log(stringValue.padEnd(8, "bar")); // "foobarba"
console.log(stringValue.padEnd(2)); // "foo"
9、字符串迭代與解構(gòu)
????????字符串的原型上暴露了一個(gè)@@iterator方法,表示可以迭代字符串的每個(gè)字符。可以像下面這樣手動使用迭代器:
let message = "abc";
let stringIterator = message[Symbol.iterator]();
console.log(stringIterator.next()); // {value: "a", done: false}
console.log(stringIterator.next()); // {value: "b", done: false}
console.log(stringIterator.next()); // {value: "c", done: false}
console.log(stringIterator.next()); // {value: undefined, done: true}
?????????在for-of循環(huán)中可以通過這個(gè)迭代器按序訪問每個(gè)字符:
for (const c of "abcde") {
console.log(c);
}
// a
// b
// c
// d
// e
?????????有了這個(gè)迭代器之后,字符串就可以通過解構(gòu)操作符來解構(gòu)了。比如,可以更方便地把字符串分割為字符數(shù)組:
let message = "abcde";
console.log([...message]); // ["a", "b", "c", "d", "e"]
?10、字符串大小寫轉(zhuǎn)換
????????下一組方法涉及大小寫轉(zhuǎn)換,包括4個(gè)方法:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。toLowerCase()和toUpperCase()方法是原來
就有的方法,與java.lang.String中的方法同名。toLocaleLowerCase()和toLocaleUpperCase()方法旨在基于特定地區(qū)實(shí)現(xiàn)。在很多地區(qū),地區(qū)特定的方法與通用的方法是一樣的。但在少數(shù)語言中(如土耳其語),Unicode大小寫轉(zhuǎn)換需應(yīng)用特殊規(guī)則,要使用地區(qū)特定的方法才能實(shí)現(xiàn)正確轉(zhuǎn)換。下面是幾個(gè)例子:
let stringValue = "hello world";
console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD"
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLocaleLowerCase()); // "hello world"
console.log(stringValue.toLowerCase()); // "hello world"
????????這里,toLowerCase()和toLocaleLowerCase()都返回hello world,而toUpperCase()和toLocaleUpperCase()都返回HELLO WORLD。通常,如果不知道代碼涉及什么語言,則最好使用地區(qū)特定的轉(zhuǎn)換方法。
11、字符串模式匹配方法
????????String類型專門為在字符串中實(shí)現(xiàn)模式匹配設(shè)計(jì)了幾個(gè)方法。第一個(gè)就是match()方法,這個(gè)方法本質(zhì)上跟RegExp對象的exec()方法相同。match()方法接收一個(gè)參數(shù),可以是一個(gè)正則表達(dá)式字符串,也可以是一個(gè)RegExp對象。來看下面的例子:
let text = "cat, bat, sat, fat";
let pattern = /.at/;
// 等價(jià)于pattern.exec(text)
let matches = text.match(pattern);
console.log(matches.index); // 0
console.log(matches[0]); // "cat"
console.log(pattern.lastIndex); // 0
????????match()方法返回的數(shù)組與RegExp對象的exec()方法返回的數(shù)組是一樣的:第一個(gè)元素是與整個(gè)模式匹配的字符串,其余元素則是與表達(dá)式中的捕獲組匹配的字符串(如果有的話)。
????????另一個(gè)查找模式的字符串方法是search()。這個(gè)方法唯一的參數(shù)與match()方法一樣:正則表達(dá)式字符串或RegExp對象。這個(gè)方法返回模式第一個(gè)匹配的位置索引,如果沒找到則返回-1。search()始終從字符串開頭向后匹配模式??聪旅娴睦樱?/p>
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1
????????這里,search(/at/)返回1,即"at"的第一個(gè)字符在字符串中的位置。?
????????為簡化子字符串替換操作,ECMAScript提供了replace()方法。這個(gè)方法接收兩個(gè)參數(shù),第一個(gè)參數(shù)可以是一個(gè)RegExp對象或一個(gè)字符串(這個(gè)字符串不會轉(zhuǎn)換為正則表達(dá)式),第二個(gè)參數(shù)可以是一個(gè)字符串或一個(gè)函數(shù)。如果第一個(gè)參數(shù)是字符串,那么只會替換第一個(gè)子字符串。要想替換所有子字符串,第一個(gè)參數(shù)必須為正則表達(dá)式并且?guī)謽?biāo)記,如下面的例子所示:
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"
result = text.replace(/at/g, "ond");
console.log(result); // "cond, bond, sond, fond"
?????????在這個(gè)例子中,字符串"at"先傳給replace()函數(shù),而替換文本是"ond"。結(jié)果是"cat"被修改為"cond",而字符串的剩余部分保持不變。通過將第一個(gè)參數(shù)改為帶全局標(biāo)記的正則表達(dá)式,字符串中的所有"at"都被替換成了"ond"。
????????第二個(gè)參數(shù)是字符串的情況下,有幾個(gè)特殊的字符序列,可以用來插入正則表達(dá)式操作的值。ECMA-262中規(guī)定了下表中的值。
????????使用這些特殊的序列,可以在替換文本中使用之前匹配的內(nèi)容,如下面的例子所示:
let text = "cat, bat, sat, fat";
result = text.replace(/(.at)/g, "word ($1)");
console.log(result); // word (cat), word (bat), word (sat), word (fat)
?????????這里,每個(gè)以"at"結(jié)尾的詞都會被替換成"word"后跟一對小括號,其中包含捕獲組匹配的內(nèi)容$1。
????????replace()的第二個(gè)參數(shù)可以是一個(gè)函數(shù)。在只有一個(gè)匹配項(xiàng)時(shí),這個(gè)函數(shù)會收到3個(gè)參數(shù):與整個(gè)模式匹配的字符串、匹配項(xiàng)在字符串中的開始位置,以及整個(gè)字符串。在有多個(gè)捕獲組的情況下,每個(gè)匹配捕獲組的字符串也會作為參數(shù)傳給這個(gè)函數(shù),但最后兩個(gè)參數(shù)還是與整個(gè)模式匹配的開始位置和原始字符串。這個(gè)函數(shù)應(yīng)該返回一個(gè)字符串,表示應(yīng)該把匹配項(xiàng)替換成什么。使用函數(shù)作為第二個(gè)參數(shù)可以更細(xì)致地控制替換過程,如下所示:
function htmlEscape(text) {
return text.replace(/[<>"&]/g, function(match, pos, originalText) {
switch(match) {
case "<":
return "<";
case ">":
return ">";
case "&":
return "&";
case "\"":
return """;
}
});
}
console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>"));
// "<p class="greeting">Hello world!</p>"
?????????這里,函數(shù)htmlEscape()用于將一段HTML中的4個(gè)字符替換成對應(yīng)的實(shí)體:小于號、大于號、和號,還有雙引號(都必須經(jīng)過轉(zhuǎn)義)。實(shí)現(xiàn)這個(gè)任務(wù)最簡單的辦法就是用一個(gè)正則表達(dá)式查找這些字符,然后定義一個(gè)函數(shù),根據(jù)匹配的每個(gè)字符分別返回特定的HTML實(shí)體。
????????最后一個(gè)與模式匹配相關(guān)的字符串方法是split()。這個(gè)方法會根據(jù)傳入的分隔符將字符串拆分成數(shù)組。作為分隔符的參數(shù)可以是字符串,也可以是RegExp對象。(字符串分隔符不會被這個(gè)方法當(dāng)成正則表達(dá)式。)還可以傳入第二個(gè)參數(shù),即數(shù)組大小,確保返回的數(shù)組不會超過指定大小。來看下面的例子:
let colorText = "red,blue,green,yellow";
let colors1 = colorText.split(","); // ["red", "blue", "green", "yellow"]
let colors2 = colorText.split(",", 2); // ["red", "blue"]
let colors3 = colorText.split(/[^,]+/); // ["", ",", ",", ",", ""]
?????????在這里,字符串colorText是一個(gè)逗號分隔的顏色名稱符串。調(diào)用split(",")會得到包含這些顏色名的數(shù)組,基于逗號進(jìn)行拆分。要把數(shù)組元素限制為2個(gè),傳入第二個(gè)參數(shù)2即可。最后,使用正則
表達(dá)式可以得到一個(gè)包含逗號的數(shù)組。注意在最后一次調(diào)用split()時(shí),返回的數(shù)組前后包含兩個(gè)空字符串。這是因?yàn)檎齽t表達(dá)式指定的分隔符出現(xiàn)在了字符串開頭("red")和末尾("yellow")。
12、localeCompare()方法
?????????最后一個(gè)方法是localeCompare(),這個(gè)方法比較兩個(gè)字符串,返回如下3個(gè)值中的一個(gè)。
- 如果按照字母表順序,字符串應(yīng)該排在字符串參數(shù)前頭,則返回負(fù)值。(通常是-1,具體還要看與實(shí)際值相關(guān)的實(shí)現(xiàn)。)
- 如果字符串與字符串參數(shù)相等,則返回0。
- 如果按照字母表順序,字符串應(yīng)該排在字符串參數(shù)后頭,則返回正值。(通常是1,具體還要看與實(shí)際值相關(guān)的實(shí)現(xiàn)。)
????????下面是一個(gè)例子:
let stringValue = "yellow";
console.log(stringValue.localeCompare("brick")); // 1
console.log(stringValue.localeCompare("yellow")); // 0
console.log(stringValue.localeCompare("zoo")); // -1
?????????在這里,字符串"yellow"與3個(gè)不同的值進(jìn)行了比較:"brick"、"yellow"和"zoo"。"brick"按字母表順序應(yīng)該排在"yellow"前頭,因此localeCompare()返回1。"yellow"等于"yellow",因此"localeCompare()"返回0。最后,"zoo"在"yellow"后面,因此localeCompare()返回-1。強(qiáng)調(diào)一
下,因?yàn)榉祷氐木唧w值可能因具體實(shí)現(xiàn)而異,所以最好像下面的示例中一樣使用localeCompare():
function determineOrder(value) {
let result = stringValue.localeCompare(value);
if (result < 0) {
console.log(`The string 'yellow' comes before the string '${value}'.`);
} else if (result > 0) {
console.log(`The string 'yellow' comes after the string '${value}'.`);
} else {
console.log(`The string 'yellow' is equal to the string '${value}'.`);
}
}
determineOrder("brick");
determineOrder("yellow");
determineOrder("zoo");
????????這樣一來,就可以保證在所有實(shí)現(xiàn)中都能正確判斷字符串的順序了。?
????????localeCompare()的獨(dú)特之處在于,實(shí)現(xiàn)所在的地區(qū)(國家和語言)決定了這個(gè)方法如何比較字符串。在美國,英語是ECMAScript實(shí)現(xiàn)的標(biāo)準(zhǔn)語言,localeCompare()區(qū)分大小寫,大寫字母排在小寫字母前面。但其他地區(qū)未必是這種情況。
13、HTML方法
????????早期的瀏覽器開發(fā)商認(rèn)為使用JavaScript動態(tài)生成HTML標(biāo)簽是一個(gè)需求。因此,早期瀏覽器擴(kuò)展了規(guī)范,增加了輔助生成HTML標(biāo)簽的方法。下表總結(jié)了這些HTML方法。不過,這些方法基本上已經(jīng)沒有人使用了,因?yàn)榻Y(jié)果通常不是語義化的標(biāo)記。
?
5.4 單例內(nèi)置對象
????????ECMA-262對內(nèi)置對象的定義是“任何由ECMAScript實(shí)現(xiàn)提供、與宿主環(huán)境無關(guān),并在ECMAScript程序開始執(zhí)行時(shí)就存在的對象”。這就意味著,開發(fā)者不用顯式地實(shí)例化內(nèi)置對象,因?yàn)樗鼈円呀?jīng)實(shí)例化好了。前面我們已經(jīng)接觸了大部分內(nèi)置對象,包括Object、Array和String。本節(jié)介紹ECMA-262定義的另外兩個(gè)單例內(nèi)置對象:Global和Math。
5.4.1 Global
????????Global對象是ECMAScript中最特別的對象,因?yàn)榇a不會顯式地訪問它。ECMA-262規(guī)定Global對象為一種兜底對象,它所針對的是不屬于任何對象的屬性和方法。事實(shí)上,不存在全局變量或全局函數(shù)這種東西。在全局作用域中定義的變量和函數(shù)都會變成Global對象的屬性 。本書前面介紹的函數(shù),包括isNaN()、isFinite()、parseInt()和parseFloat(),實(shí)際上都是Global對象的方法。除了這些,Global對象上還有另外一些方法。
1、URL編碼方式
????????encodeURI()和encodeURIComponent()方法用于編碼統(tǒng)一資源標(biāo)識符(URI),以便傳給瀏覽器。有效的URI不能包含某些字符,比如空格。使用URI編碼方法來編碼URI可以讓瀏覽器能夠理解它們,同時(shí)又以特殊的UTF-8編碼替換掉所有無效字符。
????????ecnodeURI()方法用于對整個(gè)URI進(jìn)行編碼,比如"www.wrox.com/illegal value.js"。而encodeURIComponent()方法用于編碼URI中單獨(dú)的組件,比如前面URL中的"illegal value.js"。這兩個(gè)方法的主要區(qū)別是,encodeURI()不會編碼屬于URL組件的特殊字符,比如冒號、斜杠、問號、井號,而encodeURIComponent()會編碼它發(fā)現(xiàn)的所有非標(biāo)準(zhǔn)字符。來看下面的例子:
let uri = "http://www.wrox.com/illegal value.js#start";
// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURI(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri));
????????這里使用encodeURI()編碼后,除空格被替換為%20之外,沒有任何變化。而encodeURIComponent()方法將所有非字母字符都替換成了相應(yīng)的編碼形式。這就是使用encodeURI()編碼整個(gè)URI,但只使用encodeURIComponent()編碼那些會追加到已有URI后面的字符串的原因。
注意 ????????一般來說,使用encodeURIComponent()應(yīng)該比使用encodeURI()的頻率更高,這是因?yàn)榫幋a查詢字符串參數(shù)比編碼基準(zhǔn)URI的次數(shù)更多。
????????與encodeURI()和encodeURIComponent()相對的是decodeURI()和decodeURIComponent()。decodeURI()只對使用encodeURI()編碼過的字符解碼。例如,%20會被替換為空格,但%23不會被替換為井號(#),因?yàn)榫柌皇怯蒭ncodeURI()替換的。類似地,decodeURIComponent()解碼所有被encodeURIComponent()編碼的字符,基本上就是解碼所有特殊值。來看下面的例子:
let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start";
// http%3A%2F%2Fwww.wrox.com%2Fillegal value.js%23start
console.log(decodeURI(uri));
// http:// www.wrox.com/illegal value.js#start
console.log(decodeURIComponent(uri));
????????這里,uri變量中包含一個(gè)使用encodeURIComponent()編碼過的字符串。首先輸出的是使用decodeURI()解碼的結(jié)果,可以看到只用空格替換了%20。然后是使用decodeURIComponent()解碼的結(jié)果,其中替換了所有特殊字符,并輸出了沒有包含任何轉(zhuǎn)義的字符串。(這個(gè)字符串不是有效的URL。)
注意 ????????URI方法encodeURI()、encodeURIComponent()、decodeURI()和
decodeURIComponent()取代了escape()和unescape()方法,后者在ECMA-262第3版中就已經(jīng)廢棄了。URI方法始終是首選方法,因?yàn)樗鼈儗λ蠻nicode字符進(jìn)行編碼,而原來的方法只能正確編碼ASCII字符。不要在生產(chǎn)環(huán)境中使用escape()和unescape()。
2、eval()方法
????????最后一個(gè)方法可能是整個(gè)ECMAScript語言中最強(qiáng)大的了,它就是eval()。這個(gè)方法就是一個(gè)完整的ECMAScript解釋器,它接收一個(gè)參數(shù),即一個(gè)要執(zhí)行的ECMAScript(JavaScript)字符串。來看一個(gè)例子:
eval("console.log('hi')");
?上面這行代碼的功能與下一行等價(jià):
console.log("hi");
????????當(dāng)解釋器發(fā)現(xiàn)eval()調(diào)用時(shí),會將參數(shù)解釋為實(shí)際的ECMAScript語句,然后將其插入到該位置。通過eval()執(zhí)行的代碼屬于該調(diào)用所在上下文,被執(zhí)行的代碼與該上下文擁有相同的作用域鏈。這意味著定義在包含上下文中的變量可以在eval()調(diào)用內(nèi)部被引用,比如下面這個(gè)例子:
let msg = "hello world";
eval("console.log(msg)"); // "hello world"
????????這里,變量msg是在eval()調(diào)用的外部上下文中定義的,而console.log()顯示了文本"hello world"。這是因?yàn)榈诙写a會被替換成一行真正的函數(shù)調(diào)用代碼。類似地,可以在eval()內(nèi)部
定義一個(gè)函數(shù)或變量,然后在外部代碼中引用,如下所示:? ? ?
eval("function sayHi() { console.log('hi'); }");
sayHi();
?????????這里,函數(shù)sayHi()是在eval()內(nèi)部定義的。因?yàn)樵撜{(diào)用會被替換為真正的函數(shù)定義,所以才可能在下一行代碼中調(diào)用sayHi()。對于變量也是一樣的:
eval("let msg = 'hello world';");
console.log(msg); // Reference Error: msg is not defined
????????通過eval()定義的任何變量和函數(shù)都不會被提升,這是因?yàn)樵诮馕龃a的時(shí)候,它們是被包含在一個(gè)字符串中的。它們只是在eval()執(zhí)行的時(shí)候才會被創(chuàng)建。
msg是一個(gè)普通變量,它的作用域是eval()函數(shù)的局部作用域,eval()執(zhí)行完畢后msg就被銷毀了,無法在外部被訪問。而sayHi函數(shù)是一種特殊的對象,eval()執(zhí)行完畢后它不會被立即銷毀,而是會在垃圾回收機(jī)制運(yùn)行并將其銷毀之前一直存在在內(nèi)存中,所以可以在外部訪問
????????通過eval()定義的任何變量和函數(shù)都不會被提升,這是因?yàn)樵诮馕龃a的時(shí)候,它們是被包含在一個(gè)字符串中的。它們只是在eval()執(zhí)行的時(shí)候才會被創(chuàng)建。
????????在嚴(yán)格模式下,在eval()內(nèi)部創(chuàng)建的變量和函數(shù)無法被外部訪問。換句話說,最后兩個(gè)例子會報(bào)錯(cuò)。同樣,在嚴(yán)格模式下,賦值給eval也會導(dǎo)致錯(cuò)誤:?
"use strict";
eval = "hi"; // 導(dǎo)致錯(cuò)誤
注意 ????????解釋代碼字符串的能力是非常強(qiáng)大的,但也非常危險(xiǎn)。在使用eval()的時(shí)候必須極為慎重,特別是在解釋用戶輸入的內(nèi)容時(shí)。因?yàn)檫@個(gè)方法會對XSS利用暴露出很大的攻擊面。惡意用戶可能插入會導(dǎo)致你網(wǎng)站或應(yīng)用崩潰的代碼。
3、Global對象屬性
????????Global對象有很多屬性,其中一些前面已經(jīng)提到過了。像undefined、NaN和Infinity等特殊值都是Global對象的屬性。此外,所有原生引用類型構(gòu)造函數(shù),比如Object和Function,也都是Global對象的屬性。下表列出了所有這些屬性。
4、window對象
?????????雖然ECMA-262沒有規(guī)定直接訪問Global對象的方式,但瀏覽器將window對象實(shí)現(xiàn)為Global對象的代理。因此,所有全局作用域中聲明的變量和函數(shù)都變成了window的屬性。來看下面的例子:
var color = "red";
function sayColor() {
console.log(window.color);
}
window.sayColor(); // "red"
????????這里定義了一個(gè)名為color的全局變量和一個(gè)名為sayColor()的全局函數(shù)。在sayColor()內(nèi)部,通過window.color訪問了color變量,說明全局變量變成了window的屬性。接著,又通過window對象直接調(diào)用了window.sayColor()函數(shù),從而輸出字符串。
????????另一種獲取Global對象的方式是使用如下的代碼:
let global = function() {
return this;
}();
????????這段代碼創(chuàng)建一個(gè)立即調(diào)用的函數(shù)表達(dá)式,返回了this的值。如前所述,當(dāng)一個(gè)函數(shù)在沒有明確(通過成為某個(gè)對象的方法,或者通過call()/apply())指定this值的情況下執(zhí)行時(shí),this值等于Global對象。因此,調(diào)用一個(gè)簡單返回this的函數(shù)是在任何執(zhí)行上下文中獲取Global對象的通用方式。
5.4.2? Math
????????ECMAScript提供了Math對象作為保存數(shù)學(xué)公式、信息和計(jì)算的地方。Math對象提供了一些輔助計(jì)算的屬性和方法。
注意 ????????Math對象上提供的計(jì)算要比直接在JavaScript實(shí)現(xiàn)的快得多,因?yàn)镸ath對象上的計(jì)算使用了JavaScript引擎中更高效的實(shí)現(xiàn)和處理器指令。但使用Math計(jì)算的問題是精度會因?yàn)g覽器、操作系統(tǒng)、指令集和硬件而異。
1、Math對象屬性
????????Math對象有一些屬性,主要用于保存數(shù)學(xué)中的一些特殊值。下表列出了這些屬性。
2、min()和max()方法
????????Math對象也提供了很多輔助執(zhí)行簡單或復(fù)雜數(shù)學(xué)計(jì)算的方法。min()和max()方法用于確定一組數(shù)值中的最小值和最大值。這兩個(gè)方法都接收任意多個(gè)參數(shù),如下面的例子所示:
let max = Math.max(3, 54, 32, 16);
console.log(max); // 54
let min = Math.min(3, 54, 32, 16);
console.log(min); // 3
????????在3、54、32和16中,Math.max()返回54,Math.min()返回3。使用這兩個(gè)方法可以避免使用額外的循環(huán)和if語句來確定一組數(shù)值的最大最小值。
????????要知道數(shù)組中的最大值和最小值,可以像下面這樣使用擴(kuò)展操作符:
let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max(...val);
3、舍入方法
????????接下來是用于把小數(shù)值舍入為整數(shù)的4個(gè)方法:Math.ceil()、Math.floor()、Math.round()和Math.fround()。這幾個(gè)方法處理舍入的方式如下所述。
- Math.ceil()方法始終向上舍入為最接近的整數(shù)。
- Math.floor()方法始終向下舍入為最接近的整數(shù)。
- Math.round()方法執(zhí)行四舍五入。
- Math.fround()方法返回?cái)?shù)值最接近的單精度(32位)浮點(diǎn)值表示。
????????以下示例展示了這些方法的用法:
console.log(Math.ceil(25.9)); // 26
console.log(Math.ceil(25.5)); // 26
console.log(Math.ceil(25.1)); // 26
console.log(Math.round(25.9)); // 26
console.log(Math.round(25.5)); // 26
console.log(Math.round(25.1)); // 25
console.log(Math.fround(0.4)); // 0.4000000059604645
console.log(Math.fround(0.5)); // 0.5
console.log(Math.fround(25.9)); // 25.899999618530273
console.log(Math.floor(25.9)); // 25
console.log(Math.floor(25.5)); // 25
console.log(Math.floor(25.1)); // 25
????????對于25和26(不包含)之間的所有值,Math.ceil()都會返回26,因?yàn)樗冀K向上舍入。Math.round()只在數(shù)值大于等于25.5時(shí)返回26,否則返回25。最后,Math.floor()對所有25和26(不包含)之間的值都返回25。
4、random()方法
????????Math.random()方法返回一個(gè)0~1范圍內(nèi)的隨機(jī)數(shù),其中包含0但不包含1。對于希望顯示隨機(jī)名言或隨機(jī)新聞的網(wǎng)頁,這個(gè)方法是非常方便的??梢曰谌缦鹿绞褂肕ath.random()從一組整數(shù)中隨機(jī)選擇一個(gè)數(shù):
number = Math.floor(Math.random() * total_number_of_choices + first_possible_value
????????這里使用了Math.floor()方法,因?yàn)镸ath.random()始終返回小數(shù),即便乘以一個(gè)數(shù)再加上一個(gè)數(shù)也是小數(shù)。因此,如果想從1~10范圍內(nèi)隨機(jī)選擇一個(gè)數(shù),代碼就是這樣的:
let num = Math.floor(Math.random() * 10 + 1);
????????這樣就有10個(gè)可能的值(1~10),其中最小的值是1。如果想選擇一個(gè)2~10范圍內(nèi)的值,則代碼就要寫成這樣:
let num = Math.floor(Math.random() * 9 + 2);
????????2~10只有9個(gè)數(shù),所以可選總數(shù)(total_number_of_choices)是9,而最小可能的值(first_possible_value)是2。很多時(shí)候,通過函數(shù)來算出可選總數(shù)和最小可能的值可能更方便,比如:
function selectFrom(lowerValue, upperValue) {
let choices = upperValue - lowerValue + 1;
return Math.floor(Math.random() * choices + lowerValue);
}
let num = selectFrom(2,10);
console.log(num); // 2~10范圍內(nèi)的值,其中包含2和10
????????這里的函數(shù)selectFrom()接收兩個(gè)參數(shù):應(yīng)該返回的最小值和最大值。通過將這兩個(gè)值相減再加1得到可選總數(shù),然后再套用上面的公式。于是,調(diào)用selectFrom(2,10)就可以從2~10(包含)范圍內(nèi)選擇一個(gè)值了。使用這個(gè)函數(shù),從一個(gè)數(shù)組中隨機(jī)選擇一個(gè)元素就很容易,比如:
let colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
let color = colors[selectFrom(0, colors.length-1)];
????????在這個(gè)例子中,傳給selecFrom()的第二個(gè)參數(shù)是數(shù)組長度減1,即數(shù)組最大的索引值。
注意 ????????Math.random()方法在這里出于演示目的是沒有問題的。如果是為了加密而需要生成隨機(jī)數(shù)(傳給生成器的輸入需要較高的不確定性),那么建議使用window.crypto.getRandomValues()。
5、其他方法
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
5.5 小結(jié)
????????JavaScript中的對象稱為引用值,幾種內(nèi)置的引用類型可用于創(chuàng)建特定類型的對象。
- 引用值與傳統(tǒng)面向?qū)ο缶幊陶Z言中的類相似,但實(shí)現(xiàn)不同。
- Date類型提供關(guān)于日期和時(shí)間的信息,包括當(dāng)前日期、時(shí)間及相關(guān)計(jì)算。
- RegExp類型是ECMAScript支持正則表達(dá)式的接口,提供了大多數(shù)基礎(chǔ)的和部分高級的正則表達(dá)式功能。
????????JavaScript比較獨(dú)特的一點(diǎn)是,函數(shù)實(shí)際上是Function類型的實(shí)例,也就是說函數(shù)也是對象。因?yàn)楹瘮?shù)也是對象,所以函數(shù)也有方法,可以用于增強(qiáng)其能力。
????????由于原始值包裝類型的存在,JavaScript中的原始值可以被當(dāng)成對象來使用。有3種原始值包裝類型:Boolean、Number和String。它們都具備如下特點(diǎn)。文章來源:http://www.zghlxwxcb.cn/news/detail-795339.html
- 每種包裝類型都映射到同名的原始類型。
- 以讀模式訪問原始值時(shí),后臺會實(shí)例化一個(gè)原始值包裝類型的對象,借助這個(gè)對象可以操作相應(yīng)的數(shù)據(jù)。
- 涉及原始值的語句執(zhí)行完畢后,包裝對象就會被銷毀。
????????當(dāng)代碼開始執(zhí)行時(shí),全局上下文中會存在兩個(gè)內(nèi)置對象:Global和Math。其中,Global對象在大多數(shù)ECMAScript實(shí)現(xiàn)中無法直接訪問。不過,瀏覽器將其實(shí)現(xiàn)為window對象。所有全局變量和函數(shù)都是Global對象的屬性。Math對象包含輔助完成復(fù)雜計(jì)算的屬性和方法。文章來源地址http://www.zghlxwxcb.cn/news/detail-795339.html
到了這里,關(guān)于第五章 基本引用類型(下)——原始值包裝類型、的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!