《JavaScript 闖關記》之基本包裝型別

劼哥stone發表於2016-11-15

為了便於操作基本型別值,JavaScript 還提供了3個特殊的引用型別:BooleanNumberString。實際上,每當讀取一個基本型別值的時候,後臺就會建立一個對應的基本包裝型別的物件,從而讓我們能夠呼叫一些方法來操作這些資料。來看下面的例子。

var s1 = "some text";
var s2 = s1.substring(2);複製程式碼

這個例子中的變數 s1 包含一個字串,字串當然是基本型別值。而下一行呼叫了 s1substring() 方法,並將返回的結果儲存在了 s2 中。我們知道,基本型別值不是物件,因而從邏輯上講它們不應該有方法(儘管如我們所願,它們確實有方法)。其實,為了讓我們實現這種直觀的操作,後臺已經自動完成了一系列的處理。當第二行程式碼訪問 s1 時,訪問過程處於一種讀取模式,也就是要從記憶體中讀取這個字串的值。而在讀取模式中訪問字串時,後臺都會自動完成下列處理。

  1. 建立 String 型別的一個例項;
  2. 在例項上呼叫指定的方法;
  3. 銷燬這個例項。

可以將以上三個步驟想象成是執行了下列 JavaScript 程式碼。

var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;複製程式碼

經過此番處理,基本的字串值就變得跟物件一樣了。而且,上面這三個步驟也分別適用於 BooleanNumber 型別對應的布林值和數字值。

引用型別與基本包裝型別的主要區別就是物件的生存期。使用 new 操作符建立的引用型別的例項,在執行流離開當前作用域之前都一直儲存在記憶體中。而自動建立的基本包裝型別的物件,則只存在於一行程式碼的執行瞬間,然後立即被銷燬。這意味著我們不能在執行時為基本型別值新增屬性和方法。來看下面的例子:

var s1 = "some text";
s1.color = "red";
console.log(s1.color);   // undefined複製程式碼

當然,可以顯式地呼叫 BooleanNumberString 來建立基本包裝型別的物件。不過,應該在絕對必要的情況下再這樣做,因為這種做法很容易讓人分不清自己是在處理「基本型別」還是「引用型別」的值。對基本包裝型別的例項呼叫 typeof 會返回 "object",而且所有基本包裝型別的物件都會被轉換為布林值 true

Object 建構函式也會像工廠方法一樣,根據傳入值的型別返回相應基本包裝型別的例項。例如:

var obj = new Object("some text");
console.log(obj instanceof String);   // true複製程式碼

把字串傳給 Object 建構函式,就會建立 String 的例項;而傳入數值引數會得到 Number 的例項,傳入布林值引數就會得到 Boolean 的例項。

要注意的是,使用 new 呼叫基本包裝型別的建構函式,與直接呼叫同名的轉型函式是不一樣的。 例如:

var value = "25";
var number = Number(value);  // 轉型函式
console.log(typeof number);  // "number"

var obj = new Number(value); // 建構函式
console.log(typeof obj);     // "object"複製程式碼

儘管我們不建議顯式地建立基本包裝型別的物件,但它們操作基本型別值的能力還是相當重要的。而每個基本包裝型別都提供了操作相應值的便捷方法。

Boolean 型別

Boolean 型別是與布林值對應的引用型別。要建立 Boolean 物件,可以像下面這樣呼叫 Boolean 建構函式並傳入 truefalse 值。

var booleanObject = new Boolean(true);複製程式碼

Boolean 型別的例項重寫了 valueOf() 方法,返回基本型別值 truefalse;重寫了 toString() 方法,返回字串 "true""false"。可是,Boolean 物件在 JavaScript 中的用處不大,因為它經常會造成人們的誤解。其中最常見的問題就是在布林表示式中使用 Boolean 物件,例如:

var falseObject = new Boolean(false);
var result = falseObject && true;
console.log(result);  // true

var falseValue = false;
result = falseValue && true;
console.log(result);  // false複製程式碼

在這個例子中,我們使用 false 值建立了一個 Boolean 物件。然後,將這個物件與基本型別值 true 構成了邏輯與表示式。在布林運算中,false && true 等於 false。可是,示例中的這行程式碼是對 falseObject 而不是對它的值 false 進行求值。布林表示式中的所有物件都會被轉換為 true,因此 falseObject 物件在布林表示式中代表的是 true。結果,true && true 當然就等於 true 了。

