## 3.3 原生函數
常用的原生函數有:
? String()
? Number()
? Boolean()
? Array()
? Object()
? Function()
? RegExp()
? Date()
? Error()
? Symbol()——ES6 中新加入的!
實際上,它們就是內建函數。
原生函數可以被當作構造函數來使用,通過構造函數(如new String("abc"))創建出來的是**封裝了基本類型值(如"abc")的封裝對象**。
~~~
var a = new String( "abc" );
typeof a; // 是"object",不是"String"
a instanceof String; // true
Object.prototype.toString.call( a ); // "[object String]"
~~~
可以這樣來查看封裝對象:
~~~
console.log( a );
~~~
### 3.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"。
其他基本類型值(如字符串、數字和布爾)的情況有所不同,通常稱為“**包裝**”(boxing)。
~~~
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"。
### 3.3.2 封裝對象包裝
由于基本類型值沒有`.length`和`.toString()` 這樣的屬性和方法,需要通過封裝對象才能訪問,此時JavaScript 會自動為基本類型值包裝(box 或者wrap)一個封裝對象:
~~~
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
~~~
#### 封裝對象釋疑
使用封裝對象時有些地方需要特別注意。
比如Boolean:
~~~
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // 執行不到這里
}
~~~
我們為false 創建了一個封裝對象,然而該對象是真值(“truthy”,即總是返回true),所以這里使用封裝對象得到的結果和使用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.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.3.4 原生函數作為構造函數
關于數組(array)、對象(object)、函數(function)和正則表達式,通常以常量的形式來創建它們。實際上,使用常量和使用構造函數的效果是一樣的(創建的值都是通過封裝對象來包裝)。
**1. Array()**
~~~
var a = new Array( 1, 2, 3 );
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [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;
~~~
b 在當前版本的Chrome 中顯示為[ undefined, undefined, undefined ],而a 和c 則顯示為[ undefined x 3 ]。
更糟糕的是,上例中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(..) 是一個工具函數,適用于所有函數對象,它會以一種特殊的方式來調用傳遞給它的函數。
第一個參數是this 對象,這里不用太過費心,暫將它設為null。第二個參數則必須是一個數組(或者類似數組的值,也叫作類數組對象,array-like object),其中的值被用作函數的參數。于是`Array.apply(..) `調用`Array(..)` 函數,并且將`{ length: 3 }` 作為函數的參數。
可以設想`apply(..) `內部有一個for 循環(與上述join(..) 類似),從0 開始循環到length(即循環到2,不包括3)。假設在`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) 更準確可靠。總之,**永遠不要創建和使用空單元數組**。
**2. Object(..)、Function(..) 和RegExp(..)**
同樣,除非萬不得已,否則盡量不要使用`Object(..)/Function(..)/RegExp(..)`:
~~~
var c = new Object();
c.foo = "bar";
c; // { foo: "bar" }
var d = { foo: "bar" };
d; // { foo: "bar" }
var e = new Function( "a", "return a * 2;" );
var f = function(a) { return a * 2; }
function g(a) { return a * 2; }
var h = new RegExp( "^a*b+", "g" );
var i = /^a*b+/g;
~~~
在實際情況中沒有必要使用new Object() 來創建對象,因為這樣就無法像常量形式那樣一次設定多個屬性,而必須逐一設定。
構造函數Function 只在極少數情況下很有用,比如動態定義函數參數和函數體的時候。
強烈建議使用常量形式(如`/^a*b+/g`)來定義正則表達式,這樣不僅語法簡單,執行效率也更高,因為JavaScript 引擎在代碼執行前會對它們進行預編譯和緩存。與前面的構造函數不同,RegExp(..) 有時還是很有用的,比如動態定義正則表達式時:
~~~
var name = "Kyle";
var namePattern = new RegExp( "\\b(?:" + name + ")+\\b", "ig" );
var matches = someText.match( namePattern );
~~~
上述情況在JavaScript 編程中時有發生,這時`new RegExp("pattern","flags") `就能派上用場。
**3. Date(..) 和Error(..)**
創建日期對象必須使用`new Date()`。`Date(..) `可以帶參數,用來指定日期和時間,而不帶參數的話則使用當前的日期和時間。`Date(..) `主要用來獲得當前的Unix 時間戳(從1970 年1 月1 日開始計算,以秒為單位)。該值可以通過日期對象中的getTime() 來獲得。
從ES5 開始引入了一個更簡單的方法,即靜態函數Date.now()。對ES5 之前的版本可以使用下面的polyfill:
~~~
if (!Date.now) {
Date.now = function(){
return (new Date()).getTime();
};
}
~~~
構造函數`Error(..)`(與前面的Array() 類似)帶不帶new 關鍵字都可。創建錯誤對象(error object)主要是為了獲得當前運行棧的上下文(大部分JavaScript 引擎通過只讀屬性.stack 來訪問)。棧上下文信息包括函數調用棧信息和產生錯誤的代碼行號,
以便于調試(debug)。
錯誤對象通常與throw 一起使用:
~~~
function foo(x) {
if (!x) {
throw new Error( "x wasn’t provided" );
}
// ..
}
~~~
通常錯誤對象至少包含一個`message` 屬性,有時也不乏其他屬性(必須作為只讀屬性訪問),如type。除了訪問stack 屬性以外,最好的辦法是調用(顯式調用或者通過強制類型轉換隱式調用)`toString()` 來獲得經過格式化的便于閱讀的錯誤信息。
**4. Symbol(..)**
ES6 中新加入了一個基本數據類型 ——**符號(Symbol)**。符號是具有唯一性的特殊值(并非絕對),用它來命名對象屬性不容易導致重名。該類型的引入主要源于ES6 的一些特殊構造,此外符號也可以自行定義。
符號可以用作屬性名,但無論是在代碼還是開發控制臺中都無法查看和訪問它的值,只會顯示為諸如`Symbol(Symbol.create)` 這樣的值。
ES6 中有一些預定義符號,以Symbol 的靜態屬性形式出現,如`Symbol.create、Symbol.iterator `等,可以這樣來使用:
~~~
obj[Symbol.iterator] = function(){ /*..*/ };
~~~
可以使用Symbol(..) 原生構造函數來自定義符號。但它比較特殊,**不能帶new 關鍵字**,否則會出錯:
~~~
var mysym = Symbol( "my own symbol" );
mysym; // Symbol(my own symbol)
mysym.toString(); // "Symbol(my own symbol)"
typeof mysym; // "symbol"
var a = { };
a[mysym] = "foobar";
Object.getOwnPropertySymbols( a );
// [ Symbol(my own symbol) ]
~~~
雖然符號實際上并非私有屬性(通過`Object.getOwnPropertySymbols(..)` 便可以公開獲得對象中的所有符號),但它卻主要用于私有或特殊屬性。很多開發人員喜歡用它來替代有下劃線`(_)`前綴的屬性,而下劃線前綴通常用于命名私有或特殊屬性。
**5. 原生原型**
原生構造函數有自己的`.prototype` 對象,如Array.prototype、String.prototype 等。這些對象包含其對應子類型所特有的行為特征。
例如,將字符串值封裝為字符串對象之后,就能訪問String.prototype 中定義的方法。(將`String.prototype.XYZ` 簡寫為`String#XYZ`, 對其他`.prototypes `也同樣如此。)
~~~
? String#indexOf(..)
在字符串中找到指定子字符串的位置。
? String#charAt(..)
獲得字符串指定位置上的字符。
? String#substr(..)、String#substring(..) 和String#slice(..)
獲得字符串的指定部分。
? String#toUpperCase() 和String#toLowerCase()
將字符串轉換為大寫或小寫。
? String#trim()
去掉字符串前后的空格,返回新的字符串。
~~~
以上方法并不改變原字符串的值,而是返回一個新字符串。
借助原型代理(prototype delegation),所有字符串都可以訪問這些方法:
~~~
var a = " abc ";
a.indexOf( "c" ); // 3
a.toUpperCase(); // " ABC "
a.trim(); // "abc"
~~~
其他構造函數的原型包含它們各自類型所特有的行為特征,比如`Number#tofixed(..)`(將數字轉換為指定長度的整數字符串)和`Array#concat(..)`(合并數組)。所有的函數都可以調用`Function.prototype 中的apply(..)、call(..) 和bind(..)`。
然而,有些原生原型(native prototype)并非普通對象那么簡單:
~~~
typeof Function.prototype; // "function"
Function.prototype(); // 空函數!
RegExp.prototype.toString(); // "/(?:)/"——空正則表達式
"abc".match( RegExp.prototype ); // [""]
~~~
更糟糕的是,我們甚至可以修改它們(而不僅僅是添加屬性):
~~~
Array.isArray( Array.prototype ); // true
Array.prototype.push( 1, 2, 3 ); // 3
Array.prototype; // [1,2,3]
// 需要將Array.prototype設置回空,否則會導致問題!
Array.prototype.length = 0;
~~~
這里,`Function.prototype` 是一個函數,`RegExp.prototype` 是一個正則表達式,而`Array.prototype` 是一個數組。
**將原型作為默認值**
`Function.prototype` 是一個空函數,`RegExp.prototype` 是一個“空”的正則表達式(無任何匹配),而`Array.prototype `是一個空數組。對未賦值的變量來說,它們是很好的默認值。
例如:
~~~
function isThisCool(vals,fn,rx) {
vals = vals || Array.prototype;
fn = fn || Function.prototype;
rx = rx || RegExp.prototype;
return rx.test(
vals.map( fn ).join( "" )
);
}
isThisCool(); // true
isThisCool(
["a","b","c"],
function(v){ return v.toUpperCase(); },
/D/
); // false
~~~
這種方法的一個好處是`.prototypes `已被創建并且僅創建一次。相反, 如果將`[]、function(){} 和/(?:)/ `作為默認值,則每次調用`isThisCool(..) `時它們都會被創建一次(具體創建與否取決于JavaScript 引擎,稍后它們可能會被垃圾回收),這樣無疑會造成內存和CPU 資源的浪費。
另外需要注意的一點是,如果默認值隨后會被更改,那就不要使用Array.prototype。上例中的vals 是作為只讀變量來使用,更改vals 實際上就是更改Array.prototype,而這樣會導致前面提到過的一系列問題!
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數