第 3 章原生函數
常用的原生函數有:
String()
Number()
Boolean()
Array()
Object()
Function()
RegExp()
Date()
Error()
Symbol()——ES6 中新加入的!
JavaScript 中 的 String() 和 Java 中 的 字 符 串 構 造 函 數
String(..) 非常相似,可以這樣來用:
var s = new String( "Hello World!" );
console.log( s.toString() ); // "Hello World!"
原生函數可以被當作構造函數來使用,但其構造出來的對象可能會和我們設想的有所
出入:
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"
通過構造函數(如 new String("abc"))創建出來的是封裝了基本類型值(如 "abc")的封
裝對象。
typeof 在這里返回的是對象類型的子類型。
可以這樣來查看封裝對象:
console.log( a );
在本書寫作期間, Chrome 的最新版本是這樣顯示的: String {0: "a", 1:
"b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"},而老版本這樣顯示:
String {0: "a", 1: "b", 2: "c"}。最新版本的 Firefox 這樣顯示: String
["a","b","c"];老版本這樣顯示: "abc",并且可以點擊打開對象查看器。
這些輸出結果隨著瀏覽器的演進不斷變化,也帶給人們不同的體驗。
再次強調, new String("abc") 創建的是字符串 "abc" 的封裝對象,而非基本類型值 "abc"。
3.1 內部屬性 [[Class]]
所有 typeof 返回值為 "object" 的對象(如數組)都包含一個內部屬性 [[Class]](我們可
以把它看作一個內部的分類,而非傳統的面向對象意義上的類)。這個屬性無法直接訪問,
一般通過 Object.prototype.toString(..) 來查看。
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
對象的內部 [[Class]] 屬性和創建該對象的內建原生構造函數相對應,但并非
總是如此。
基本類型值
Object.prototype.toString.call( null );
// "[object Null]"
Object.prototype.toString.call( undefined );
// "[object Undefined]"
雖然 Null() 和 Undefined() 這樣的原生構造函數并不存在,但是內部 [[Class]] 屬性值仍
然是 "Null" 和 "Undefined"。
其他基本類型值(如字符串、數字和布爾)的情況有所不同,通常稱為“包裝”
Object.prototype.toString.call( "abc" );
// "[object String]"
Object.prototype.toString.call( 42 );
// "[object Number]"
Object.prototype.toString.call( true );
// "[object Boolean]
上例中基本類型值被各自的封裝對象自動包裝,所以它們的內部 [[Class]] 屬性值分別為
"String"、 "Number" 和 "Boolean"。
從 ES5 到 ES6, toString() 和 [[Class]] 的行為發生了一些變化
3.2 封裝對象包裝
由 于 基 本 類 型 值 沒 有 .length
和 .toString() 這樣的屬性和方法,需要通過封裝對象才能訪問,此時 JavaScript 會自動為
基本類型值包裝( box 或者 wrap)一個封裝對象:
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
如果需要經常用到這些字符串屬性和方法,比如在 for 循環中使用 i < a.length,那么從
一開始就創建一個封裝對象也許更為方便,這樣 JavaScript 引擎就不用每次都自動創建了。
但實際證明這并不是一個好辦法,因為瀏覽器已經為 .length 這樣的常見情況做了性能優
化,直接使用封裝對象來“提前優化”代碼反而會降低執行效率。
一般情況下,我們不需要直接使用封裝對象。最好的辦法是讓 JavaScript 引擎自己決定什
么時候應該使用封裝對象。換句話說,就是應該優先考慮使用 "abc" 和 42 這樣的基本類型
值,而非 new String("abc") 和 new Number(42)。
封裝對象釋疑
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // 執行不到這里
}
我們為 false 創建了一個封裝對象,然而該對象是真值(“ truthy”,即總是返回 true,參見
第 4 章),所以這里使用封裝對象得到的結果和使用 false 截然相反。
如果想要自行封裝基本類型值,可以使用 Object(..) 函數(不帶 new 關鍵字):
var a = "abc";
var b = new String( a );
var c = Object( a );
typeof a; // "string"
typeof b; // "object"
typeof c; // "object"
b instanceof String; // true
c instanceof String; // true
Object.prototype.toString.call( b ); // "[object String]"
Object.prototype.toString.call( c ); // "[object String]"
一般不推薦直接使用封裝對象(如上例中的 b 和 c),但它們偶爾也會派上
用場。
3.3 拆封
如果想要得到封裝對象中的基本類型值,可以使用 valueOf() 函數:
var a = new String( "abc" );
var b = new Number( 42 );
var c = new Boolean( true );
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
在需要用到封裝對象中的基本類型值的地方會發生隱式拆封。具體過程(即強制類型轉
換)
var a = new String( "abc" );
var b = a + ""; // b的值為"abc"
typeof a; // "object"
typeof b; // "string"
3.4 原生函數作為構造函數
關于數組( array)、對象( object)、函數( function)和正則表達式,我們通常喜歡以常
量的形式來創建它們。實際上,使用常量和使用構造函數的效果是一樣的(創建的值都是
通過封裝對象來包裝)。
如前所述,應該盡量避免使用構造函數,除非十分必要,因為它們經常會產生意想不到的
結果。
3.4.1 Array(..)
var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]
構造函數 Array(..) 不要求必須帶 new 關鍵字。不帶時,它會被自動補上。
因此 Array(1,2,3) 和 new Array(1,2,3) 的效果是一樣的。
Array 構造函數只帶一個數字參數的時候,該參數會被作為數組的預設長度( length),而
非只充當數組中的一個元素。
這實非明智之舉:一是容易忘記,二是容易出錯。
更為關鍵的是,數組并沒有預設長度這個概念。這樣創建出來的只是一個空數組,只不過
它的 length 屬性被設置成了指定的值。
如若一個數組沒有任何單元,但它的 length 屬性中卻顯示有單元數量,這樣奇特的數據結
構會導致一些怪異的行為。而這一切都歸咎于已被廢止的舊特性(類似 arguments 這樣的
類數組)。
我們將包含至少一個“空單元”的數組稱為“稀疏數組”。
var a = new Array( 3 );
a.length; // 3
a;
a 在 Chrome 中顯示為 [ undefined x 3 ](目前為止),這意味著它有三個值為 undefined
的單元,但實際上單元并不存在(“空單元” 這個叫法也同樣不準確)。
var a = new Array( 3 );
var b = [ undefined, undefined, undefined ];
var c = [];
c.length = 3;
a;
b;
c;
我們可以創建包含空單元的數組,如上例中的 c。只要將 length 屬性設置為
超過實際單元數的值,就能隱式地制造出空單元。另外還可以通過 delete
b[1] 在數組 b 中制造出一個空單元。
b 在當前版本的 Chrome 中顯示為 [ undefined, undefined, undefined ],而 a 和 c 則顯示
為 [ undefined x 3 ]。是不是感到很困惑?
更令人費解的是在當前版本的 Firefox 中 a 和 c 顯示為 [ , , , ]。仔細看來,這其中有三
個逗號,代表四個空單元,而不是三個。
Firefox 在輸出結果后面多添了一個 ,,原因是從 ES5 規范開始就允許在列表(數組值、屬性列表等)末尾多加一個逗號(在實際處理中會被忽略不計)。所以如果你在代碼或者調
試控制臺中輸入 [ , , , ],實際得到的是 [ , , ](包含三個空單元的數組)。這樣做雖
然在控制臺中看似令人費解,實則是為了讓復制粘貼結果更為準確。
針對這種情況, Firefox 將 [ , , , ] 改為顯示 Array [<3 empty slots>],這
無疑是個很大的提升。
更糟糕的是,上例中 a 和 b 的行為有時相同,有時又大相徑庭:
a.join( "-" ); // "--"
b.join( "-" ); // "--"
a.map(function(v,i){ return i; }); // [ undefined x 3 ]
b.map(function(v,i){ return i; }); // [ 0, 1, 2 ]
a.map(..) 之所以執行失敗,是因為數組中并不存在任何單元,所以 map(..) 無從遍歷。而
join(..) 卻不一樣,它的具體實現可參考下面的代碼:
function fakeJoin(arr,connector) {
var str = "";
for (var i = 0; i < arr.length; i++) {
if (i > 0) {
str += connector;
}
if (arr[i] !== undefined) {
str += arr[i];
}
}
return str;
}
var a = new Array( 3 );
fakeJoin( a, "-" ); // "--"
從中可以看出, join(..) 首先假定數組不為空,然后通過 length 屬性值來遍歷其中的元
素。而 map(..) 并不做這樣的假定,因此結果也往往在預期之外,并可能導致失敗。
我們可以通過下述方式來創建包含 undefined 單元(而非“空單元”)的數組:
var a = Array.apply( null, { length: 3 } );
a; // [ undefined, undefined, undefined ]
apply(..) 是一個工具函數,適用于所有函數對象,它會以一種特殊的方式來調用傳遞給
它的函數。
假設在 apply(..) 內部該數組參數名為 arr, for 循環就會這樣來遍歷數組: arr[0]、
arr[1]、 arr[2]。 然 而, 由 于 { length: 3 } 中 并 不 存 在 這 些 屬 性, 所 以 返 回 值 為
undefined
。
換句話說,我們執行的實際上是 Array(undefined, undefined, undefined),所以結果是單
元值為 undefined 的數組,而非空單元數組。
雖然 Array.apply( null, { length: 3 } ) 在創建 undefined 值的數組時有些奇怪和繁瑣,
但是其結果遠比 Array(3) 更準確可靠。
總之, 永遠不要創建和使用空單元數組。