基本型別與引用型別的布林值還有兩個區別。首先,typeof 操作符對基本型別返回 "boolean",而對引用型別返回 "object"。其次,由於 Boolean 物件是 Boolean 型別的例項,所以使用 instanceof 操作符測試 Boolean 物件會返回 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 物件之間的區別非常重要,我們的建議是永遠不要使用 Boolean 物件。

Number 型別

Number 是與數字值對應的引用型別。要建立 Number 物件,可以在呼叫 Number 建構函式時向其中傳遞相應的數值。下面是一個例子。

var numberObject = new Number(10);複製程式碼

Boolean 型別一樣,Number 型別也重寫了 valueOf()toLocaleString()toString() 方法。重寫後的 valueOf() 方法返回物件表示的基本型別的數值,另外兩個方法則返回字串形式的數值。可以為 toString() 方法傳遞一個表示基數的引數,告訴它返回幾進位制數值的字串形式,如下面的例子所示。

var 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 型別還提供了一些用於將數值格式化為字串的方法。其中,toFixed() 方法會按照指定的小數位返回數值的字串表示,例如:

var num = 10;
console.log(num.toFixed(2));    // "10.00"複製程式碼

這裡給 toFixed() 方法傳入了數值 2,意思是顯示幾位小數。於是,這個方法返回了 "10.00",即以 0 填補了必要的小數位。如果數值本身包含的小數位比指定的還多,那麼接近指定的最大小數位的值就會舍入,如下面的例子所示。

var num = 10.005;
console.log(num.toFixed(2));    // "10.01"複製程式碼

能夠自動舍入的特性,使得 toFixed() 方法很適合處理貨幣值。

但需要注意的是,不同瀏覽器給這個方法設定的舍入規則可能會有所不同。

在給 toFixed() 傳入0的情況下,IE8 及之前版本不能正確舍入範圍在{(-0.94,-0.5],[0.5,0.94)}之間的值。對於這個範圍內的值,IE8 會返回0,而不是-1或1;其他瀏覽器都能返回正確的值。IE9 修復了這個問題。

toFixed() 方法可以表示帶有0到20個小數位的數值。但這只是標準實現的範圍,有些瀏覽器也可能支援更多位數。

另外可用於格式化數值的方法是 toExponential(),該方法返回以指數表示法(也稱 e 表示法)表示的數值的字串形式。與 toFixed() 一樣,toExponential() 也接收一個引數,而且該引數同樣也是指定輸出結果中的小數位數。看下面的例子。

var num = 10;
console.log(num.toExponential(1));     // "1.0e+1"複製程式碼

以上程式碼輸出了 "1.0e+1";不過,這麼小的數值一般不必使用 e 表示法。如果你想得到表示某個數值的最合適的格式,就應該使用 toPrecision() 方法。

對於一個數值來說,toPrecision() 方法可能會返回固定大小(fixed)格式,也可能返回指數(exponential)格式;具體規則是看哪種格式最合適。這個方法接收一個引數,即表示數值的所有數字的位數(不包括指數部分)。請看下面的例子。

var num = 99;
console.log(num.toPrecision(1));     // "1e+2"
console.log(num.toPrecision(2));     // "99"
console.log(num.toPrecision(3));     // "99.0"複製程式碼

以上程式碼首先完成的任務是以一位數來表示 99,結果是 "1e+2",即 100。因為一位數無法準確地表示 99,因此 toPrecision() 就將它向上舍入為 100,這樣就可以使用一位數來表示它了。而接下來的用兩位數表示 99,當然還是 "99"。最後,在想以三位數表示 99 時,toPrecision() 方法返回了 "99.0"。實際上,toPrecision() 會根據要處理的數值決定到底是呼叫 toFixed() 還是呼叫 toExponential()。而這三個方法都可以通過向上或向下舍入,做到以最準確的形式來表示帶有正確小數位的值。

toPrecision() 方法可以表現1到21位小數。但這只是標準實現的範圍,有些瀏覽器也可能支援更多位數。

Boolean 物件類似,Number 物件也以後臺方式為數值提供了重要的功能。但與此同時,我們仍然不建議直接例項化 Number 型別,而原因與顯式建立 Boolean 物件一樣。具體來講,就是在使用 typeofinstanceof 操作符測試基本型別數值與引用型別數值時,得到的結果完全不同,如下面的例子所示。

var numberObject = new Number(10);
var numberValue = 10;
console.log(typeof numberObject);   // "object"
console.log(typeof numberValue);    // "number"
console.log(numberObject instanceof Number);  // true
console.log(numberValue instanceof Number);   // false複製程式碼

String 型別

String 型別是字串的物件包裝型別,可以像下面這樣使用 String 建構函式來建立。

var stringObject = new String("hello world");複製程式碼

String 物件的方法也可以在所有基本的字串值中訪問到。其中,繼承的 valueOf()toLocaleString()toString() 方法,都返回物件所表示的基本字串值。

String 型別的每個例項都有一個 length 屬性,表示字串中包含多個字元。來看下面的例子。

var stringValue = "hello world";
console.log(stringValue.length);     // 11複製程式碼

應該注意的是,即使字串中包含雙位元組字元(不是佔一個位元組的 ASCII 字元),每個字元也仍然算一個字元。例如:

var stringValue = "大家好";
console.log(stringValue.length);     // 3複製程式碼

String 型別提供了很多方法,用於輔助完成對 JavaScript 中字串的解析和操作。

字元方法

兩個用於訪問字串中特定字元的方法是:charAt()charCodeAt()。這兩個方法都接收一個引數,即基於0的字元位置。其中,charAt() 方法以單字元字串的形式返回給定位置的那個字元(JavaScript 中沒有字元型別)。例如:

var stringValue = "hello world";
console.log(stringValue.charAt(1));  // "e"複製程式碼

如果你想得到的不是字元而是字元編碼,那麼就要像下面這樣使用 charCodeAt() 了。例如:

var stringValue = "hello world";
console.log(stringValue.charCodeAt(1));  // 101,101是小寫字母"e"的字元編碼複製程式碼

ECMAScript 5 還定義了另一個訪問個別字元的方法。在支援瀏覽器中,可以使用方括號加數字索引來訪問字串中的特定字元,如下面的例子所示。

var stringValue = "hello world";
console.log(stringValue[1]);   // "e"複製程式碼

字串操作方法

下面介紹與操作字串有關的幾個方法。第一個就是 concat(),用於將一或多個字串拼接起來,返回拼接得到的新字串。先來看一個例子。

var stringValue = "hello ";
var result = stringValue.concat("world");

console.log(result);        // "hello world"
console.log(stringValue);   // "hello"複製程式碼

實際上,concat() 方法可以接受任意多個引數,也就是說可以通過它拼接任意多個字串。再看一個例子:

var stringValue = "hello ";
var result = stringValue.concat("world", "!");

console.log(result);        // "hello world!"
console.log(stringValue);   // "hello"複製程式碼

雖然 concat() 是專門用來拼接字串的方法,但實踐中使用更多的還是加號操作符 + 。而且,使用加號操作符 + 在大多數情況下都比使用 concat()方法要簡便易行(特別是在拼接多個字串的情況下)。

JavaScript 還提供了三個基於子字串建立新字串的方法:slice()substr()substring()。這三個方法都會返回被操作字串的一個子字串,而且也都接受一或兩個引數。第一個引數指定子字串的開始位置,第二個引數(在指定的情況下)表示子字串到哪裡結束。具體來說,slice()substring() 的第二個引數指定的是子字串最後一個字元後面的位置。而 substr() 的第二個引數指定的則是返回的字元個數。如果沒有給這些方法傳遞第二個引數,則將字串的長度作為結束位置。與 concat() 方法一樣,slice()substr()substring()也不會修改字串本身的值,它們只是返回一個基本型別的字串值,對原始字串沒有任何影響。請看下面的例子。

var 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"複製程式碼

在傳遞給這些方法的引數是負值的情況下,它們的行為就不盡相同了。其中,slice() 方法會將傳入的負值與字串的長度相加,substr() 方法將負的第一個引數加上字串的長度,而將負的第二個引數轉換為0。最後,substring() 方法會把所有負值引數都轉換為0。下面來看例子。

var 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));       //""(空字串)複製程式碼

字串位置方法

有兩個可以從字串中查詢子字串的方法:indexOf()lastIndexOf()。這兩個方法都是從一個字串中搜尋給定的子字串,然後返子字串的位置(如果沒有找到該子字串,則返回-1)。這兩個方法的區別在於:indexOf() 方法從字串的開頭向後搜尋子字串,而 lastIndexOf() 方法是從字串的末尾向前搜尋子字串。還是來看一個例子吧。

var stringValue = "hello world";
console.log(stringValue.indexOf("o"));             // 4
console.log(stringValue.lastIndexOf("o"));         // 7複製程式碼

這兩個方法都可以接收可選的第二個引數,表示從字串中的哪個位置開始搜尋。換句話說,indexOf()會從該引數指定的位置向後搜尋,忽略該位置之前的所有字元;而lastIndexOf()則會從指定的位置向前搜尋,忽略該位置之後的所有字元。看下面的例子。

var stringValue = "hello world";
console.log(stringValue.indexOf("o", 6));          // 7
console.log(stringValue.lastIndexOf("o", 6));      // 4複製程式碼

在使用第二個引數的情況下,可以通過迴圈呼叫 indexOf()lastIndexOf() 來找到所有匹配的子字串,如下面的例子所示:

var stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
var positions = new Array();
var pos = stringValue.indexOf("e");

while(pos > -1){
    positions.push(pos);
    pos = stringValue.indexOf("e", pos + 1);
}
console.log(positions);    // "3,24,32,35,52"複製程式碼

trim() 方法

ECMAScript 5 為所有字串定義了 trim() 方法。這個方法會建立一個字串的副本,刪除前置及字尾的所有空格,然後返回結果。例如:

var stringValue = "   hello world   ";
var trimmedStringValue = stringValue.trim();
console.log(stringValue);            // "   hello world   "
console.log(trimmedStringValue);     // "hello world"複製程式碼

字串大小寫轉換方法

JavaScript 中涉及字串大小寫轉換的方法有4個:toLowerCase()toLocaleLowerCase()toUpperCase()toLocaleUpperCase()。其中,toLowerCase()toUpperCase() 是兩個經典的方法,借鑑自 java.lang.String 中的同名方法。而 toLocaleLowerCase()toLocaleUpperCase() 方法則是針對特定地區的實現。對有些地區來說,針對地區的方法與其通用方法得到的結果相同,但少數語言(如土耳其語)會為 Unicode 大小寫轉換應用特殊的規則,這時候就必須使用針對地區的方法來保證實現正確的轉換。以下是幾個例子。

var 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"複製程式碼

一般來說,在不知道自己的程式碼將在哪種語言環境中執行的情況下,還是使用針對地區的方法更穩妥一些。

字串的模式匹配方法

String 型別定義了幾個用於在字串中匹配模式的方法。第一個方法就是 match(),在字串上呼叫這個方法,本質上與呼叫 RegExpexec() 方法相同。match() 方法只接受一個引數,要麼是一個正規表示式,要麼是一個 RegExp 物件。來看下面的例子。

var text = "cat, bat, sat, fat"; 
var pattern = /.at/;

// 與pattern.exec(text)相同
var matches = text.match(pattern);
console.log(matches.index);               // 0
console.log(matches[0]);                  // "cat"
console.log(pattern.lastIndex);           // 0複製程式碼

另一個用於查詢模式的方法是 search()。這個方法的唯一引數與 match() 方法的引數相同:由字串或 RegExp 物件指定的一個正規表示式。search() 方法返回字串中第一個匹配項的索引;如果沒有找到匹配項,則返回-1。而且,search() 方法始終是從字串開頭向後查詢模式。看下面的例子。

var text = "cat, bat, sat, fat"; 
var pos = text.search(/at/);
console.log(pos);   // 1,即"at"第一次出現的位置複製程式碼

為了簡化替換子字串的操作,JavaScript 提供了 replace() 方法。這個方法接受兩個引數:第一個引數可以是一個 RegExp 物件或者一個字串(這個字串不會被轉換成正規表示式),第二個引數可以是一個字串或者一個函式。如果第一個引數是字串,那麼只會替換第一個子字串。要想替換所有子字串,唯一的辦法就是提供一個正規表示式,而且要指定全域性 g 標誌,如下所示。

var text = "cat, bat, sat, fat"; 
var 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"複製程式碼

最後一個與模式匹配有關的方法是 split(),這個方法可以基於指定的分隔符將一個字串分割成多個子字串,並將結果放在一個陣列中。分隔符可以是字串,也可以是一個 RegExp 物件(這個方法不會將字串看成正規表示式)。split() 方法可以接受可選的第二個引數,用於指定陣列的大小,以便確保返回的陣列不會超過既定大小。請看下面的例子。

var colorText = "red,blue,green,yellow";
var colors1 = colorText.split(",");          // ["red", "blue", "green", "yellow"]
var colors2 = colorText.split(",", 2);       // ["red", "blue"]複製程式碼

localeCompare() 方法

這個方法比較兩個字串,並返回下列值中的一個:

  • 如果字串在字母表中應該排在字串引數之前,則返回一個負數(大多數情況下是-1,具體的值要視實現而定);
  • 如果字串等於字串引數,則返回0;
  • 如果字串在字母表中應該排在字串引數之後,則返回一個正數(大多數情況下是1,具體的值同樣要視實現而定)。

下面是幾個例子。

var stringValue = "yellow";       
console.log(stringValue.localeCompare("brick"));    // 1
console.log(stringValue.localeCompare("yellow"));   // 0
console.log(stringValue.localeCompare("zoo"));      // -1複製程式碼

這個例子比較了字串 "yellow" 和另外幾個值:"brick""yellow""zoo"。因為 "brick" 在字母表中排在 "yellow" 之前,所以 localeCompare() 返回了1;而 "yellow" 等於 "yellow",所以 localeCompare() 返回了0;最後,"zoo" 在字母表中排在 "yellow" 後面,所以 localeCompare() 返回了-1。再強調一次,因為 localeCompare() 返回的數值取決於實現,所以最好是像下面例子所示的這樣使用這個方法。

function determineOrder(value) {
    var 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");複製程式碼

使用這種結構,就可以確保自己的程式碼在任何實現中都可以正確地執行了。

localeCompare() 方法比較與眾不同的地方,就是實現所支援的地區(國家和語言)決定了這個方法的行為。比如,美國以英語作為 JavaScript 實現的標準語言,因此 localeCompare() 就是區分大小寫的,於是大寫字母在字母表中排在小寫字母前頭就成為了一項決定性的比較規則。不過,在其他地區恐怕就不是這種情況了。

fromCharCode() 方法

另外,String 建構函式本身還有一個靜態方法:fromCharCode()。這個方法的任務是接收一或多個字元編碼,然後將它們轉換成一個字串。從本質上來看,這個方法與例項方法 charCodeAt() 執行的是相反的操作。來看一個例子:

console.log(String.fromCharCode(104, 101, 108, 108, 111)); // "hello"

var s = 'hello';
for(let i=0;i複製程式碼

在這裡,我們給 fromCharCode() 傳遞的是字串 "hello" 中每個字母的字元編碼。

關卡

// 挑戰一
var falseObject = new Object(false);
console.log(typeof falseObject);             // ???
console.log(falseObject instanceof Object);  // ???
console.log(falseObject instanceof Boolean); // ???複製程式碼
// 挑戰二
var numberObject = new Object(100);
console.log(typeof numberObject);             // ???
console.log(numberObject instanceof Object);  // ???
console.log(numberObject instanceof Number);  // ???複製程式碼
// 挑戰三
var stringObject = new Object("abcde");
console.log(typeof stringObject);             // ???
console.log(stringObject instanceof Object);  // ???
console.log(stringObject instanceof String);  // ???複製程式碼
// 挑戰四,翻轉一個字串
// 提示:可以使用陣列的 reverse() 方法
var reverse = function(str) {
    // 待實現方法體
}
console.log(reverse("hello"));  // "olleh"複製程式碼

更多

關注微信公眾號「劼哥舍」回覆「答案」,獲取關卡詳解。
關注 github.com/stone0090/j…,獲取最新動態。

相關文章