JavaScript的5種原始類型:undefined、null、布爾值、數字和字符串。
JavaScript中兩個非常重要的數據類型是對象和數組。
通過方括號定義數組元素和通過花括號定義對象屬性名和屬性值之間的映射關系。
3.1 數字
JavaScript不區分整數值和浮點數值,JavaScript中的所有數字均用浮點數值表示。
JavaScript中的算術運算在溢出(overflow)、下溢(underflow)或被零整除時不會報錯。當數字運算結果超過了JavaScript所能表示的數字上限(溢出),結果為一個特殊的無窮大(infinity)值,在JavaScript中以Infinity表示。同樣地,當負數的值超過了JavaScript所能表示的負數范圍,結果為負無窮大,在JavaScript中以-Infinity表示。無窮大值的行為特性和我們所期望的是一致的:基於它們的加、減、乘和除結果還是無窮大值(當然還保留它們的正負號)。
下溢(underflow)是當運算結果無限接近於零並比JavaScript能表示的最小值還小的時候發生的一種情形。這種情況下,JavaScript將會返回0。當一個負數發生下溢時,JavaScript返回一個特殊的值“負零”。這個值(負零)幾乎和正常的零完全一樣,JavaScript程序員很少用到負零。
被零整除在JavaScript並不報錯:它只是簡單的返回無窮大(Infinity)或負無窮大(-Infinity)。
但有一個例外,零除以零是沒有意義的,這種整除運算結果也是一個非數字(non-a-number)值,用NaN表示。無窮大除以無窮大,給任意負數作開方運算或者算術運算符與不是數字或無法轉換為數字的操作數一起使用時都將返回NaN。
3.1.4 二進制浮點數和四舍五入錯誤
JavaScript采用IEEE-745浮點數表示法(幾乎所有現代編程語言所采用),這是一種二進制表示法,可以精確地表示分數,比如 1/2 、 1/8 和 1/1024 。 遺憾的是,我們常用的分數(特別是在金融計算方面)都是十進制分數 1/10 、 1/100 等。二進制浮點數表示法並不能精確表示類似0.1這樣簡單的數字。
JavaScript中的數字具有足夠的精度,並可以及其近似於0.1。但事實是,數字不能精確表述的確帶來了一些問題。看下這段代碼:
var x = .3 - .2; var y = .2 - .1; x == y; // ==> false: 兩值不相等! x == .1; // ==> false; y == .1; // ==> true;
由於舍入誤差,0.3和0.2之間的近似差值實際上並不等於0.2和0.1之間的近似差值。這個問題並不只在JavaScript中才會出現,理解這一點非常重要:在任何使用二進制浮點數的編程語言中都會有這個問題。
3.1.5 日期和時間
var date = new Date("2015/4/12");
date.getDay(); // ==> 0, (2015/4/12 是星期天);
書中出現錯誤,0表示星期日,1表示星期一。
3.2 文本
字符串(string)是一組由16位值組成的不可變的有序序列。JavaScript中並沒有表示單個字符的“字符型”。要表示一個16位值,只需將其賦值給字符串變量即可,這個字符串長度為1。
類似replace()和toUpperCase()方法都返回新字符串,原字符串本身並沒有發生改變。
在 ECMAScript 5 中,字符串可以當做只讀數組,除了使用charAt()方法,也可以使用方括號來訪問字符串中的單個字符(16位值):
s = "hello, world"; s[0]; // ==> "h"
基於Mozilla的Web瀏覽器(比如Firefox)很久之前就支持這種方式的字符串索引,多數現代瀏覽器(IE除外)也緊跟Mozilla的腳步,在 ECMAScript 5 成型之前就支持了這一特性。
3.2.4 模式匹配
JavaScript定義了RegExp()構造函數,用來創建表示文本匹配模式的對象。
盡管RegExp並不是語言中的基本數據類型,但是它們依然具有直接量寫法,可以直接在JavaScript程序中使用。在兩條斜線之間的文本構成了一個正則表達式。第二條斜線之后也可以跟隨一個或多個字母,用來修飾匹配模式的含義。
3.3 布爾值
下面這些值會被轉換成false:
- undefined
- null
- 0
- -0
- NaN
- ""
所有其他值,包括所有對象(數組)都會轉換成true。
3.6 包裝對象
我們看到字符串也同樣具有屬性和方法:
var s = "hello world!"; var word = s.substring(s.indexOf(" ")+1, s.length);
字符串既然不是對象,為什么它會有屬性呢? 只要引用了字符串s的屬性,JavaScript就會將字符串值通過調用new String(s)的方式轉換成對象,這個對象繼承了字符串的方法,並被用來處理屬性的引用。一旦屬性引用結束,這個新創建的對象就會銷毀(其實在表現上並不一定創建或銷毀這個臨時對象,然而整個過程看起來是這樣)。
同字符串一樣,數字和布爾值也具有各自的方法:通過Number()和Boolean()構造函數創建一個臨時對象,這些方法的調用均是來自於這個臨時對象。null和undefined沒有包裝對象:訪問它們的屬性會造成一個類型錯誤。
var s = "test"; s.len = 4; var t = s.len;
當運行這段代碼時,t的值是undefined。第二行代碼創建一個臨時字符串對象,並給其len屬性賦值為4,隨即銷毀這個對象。第三行通過原始的(沒有被修改過的)字符串值創建一個新字符串對象,嘗試讀取其len屬性,這個屬性自然不存在,表達式求值結果是undefined。這段代碼說明了在讀取字符串、數字和布爾值的屬性值(或方法)的時候,表現得像對象一樣。但如果你試圖給其屬性賦值,則會忽略這個操作:修改只是發生在臨時對象身上,而這個臨時對象並未繼續保留下來。
存取字符串、數字或布爾值的屬性時創建的臨時對象稱做包裝對象,它只是偶爾用來區分字符串值和字符串對象、數字和數值對象以及布爾值和布爾對象。你需要明白字符串、數字和布爾值是有別於對象的。
需要注意的是,可通過String(),Number()或Boolean()構造函數來顯式創建包裝對象:
var s = "test"; var S = new String(s); typeof(s); // ==> "string" typeof(S); // ==> "object"
typeof可以檢測給定變量的數據類型,可能的返回值有:
1. 'undefined' --- 這個值未定義;
2. 'boolean' --- 這個值是布爾值;
3. 'string' --- 這個值是字符串;
4. 'number' --- 這個值是數值;
5. 'object' --- 這個值是對象或null;
6. 'function' --- 這個值是函數;
3.7 不可變的原始值和可變的對象引用
JavaScript中的原始值(undefined、null、布爾值、數字和字符串)與對象有着根本區別。
原始值是不可更改的:任何方法都無法更改一個原始值。
對象和原始值不同,首先,對象是可變的,它們的值是可修改的;其次,對象的比較並非值的比較:即使兩個對象包含相同的屬性及相同的值,他們也是不相等的。
我們通常將對象稱為引用類型(reference type),以此來和JavaScript的基本類型區分開來。依照術語的叫法,對象值都是引用(reference),對象的比較均是引用的比較:當且僅當它們引用同一基對象時,它們才相等。
3.8 類型轉換
var n = 1 - "x"; // ==> NaN:字符串"x"無法轉換為數字 n + " objects" // ==> "NaN objects":NaN轉換為字符串"NaN"
"=="等於運算符在判斷兩個值是否相等時會做類型轉換,"==="恆等運算符在判斷相等時並未做任何類型轉換。
3.8.2 顯式類型轉換
做顯式類型轉換最簡單的方法就是使用Boolean()、Number()、String()或Object()函數。當不通過new運算符調用這些函數時,它們會作為類型轉換函數進行類型轉換。
Number類定義的toString()方法可以接收表示轉換基數(radix)的可選參數,如果不指定此參數,轉換規則將是基於十進制。同樣,亦可以將數字轉換為其他進制數,例如
var n = 17; binary_string = n.toString(2); // 轉換為 “10001” octal_string = "0" + n.toString(8); // 轉換為 “021” hex_string = "0x" + n.toString(16); // 轉換為 “0x11”
Number類為數字到字符串的類型轉換場景定義了三個方法:
- toFixed()根據小數點后的指定位數將數字轉換為字符串,它從不使用指數計數法;
- toExponential()使用指數計數法將數字轉換為指數形式的字符串;
- toPrecision()根據指定的有效數字位數將數字轉換為字符串。如果有效數字的位數少於數字整數部分的位數,則轉換成指數形式。
如果通過Number()轉換函數傳入一個字符串,它會試圖將其轉換為一個整數或浮點直接量,這個方法只能基於十進制進行轉換,並且不能出現非法的尾隨字符。ParseInt()只解析整數,而ParseFloat()則可以解析整數和浮點數。如果字符串前綴是"0x"或者"0X",parseInt()將其解釋為十六進制數,parseInt()和parseFloat()都會跳過任意數量的前導空格,盡可能解析更多數值字符,並忽略后面的內容。如果第一個非空格字符是非法的數字直接量,將最終返回NaN;
parseInt("3 blind mice"); // ==> 3
parseInt("0xFF"); // ==> 255
parseIntFloat(".1"); // ==> NaN:整數不能以"."開始
parseInt()可以接收第二個可選參數,這個參數指定數字轉換的基數,合法的取值范圍是2~36:
parseInt("11", 2); // ==> 3
parseInt("077", 10); // ==> 77
3.9 變量聲明
如果未在var聲明語句中給變量指定初始值,那么雖然聲明了這個變量,但在給它存入一個值之前,它的初始值就是undefined;
應當始終使用var來聲明變量。
3.10.1 函數作用域和聲明提前
在一些類似C語言的編程語言中,花括號內的每一段代碼都具有各自的作用域,而且變量在聲明它們的代碼段之外是不可見的,我們稱之為塊級作用域(block scope),而JavaScript中沒有塊級作用域。JavaScript取而代之地使用了函數作用域(function scope):變量在聲明它們的函數體以及這個函數體嵌套的任意函數體內都是有定義的。
JavaScript的函數作用域是指在函數內聲明的所有變量在函數體內始終是可見的。有意思的是,這意味着變量在聲明之前甚至已經可用。JavaScript的這個特性被非正式地稱為聲明提前(hoisting),即JavaScript函數里聲明的所有變量(但不涉及到賦值)都被“提前”至函數體的頂部,看一下如下代碼:
var scope = "global"; function f() { console.log(scope); // 輸出 "undefined", 而不是"global" var scope = "local"; // 變量在這里賦初始值,但變量本身在函數體內任何地方均是有定義的 console.log(scope); // 輸出 "local" }
你可能會誤以為函數中的第一行會輸出"global",因為代碼還沒有執行到var語句聲明局部變量的地方。其實不然,由於函數作用域的特性,局部變量在整個函數體始終是有定義的,也就是說,在函數體內局部變量覆蓋了同名全局變量。盡管如此,只有在程序執行到var語句的時候,局部變量才會被真正賦值。因此,上述過程等價於:將函數內的變量聲明“提前”至函數體頂部,同時變量初始化留在原來的位置:
function f() { var scope; // 在函數頂部聲明了局部變量 console.log(scope); // 變量存在,但其值是"undefined" scope = "local"; // 這里將其初始化並賦值 console.log(scope); // 這里它具有了我們所期望的值 }
由於JavaScript沒有塊級作用域,因此一些程序員特意將變量聲明放在函數體頂部。這種做法使得他們的源代碼非常清晰地反映了真實的變量作用域。
3.10.2 作為屬性的變量
當聲明一個JavaScript全局變量時,實際上是定義了全局對象的一個屬性。當使用var聲明一個變量時,創建的這個屬性是不可配置的,也就是說這個變量無法通過delete運算符刪除。如果你沒有使用嚴格模式並給一個未聲明的變量賦值的話,JavaScript會自動創建一個全局變量。以這種方式創建的變量是全局對象的正常的可配置屬性,並可以刪除它們:
var truevar = 1; // 聲明一個不可刪除的全局變量 fakevar = 2; // 創建全局對象的一個可刪除的屬性 this.fakevar2 = 3; // 同上 delete truevar; // ==> false:變量並沒有被刪除 delete fakevar; // ==> true: 變量被刪除 delete this.fakevar2; // ==> true: 變量被刪除
JavaScript可以允許使用this關鍵字來引用全局對象,卻沒有方法可以引用局部變量中存放的對象。這種存放局部變量的對象的特有性質,是一種對我們不可見的內部實現。
3.10.3 作用域鏈
JavaScript是基於詞法作用域的語言:全局變量在程序中始終都是有定義的。局部變量在聲明它的函數體內以及其所嵌套的函數內始終是有定義的。
如果將一個局部變量看做是自定義實現的對象的類型的話,那么可以換個角度來解讀變量作用域。每一段JavaScript代碼(全局代碼或函數)都有一個與之相關的作用域鏈(scope chain)。這個作用域鏈是一個對象列表或者鏈表,這組對象定義了這段代碼“作用域中”的變量。當JavaScript需要查找變量x的值的時候(這個過程稱作“變量解析”(variable resolution)),它會從鏈中的第一個對象開始查找,如果這個對象有一個名為x的屬性,則會直接使用這個屬性的值,如果第一個對象中不存在名為x的屬性,JavaScript會繼續查找鏈上的下一個對象。如果第二個對象依然沒有名為x的屬性,則會繼續查找下一個對象,以此類推。如果作用域鏈上沒有任何一個對象含有屬性x,那么認為這段代碼的作用域鏈上不存在x,並最終拋出一個引用錯誤(ReferenceError)異常。
在JavaScript的最頂層代碼中(也就是不包含在任何函數定義內的代碼),作用域鏈由一個全局對象組成。在不包含嵌套的函數體內,作用域鏈由兩個對象,第一個是定義函數參數和局部變量的對象,第二個是全局對象。在一個嵌套的函數體內,作用域鏈上至少有三個對象。理解對象鏈的創建規則是十分重要的。當定義一個函數時,它實際上保存一個作用域鏈。當調用這個函數時,它創建一個新的對象來存儲它的局部變量,並將這個對象添加至保存的那個作用域鏈上,同時創建一個新的更長的表示函數調用作用域的“鏈”。對於嵌套函數來講,事情變得更加有趣,每次調用外部函數時,內部函數又會重新定義一遍。因為每次調用外部函數的時候,作用域鏈都是不同的。內部函數在每次定義的時候都有微妙的差別——在每次調用外部函數時,內部函數的代碼都是相同的,而且關聯這段代碼的作用域鏈也不相同。
作用域鏈的概念對於理解with語句是非常有幫助的,同樣對理解閉包的概念也至關重要。
4.8 算術表達式
在JavaScript中,所有的數字都是浮點型的,除法運算的結果也是浮點型的,比如5/2的結果是2.5,而不是2.
對於數字和字符串操作符來說,加號運算符和比較運算符的行為都有所不同,前者更偏愛字符串,如果它的其中一個操作數是字符串的話,則進行字符串連接操作。而比較運算符則更偏愛數字,只有在兩個操作數都是字符串的情況下,才會進行字符串的比較。
4.9.3 in運算符
in運算符希望它的左操作數是一個字符串或可以轉換為字符串,希望它的右操作數是一個對象。如果右側的對象擁有一個名為左操作數值的屬性名,那么表達式返回true:
var point = { x:1, y:1 }; "x" in point; // ==> true "toString" in point; // ==> true var data = [7, 8, 9]; "0" in data; // ==> true:數組包含元素"0" 3 in data; // ==> false: 沒有索引為3的元素
4.9.4 instanceof 運算符
instanceof運算符判斷一個對象是否是一個類的實例.
instanceof運算符希望左操作數是一個對象,右操作數標識對象的類. 如果左側的對象是右側類的實例,則表達式返回true; 否則返回false; 第9章將會講到, JavaScript中對象的類是通過初始化他們的構造函數來定義的. 這樣的話, instanceof的右操作數應當是一個函數.
需要注意的是, 所有的對象都是Object的實例. 如果instanceof的左操作數不是對象的話, instanceof返回false. 如果右操作數不是函數, 則拋出一個類型錯誤異常.
為了理解instanceof運算符是如何工作的, 必須首先理解"原型鏈"(prototype chain). 為了計算表達式 o instance of f , JavaScript首先計算f.prototype, 然后在原型鏈中查找o, 如果找到, 那么o是f(或者f的父類)的一個實例, 表達式返回true. 如果f.prototype不在o的原型鏈中的話, 那么o就不是f的實例, instanceof返回false;
對象o中存在一個隱藏的成員, 這個成員指向其父類的原型, 如果父類的原型是另外一個類的實例的話, 則這個原型對象中也存在一個隱藏對象指向另外一個類的原型, 這種鏈條將許多對象或類串接起來, 構成原型鏈.
4.10.1 邏輯與(&&)
在JavaScript中任何希望使用布爾值的地方,表達式和語句都會將其當做真值或假值來對待,因此實際上"&&"並不總是返回true和false,但也並無大礙。
&&運算符首先計算左操作數的值,即首先計算“&&”左側的表達式。如果計算結果是假值,那么整個表達式的結果一定也是假值,因此“&&”這時簡單地返回左操作數的值,而並不會對右操作數進行計算。
反過來講,如果左操作數是真值,那么整個表達式的結果則依賴於右操作數的值。如果右操作數是真值,那么整個表達式的值一定是真值;如果右操作數是假值,那么整個表達式的值一定是假值。因此,當左操作數是真值時,“&&”運算符將計算右操作數的值並將其返回作為整個表達式的計算結果:
var o = { x:2 }; var p = null; o && o.x; // ==> 2 p && p.x; // ==> null
盡管“&&”可以按照第二層和第三層的理解進行一些復雜表達式運算,但大多數情況下,“&&”僅用來對真值和假值做布爾運算。
這個運算符最常用的方式是用來從一組備選表達式中選出一個真值表達式:
// 如果max_width已經定義了,直接使用它;否則在preferences對象中查找max_width // 如果沒有定義它,則使用一個寫死的常量 var max = max_width || preferences.max_width || 500;
這種慣用法通常用在函數體內, 用來給參數提供默認值:
// 將o的成員屬性復制到p中,並返回p function copy(o, p) { p = p || {}; // 如果向參數p沒有傳入任何對象,則使用一個新創建的對象 }
4.10.3 邏輯非(!)
和“&&”與“||”運算符不同,“!”運算符首先將其操作數轉換為布爾值,然后再對布爾值取反。也就是說“!”總是返回true或者false,並且,可以通過使用兩次邏輯非運算來得到一個值的等價布爾值:!!x
4.12 表達式計算
eval()只有一個參數。如果傳入的參數不是字符串,它直接返回這個參數。如果參數是字符串,它會把字符串當成JavaScript代碼進行編譯(parse),如果編譯失敗則拋出一個語法錯誤(SyntaxError)異常。如果編譯成功,則開始執行這段代碼,並返回字符串中的最后一個表達式或語句的值,如果最后一個表達式或語句沒有值,則最終返回undefined。如果字符串拋出一個異常,這個異常將把該調用傳遞給eval()。
現代JavaScript解釋器進行了大量的代碼分析和優化,而eval()的問題在於,用於動態執行的代碼通常來講是不能分析。一般來講,如果一個函數調用了eval(),那么解釋器將無法對這個函數做進一步優化。
4.13.2 typeof運算符
typeof是一元運算符,放在其單個操作數的前面,操作數可以是任意類型。返回值為表示操作數類型的一個字符串。
需要注意的是,typeof運算符可以帶上圓括號,這讓typeof看起來像一個函數名,而不是一個運算符關鍵字。
4.13.3 delete運算符
delete是一元操作符,它用來刪除對象屬性或者數組元素。
var o = { x:1, y:2 }; delete o.x; "x" in o; // ==> false:這個屬性在對象中不再存在 var a = [1, 2, 3]; delete a[2]; 2 in a; // ==> false:元素2在數組中已經不存在了 a.length // ==>3:注意,數組長度並沒有改變。a[2]="undefined"
5.3.2 function
函數聲明語句通常出現在JavaScript代碼的最頂層,也可以嵌套在其他函數體內。但在嵌套時,函數聲明只能出現在所嵌套函數的頂部。也就是說,函數定義不能出現在if語句、while循環或其他任何語句中。
5.6.6 try/catch/finally語句
try/catch/finally語句是JavaScript的異常處理機制。其中try從句定義了需要處理的異常所在的代碼塊。catch從句跟隨在try從句之后,當try塊內某處發生了異常時,調用catch內的代碼邏輯。catch從句后跟隨finally塊,后者中放置清理代碼,不管try塊中是否產生異常,finally塊內的邏輯總是會執行。
和普通的變量不同,catch子句中的標識符具有塊級作用域,它只在catch語句塊內有定義。
5.7.3 "use strict"
"use strict"是ECMAScript 5引入的一條指令。
使用"use strict"指令的目的是說明(腳本或函數中)后續的代碼將會解析為嚴格代碼(strict code)。如果頂層(不在任何函數內的)代碼使用了"use strict"指令,那么它們就是嚴格代碼。如果函數體定義所處的代碼是嚴格代碼或者函數體使用了"use strict"指令,那么函數體的代碼也是嚴格代碼。
嚴格代碼以嚴格模式執行。ECMAScript 5中的嚴格模式是該語言的一個受限制的子集,它修正了語言的重要缺陷,並提供健壯的差錯功能和增強的安全機制。嚴格模式和非嚴格模式之間的區別如下:
- 在嚴格模式中嚴禁使用with語句。
- 在嚴格模式中,所有的變量都要先聲明,如果給一個未聲明的變量、函數、函數參數、catch從句參數或全局對象的屬性賦值,將會拋出一個引用錯誤(在非嚴格模式中,這種隱式聲明的全局變量的方法是給全局對象新添加一個新屬性)。
- 在嚴格模式中,函數(不是方法)中的this值是undefined。(在非嚴格模式中,調用函數的this值總是全局對象)。可以利用這個特性來判斷JavaScript實現是否支持嚴格模式:
var hasStrictMode = (function(){ "use strict"; return this==undefined}());
for/in語句遍歷一個對象的屬性。
第六章 對象
JavaScript對象可以從一個稱為原型的對象繼承屬性。對象的方法通常是繼承的屬性。這種"原型式繼承"(prototypal inheritance)是JavaScript的核心特征。
JavaScript對象是動態的,但他們常用來模擬靜態對象以及靜態類型語言中的“結構體”(struct)。
對象最常見的用法是創建(create)、設置(set)、查找(query)、刪除(delete)、檢索(test)和枚舉(enumerate)它的屬性。
除了名字和值之外,每個屬性還有一些與之相關的值,稱為“屬性特性”(property attribute);
- 可寫(writable attribute),表明是否可以設置該屬性的值。
- 可枚舉(enumerable attribute),表明是否可以通過for/in循環返回該屬性。
- 可配置(configurable attribute),表明是否可以刪除或修改該屬性。
除了包含屬性之外,每個對象還擁有三個相關的對象特性(object attribute):
- 對象的原型(prototype)指向另外一個對象,本對象的屬性繼承自它的原型對象;
- 對象的類(class)是一個標識對象類型的字符串。
- 對象的擴展標記(extensible flag)指明了(在ECMScript 5中)是否可以像該對象添加新屬性。
最后,我們用下面這些術語來對三類JavaScript對象和兩類屬性作划分:
- 內置對象(native object)是由ECMAScript規范定義的對象或類。例如,數組、函數、日期和正則表達式都是內置對象。
- 數組對象(host object)是由JavaScript解釋器所嵌入的宿主環境(比如Web瀏覽器)定義的。客戶端JavaScript中表示網頁結構的HTMLElement對象均是宿主對象。既然宿主環境定義的方法可以當成普通的JavaScript函數對象,那么宿主對象也可以當成內置對象。
- 自定義對象(user-defined object)是由運行中的JavaScript代碼創建的對象。
- 自有屬性(own property)是直接在對象中定義的屬性。
- 繼承屬性(inherited property)是在對象的原型對象中定義的屬性。
6.1 創建對象
可以通過對象直接量、關鍵字new和(ECMAScript 5中的)Object.create()函數來創建對象。
6.1.1 對象直接量
創建對象最簡單的方法就是在JavaScript代碼中使用對象直接量。對象直接量是由若干名/值對組成的映射表,名/值對中間用冒號分隔,名/值對之間用逗號分隔,整個映射表用花括號括起來。屬性名可以是JavaScript標識符也可以是字符串直接量。
var book = { "main title": "JavaScript", // 屬性名字里有空格,必須用字符串表示 "sub-title": "The Definitive Guide", // 屬性名字里有連字符,必須用字符串表示 "for": "all audiences", // "for"是保留字,因此必須用引號 author: { // 注意,這里的屬性名都沒有引號 firstname: "David", surname: "Flanagan" } };
6.1.2 通過new創建對象
new + 構造函數
6.1.3 原型
所有通過對象直接量創建的對象都具有同一個原型對象,並可以通過JavaScript代碼Object.prototype獲得對原型對象的引用。通過關鍵字new和構造函數創建的對象的原型就是構造函數的prototype屬性的值。因此,同使用{}創建對象一樣,通過new Object()創建的對象也繼承自Object.prototype。同樣,通過new Array()創建的對象的原型就是Array.prototype,通過new Date()創建的對象的原型就是Date.prototype。
沒有原型的對象為數不多,Object.prototype就是其中一個。它不繼承任何屬性。其他原型對象都是普通對象,普通對象都具有原型。所有的內置構造函數(以及大部分自定義的構造函數)都具有一個繼承自Object.prototype的原型。例如,Date.prototype的屬性繼承自Object.prototype,因此由new Date()創建的Date對象的屬性同時繼承自Date.prototype和Object.prototype。這一系列鏈接的原型對象就是所謂的“原型鏈”(prototype chain)。
6.1.4 Object.create()
ECMAScript 5 定義了一個名為Object.create()的方法,它創建一個新對象,其中第一個參數是這個對象的原型。Object.create()提供第二個參數,用以對對象的屬性進行進一步的描述。
Object.create()是一個靜態函數,而不是提供給某個對象調用的方法。使用它的方法很簡單,只須傳入所需的原型對象即可:
var o1 = Object.create( { x:1 , y:2} );
可以通過傳入參數null來創建一個沒有原型的新對象,但通過這種方式創建的對象不會繼承任何東西,甚至不包括基礎方法,比如toString(),也就是說,它將不能和"+"運算符一起正常工作:
var o2 = Object.create(null); // o2不繼承任何屬性和方法
如果想創建一個普通的空對象,需要傳入Object.prototype:
var o3 = Object.create(Object.prototype);
例6-1:通過原型繼承創建一個新對象
// inherit() 返回了一個繼承自原型對象p的屬性的新對象 // 這里使用ECMAScript 5 中的Object.create()函數(如果存在的話) // 如果不存在Object.create(),則退化使用其他方法 function inherit(p) { if (p == null) throw TypeError(); // p是一個對象,但不能是null if (Object.create) // 如果Object.create()存在 return Object.create(p); // 直接使用它 var t = typeof p; // 否則進行進一步檢測 if (t !== "object" && t !== "function") throw TypeError(); function f() {}; // 定義一個空構造函數 f.prototype = p; // 將其原型屬性設置為p return new f(); // 使用f()創建p的繼承對象 }
6.2.2 繼承
屬性賦值操作首先檢查原型鏈,以此判定是否允許賦值操作。例如,如果o繼承自一個只讀屬性x,那么賦值操作是不允許的。如果允許屬性賦值操作,它也總是在原始對象上創建屬性或對已有的屬性賦值,而不會去修改原型鏈。在JavaScript中,只有在查詢屬性時才會體會到繼承的存在,而設置屬性和繼承無關,這是JavaScript的一個重要特性,該特性可以讓程序員可以有選擇的覆蓋(override)繼承的屬性。
6.3 刪除屬性
delete運算符只能刪除自有屬性,不能刪除繼承屬性(要刪除繼承屬性必須從定義這個屬性的原型對象上去刪除它,而且這會影響到所有繼承自這個原型的對象)。
delete不能刪除那些可配置性為false的屬性(盡管可以刪除不可擴展對象的可配置屬性)。某些內置對象的屬性是不可配置的,比如通過變量聲明和函數聲明創建的全局對象的屬性。
6.4 檢測屬性
JavaScript對象可以看做屬性的集合,我們經常會檢測集合中成員的所屬關系——判斷某個屬性是否存在於某個對象中。可以通過in運算符、hasOwnPreperty()和propertyIsEnumerable()方法來完成這個工作,甚至僅通過屬性查詢也可以做到這一點。
in運算符的左側是屬性名(字符串),右側是對象。如果對象的自有屬性或繼承屬性中包含這個屬性則返回true;
對象的hasOwnProperty()方法用來檢測給定的名字是否是對象的自有屬性。對於繼承屬性它將返回false;
propertyIsEnumerable()是hasOwnProperty()的增強版,只有檢測到是自有屬性且這個屬性的可枚舉性(enumerable attribute)為true時它才返回true。某些內置屬性是不可枚舉的。通常由JavaScript代碼創建的屬性都是可枚舉的,除非在EMCAScript 5中使用一個特殊的方法來改變屬性的可枚舉性。
除了使用in運算符之外,另一種更簡便的方法是使用"!=="判斷一個屬性是否是undefined:
var o = { x:1 }; o.x !== undefined; // true: o中有屬性x o.y !== undefined; // false: o中沒有屬性y o.toString !== undefined; // true: o繼承了toString屬性
然而有一種場景只能使用in運算符而不能使用上述屬性訪問的方式。in可以區分不存在的屬性和存在但值為undefined的屬性。例如下面的代碼:
var o = { x: undefined }; // 屬性被顯式賦值為undefined o.x !== undefined; // false:屬性存在,但值為undefined o.y !== undefined; // false:屬性不存在 "x" in o; // true:屬性存在 "y" in o; // false:屬性不存在 delete o.x; // 刪除了屬性x "x" in o; // false:屬性不再存在
6.5 枚舉屬性
除了for/in循環外,ECMAScript 5 定義了兩個用以枚舉屬性名稱的函數。
第一個是Object.keys(),它返回一個數組,這個數組由對象中可枚舉的自有屬性的名稱組成。
ECMAScript 5 中第二個枚舉屬性的函數是Object.getOwnPropertyNames(),它和Object.keys()類似,只是它返回對象的所有自有屬性的名稱,而不僅僅是可枚舉的屬性。
6.6 屬性getter和setter
我們知道,對象屬性是由名字、值和一組特性(attribute)構成的。在ECMAScript 5 中,屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter。由getter和setter定義的屬性稱做“存取器屬性”(accessor property)。
和數據屬性不同,如果屬性同時具有getter和setter方法,那么它是一個讀/寫屬性。如果它只有getter方法,那么它是一個只讀屬性。如果它只有setter方法,那么它是一個只寫屬性(數據屬性中有一些例外),讀取只寫屬性總是返回undefined。
存取器屬性定義為一個或兩個和屬性同名的函數,這個函數定義沒有使用function關鍵字,而是使用get和(或)set。注意,這里沒有使用冒號將屬性名和函數體分隔開,但在函數體的結束和下一個方法和數據屬性之間有逗號分隔。
var p = { // x和y是普通的可讀寫的數據屬性 x: 1.0, y: 1.0, // r是可讀寫的存取器屬性,它有getter和setter. // 函數體結束后不要忘記帶上逗號 get r() { return Math.sqrt(this.x*this.x + this.y*this.y); }, set r(newvalue) { var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y); var ratio = newvalue/oldvalue; this.x *= ratio; this.y *= ratio; }, // theta是只讀存取器屬性,它只用getter方法 get theta() { return Math.atan2(this.y, this.x); } };
注意在這段代碼中getter和setter里this關鍵字的用法。JavaScript把這些函數當做對象的方法來調用。也就是說,在函數體內的this指向表示這個點的對象,因此,r屬性的getter方法可以通過this.x和this.y引用x和y的屬性。
和數據屬性一樣,存取器屬性是可以繼承的。
// 這個對象產生嚴格自增的序列號 var serialnum = { // 這個數據屬性包含下一個序列號 // $符號暗示這個屬性是一個私有屬性 $n: 0, // 返回當前值,然后自增 get next() { return this.$n++; } // 給n設置新的值,但只有當它比當前值大時才設置成功 set next(n) { if (n >= this.$n) this.$n = n; else throw "序列號的值不能比當前值小"; } };
6.7 屬性的特性
除了包含名字和值之外,屬性還包含一些標識它們可寫、可枚舉和可配置的特性。在ECMAScript 3 中無法設置這些特性,所有通過ECMAScript 3 的程序創建的屬性都是可寫、可枚舉和可配置的,且無法對這些特性做修改。本節將講述ECMAScript 5 中查詢和設置這些屬性特性的API。這些API對於庫的開發者來說非常重要,因為:
- 可以通過這些API給原型對象添加方法,並將它們設置成不可枚舉的,這讓它們看起來更像內置方法。
- 可以通過這些API給對象定義不能修改或刪除的屬性,借此“鎖定”這個對象。
在本節里,我們將存取器屬性的getter和setter方法看成是屬性的特性。按照這個邏輯,我們也可以把數據屬性的值同樣看做屬性的特性。因此,可以認為一個屬性包含一個名字和4個特性。數據屬性的4個特性分別是它的值(value)、可寫性(writable)、可枚舉性(enumerable)和可配置性(configurable)。存取器屬性不具有值(value)特性和可寫性,他們的可寫性是由setter方法存在與否決定的。因此存取器屬性的4個特性是讀取(get)、寫入(set)、可枚舉性和可配置性。
為了實現屬性特性的查詢和設置操作,ECMAScript 5 中定義了一個名為“屬性描述符”(property descriptor)的對象,這個對象代表那4個特性。描述符對象的屬性和它們所描述的屬性特性是同名的。因此,數據屬性的描述符對象的屬性有value、writable、enumerable和configurable。存取器屬性的描述符對象則用get屬性和set屬性代替value和writable。其中writable、enumerable和configurable都是布爾值,當然,get屬性和set屬性是函數值。
通過調用Object.getOwnPropertyDescriptor()可以獲得某個對象特定屬性的屬性描述符:
// 返回 { value: 1, writable: true, enumerable: true, configurable: true } Object.getOwnPropertyDescriptor( {x:1}, "x" ); // 對於繼承屬性或不存在的屬性,返回undefined Object.getOwnPropertyDescriptor( {}, "x" ); // undefined
要想獲得繼承屬性的特性,需要遍歷原型鏈(Object.getPrototypeOf())。
想要設置屬性的特性,或者想讓新建屬性具有某種特性,則需要調用Object.defineProperty(),傳入要修改的對象、要創建或修改的屬性的名稱以及屬性描述符對象:
var o = {}; // 添加一個不可枚舉的數據屬性x,並賦值為1 Object.defineProperty(o, "x", { value: 1, writtable: true,
enumerable: false, configurable: true}) ; // 對屬性x做修改,讓它變為只讀 Object.defineProperty(o, "x", { writable: false }); // 試圖更改這個屬性的值 o.x = 2; // 操作失敗但不報錯,而在嚴格模式中拋出類型錯誤異常 // 現在將x從數據屬性修改為存取器屬性 Object.defineProperty(o, "x", { get: function() {return 0;} });
如果要同時修改或創建多個屬性,則需要使用Object.defineProperties()。第一個參數是要修改的對象,第二個參數是一個映射表,它包含要新建或修改的屬性的名稱,以及它們的屬性描述符。
完整的規則,任何對Object.defineProperty()或Object.defineProperties()違反規則的使用都會拋出類型錯誤異常:
- 如果對象是不可擴展的,則可以編輯已有的自有屬性,但不能給它添加新屬性。
- 如果屬性是不可配置的,則不能修改它的可配置性和可枚舉性。
- 如果存取器屬性是不可配置的,則不能修改其getter和setter方法,也不能將它轉換為數據類型。
- 如果數據屬性是不可配置的,則不能將它的可寫性從false修改為true,但可以從true修改為false。
- 如果屬性是不可配置且不可寫的,則不能修改它的值。然而可配置但不可寫屬性的值是可以修改的(實際上是先將它標記為可寫的,然后修改它的值,最后轉換為不可寫的)。
6.8 對象的三個屬性
每一個對象都有與之相關的原型(prototype)、類(class)和可擴展性(extensible attribute)。
6.8.1 原型屬性
在ECMAScript 5中,將對象作為參數傳入Object.getPrototypeOf()可以查詢它的原型。
isPrototypeOf()方法可以檢測一個對象是否是另一個對象的原型。
6.8.2 類屬性
對象的類屬性(class attribute)是一個字符串,用以表示對象的類型信息。ECMAScript 3和ECMAScript 5 都未提供設置這個屬性的方法,並只有一種間接的方法可以查詢它。默認的toString()方法(繼承自Object.prototype)返回了如下這種格式的字符串:
[ object class ]
通過內置構造函數(比如Array和Date)創建的對象包含“類屬性”(class attribute),它與構造函數的名稱相匹配。通過對象直接量和Object.create創建的對象的類屬性是“Object”,那些自定義構造函數創建的對象也是一樣,類屬性也是“Object”。
6.8.3 可擴展性
對象的可擴展性用來表示是否可以給對象添加新屬性。所有內置對象和自定義對象都是顯式可擴展的,宿主對象的可擴展性由JavaScript引擎定義。
ECMAScript 5 定義了用來查詢和設置對象可擴展性的函數。通過將對象傳入Object.esExtensible(),來判斷該對象是否是可擴展的。如果想將對象轉換為不可擴展的,需要調用Object.preventExtensions(),將待轉換的對象作為參數傳進去。注意,一旦將對象轉換為不可擴展的,就無法再將其轉換回可擴展的了。同樣需要注意的是,preventExtensions()只影響到對象本身的可擴展性。如果給一個不可擴展的對象的原型添加屬性,這個不可擴展的對象同樣會繼承這些新屬性。
可擴展屬性的目的是將對象“鎖定”,以避免外界的干擾。對象的可擴展性通常和屬性的可配置性與可寫性配合使用,ECMAScript 5定義的一些函數可以更方便地設置多種屬性。
Object.seal()和Object.preventExtensions()類似,除了能夠將對象設置為不可擴展的,還可以將對象的所有自有屬性都設置為不可配置的。也就是說,不能給這個對象添加新屬性,而且它已有的屬性也不能刪除或配置,不過它已有的可寫屬性依然可以設置。對於那些已經封閉(sealed)起來的對象是不能解封的。可以使用Object.isSealed()來檢測對象是否封閉。
Object.freeze()將更嚴格地鎖定對象——“凍結”(frozen)。除了將對象設置為不可擴展的和將其屬性設置為不可配置的之外,還可以將它自有的所有數據屬性設置為只讀(writable: false),如果對象的存取器屬性具有setter方法,存取器屬性將不受影響,仍可以通過給屬性賦值調用他們。使用Object.isFrozen()來檢測對象是否凍結。
Object.preventExtensions()、Object.seal()和Object.freeze()都返回傳入的對象,也就是說,可以通過函數嵌套的方式調用它們:
// 創建一個封閉對象,包括一個凍結的原型和一個不可枚舉的類型 var o = Object.seal(Object.create(Object.freeze({x:1}), // 返回的是一個對象 {y:{value:2, writable: true}}));
6.9 序列化對象
對象序列化(serialization)是指將對象的狀態轉換為字符串,也可將字符串還原為對象。ECMAScript 5 提供了內置函數JSON.stringify()和JSON.parse()用來序列化和還原JavaScript對象。這些方法都使用了JSON作為數據交換格式,JSON的全稱是“JavaScript Object Notation” —— JavaScript對象表示法。
JSON的語法是JavaScript語法的子集,它並不能表示JavaScript里的所有值。支持對象、數組、字符串、無窮大數字、true、false和null,並且它們可以序列化和還原。NaN、Infinity和-Infinity序列化的結果是null,日期對象序列化的結果是ISO格式的日期字符串,但JSON.parse()依然保留它們的字符串形態,而不會將它們還原為原始日期對象。函數、RegExp、Error對象和undefined值不能序列化和還原。JSON.stringify()只能序列化對象可枚舉的自有屬性。對於一個不能序列化的屬性來說,在序列化后的輸出字符串中會將這個屬性省略掉。JSON.stringify()和JSON.parse()都可以接收第二個可選參數,通過傳入需要序列化或還原的屬性列表來定制自定義的序列化或還原操作。
6.10 對象方法
本節將對定義在Object.prototype里的對象方法展開講解。
- toString()方法,返回一個表示調用這個方法的對象值的字符串。
- toLocaleString()方法,返回一個表示這個對象的本地化字符串。(Date、Number)
第7章 數組
JavaScript數組是無類型的:數組元素可以是任意類型,並且同一個數組中的不同元素也可能有不同的類型。JavaScript數組是動態的:根據需要他們會增長或縮減,並且在創建數組時無須聲明一個固定的大小或者在數組大小變化時無須重新分配空間。
通常,數組的實現是經過優化的,用數字索引來訪問數組元素一般來說比訪問常規的對象屬性要快很多。數組繼承自Array.prototype中的屬性,它定義了一套豐富的數組操作方法。
7.1 創建數組
- 數組直接量,如果省略數組直接量中的某個值,省略的元素將被賦予undefined值;
- 使用Array()構造函數
7.2 數組元素的讀和寫
使用[]操作符來訪問數組中的一個元素。
請記住,數組是對象的特殊形式。使用方括號訪問數組就像用方括號訪問對象屬性一樣。JavaScript將指定的數字索引值轉換成字符串,然后將其作為屬性名來使用。
事實上數組索引僅僅是對象屬性名的一種特殊類型,這意味着JavaScript數組沒有“越界”錯誤的概念。當試圖查詢任何對象中不存在的屬性時,不會報錯,只會得到undefined值。
7.3 稀疏數組
稀疏數組就是包含從0開始的不連續索引的數組。通常,數組的length屬性值代表數組中元素的個數。如果數組是稀疏的,length屬性值大於元素的個數。
var a = []; // 創建一個空數組,length = 0; a[1000] = 0; // length = 1001;
7.4 數組長度
數組有兩個特殊的行為。
- 如果為一個數組元素賦值,它的索引 i 大於或等於現有數組的長度時,length屬性的值將設置為 i+1 ;
- 設置length屬性為一個小於當前長度的非負整數n時,當前數組中那些索引值大於或等於n的元素將從中刪除;
var a = [1, 2, 3, 4, 5]; console.log(a[4]); // ==>5 a.length = 0; // 刪除所有元素,a為[] a.length = 5; console.log(a[4]); // ==> "undefined"
在ECMAScript 5 中,可以使用Object.defineProperty()讓數組的length屬性變為只讀。
類似地,如果讓一個數組元素不能配置,就不能刪除它。如果不能刪除它,length屬性就不能設置為小於不可配置元素的索引值。
7.5 數組元素的添加和刪除
可以使用unshift()方法在數組的首部插入一個元素,並且將其他元素依次移到更高的索引處。
可以像刪除對象屬性一樣使用delete運算符來刪除數組元素。刪除數組元素與為其賦undefined值是類似的(但有一些微妙的區別)。
shift()方法從數組頭部刪除一個元素。同時將所有元素下移到比當前索引低1的地方。
splice()是一個通用的方法來插入、刪除和替換數組元素。他會根據需要修改length屬性並移動元素到更高或更低的索引處。
7.6 數組遍歷
在嵌套循環或其他性能非常重要的上下文中,數組的長度應該只查詢一次而非每次循環都要查詢:
for (var i=0, len=keys.length; i<len; i++) { // 循環體仍然不變 }
forEach()方法;
7.8 數組方法
7.8.1 join()
Array.join()方法將數組中所有元素都轉化為字符串並連接在一起,返回最后生成的字符串。可以指定一個可選的字符串在生成的字符串中來分隔數組中的各個元素。不過不指定分隔符,默認使用逗號。
Array.join()方法是String.split()方法的逆向操作,后者是將字符串分隔成若干塊來創建一個數組。
7.8.2 reverse()
Array.reverse()方法將數組中的元素顛倒順序,返回逆序的數組。它采用了替換。
7.8.3 sort()
Array.sort()方法將數組中的元素排序並返回排序后的數組。當不帶參數調用sort()時,數組元素以字母表順序排序(如有必要將臨時轉換為字符串進行比較):
var a = [33, 4, 1111, 222]; a.sort(); // 字母表順序: 111, 222, 33, 4 a.sort( function(a, b) {return a-b;} ); // 數值順序:4, 33, 222, 1111
注意,這里使用匿名函數表達式非常方便。既然比較函數只使用一次,就沒有必要給它們命名了。
7.8.4 concat()
Array.concat()方法創建並返回一個新數組,它的元素包括調用concat()的原始數組的元素和concat()的每個參數。
7.8.5 slice()
Array.slice()方法返回指定數組的一個片段或子數組。它的兩個參數分別指定了片段的開始和結束的位置。返回的數組包含第一個參數指定的位置和所有到但不包含第二個參數指定的位置之間的所有數組元素。如果只指定一個參數,返回的數組將包含從開始位置到數組結尾的所有元素。如參數中出現負數,它表示相對於數組中最后一個元素的位置。例如,參數-1指定了最后一個元素,而-3指定了倒數第三個元素。注意,slice()不會修改調用的數組。
7.8.6 splice()
Array.splice()方法是在數組中插入或刪除元素的通用方法,splice()會修改調用的函數。
splice()的第一個參數指定了插入和(或)刪除的起始位置,第二個參數指定了應該從數組中刪除的元素的個數。如果省略第二個參數,從起始點開始到數組結尾的所有元素都將被刪除。splice()返回一個由刪除元素組成的數組,或者如果沒有刪除元素就返回一個空數組。例如:
var a = [1, 2, 3, 4, 5, 6, 7, 8]; a.splice(4); // 返回[5, 6, 7, 8]; a是[1, 2, 3, 4]; a.splice(1, 2); // 返回[2, 3]; a是[1, 4]; a.splice(1, 1); // 返回[4]; a是[1];
splice()的前兩個參數指定了需要刪除的數組元素。緊隨其后的任意個數的參數指定了需要插入到數組中的元素,從第一個參數指定的位置開始插入。例如:
var a = [1, 2, 3, 4, 5]; a.splice(2, 0, 'a', 'b'); // 返回[]; a是[1, 2, 'a', 'b', 3, 4, 5] a.splice(2, 2, [1, 2], 3); // 返回['a', 'b']; a是[1, 2, [1, 2], 3, 3, 4, 5]
7.9 ECMAScript 5 中的數組方法
ECMAScript5 定義了9個新的數組方法來遍歷、映射、過濾、檢測、簡化和搜索數組。
大多數方法的第一個參數接收一個函數,並且對數組的每個元素(或一些)元素調用一次該函數。如果是稀疏數組,對不存在的元素不調用傳遞的函數。在多數情況下,調用提供的函數使用三個參數:數組元素、元素的索引和數組本身。通常,只需要第一個參數值,可以忽略后兩個參數。
7.9.1 forEach()
forEach()方法從頭到尾遍歷數組,為每個元素調用指定的函數。
var data = [1, 2, 3, 4, 5]; var sum = 0; data.forEach(function(value){ sum+= value;}); console.log(sum);
7.9.2 map()
map()方法將調用的數組的每個元素傳遞給指定的函數,並返回一個數組,它包含該函數的返回值。例如:
a = [1, 2, 3]; b = a.map( function(x){return x*x;} ); // b是[1, 4, 9]
傳遞給map()的函數的調用方式和傳遞給forEach()的函數的調用方式一樣。但傳遞給map()的函數應該有返回值。注意,map()返回的是新數組:它不修改調用的數組。如果是稀疏數組,返回的也是相同方式的稀疏數組:它具有相同的長度,相同的缺失元素;
7.9.3 filter()
filter()方法返回的數組元素是調用的數組的一個子集。傳遞的函數是用來邏輯判定的:該函數返回true或false。如果返回值為true或能轉化為true的值,那么傳遞給判定函數的元素就是這個子集的成員,它將被添加到一個作為返回值的數組中。例如:
a = [5, 4, 3, 2, 1]; smallvalues = a.filter( function(x){return x<3;} ); // [2, 1] everyother = a.filter( function(x, i){ return i%2==0;} ); // [5, 3, 1]
注意,filter()會跳過稀疏數組中缺少的元素,它的返回數組總是稠密的。為了壓縮稀疏數組的空缺,代碼如下:
var dense = sparse.filter( function(){return true;} );
甚至,壓縮空缺並刪除undefined和null元素,可以這樣使用filter():
a = a.filter( function(x){return x!==undefined && x!=null; } );
7.9.4 every()和some()
every()方法就像數學中的“全稱量詞”:當前僅當針對數組中的所有元素調用判定函數都返回true,它才返回true:
a = [1, 2, 3, 4, 5]; a.every( function(x){return x<10;} ); // ==> true; a.every( function(x){return x%2===0;} ); // ==>false;
some()方法就像數學中的“存在量詞”:當數組中至少有一個元素調用判定函數返回true,它就返回true;並且當且僅當數值中的所有元素調用判定函數都返回false,他才返回false:
a = [1, 2, 3, 4, 5]; a.some( function(x){return x%2===0;} ); // ==>true; a.some(isNaN); // ==>false;
注意,一旦every()和some()確認該返回什么值他們就會停止遍歷數組元素。some()在判定函數第一次返回true后就返回true,但如果判定函數一直返回false,它將會遍歷整個數組。every()恰好相反:它在判定函數第一次返回false后就返回false,但如果判定函數一直返回true,它將會遍歷整個數組。注意,根據數學上的慣例,在空數組上調用時,every()返回true,some()返回false;
7.9.5 reduce()和reduceRight()
reduce()和reduceRight()方法使用指定的函數將數組元素進行組合,生成單個值,也可以稱為“注入”和“折疊”。舉例說明它是如何工作的:
var a = [1, 2, 3, 4, 5]; var sum = a.reduce( function(x,y){return x+y}, 0 ); // 數組求和 var product = a.reduce( function(x,y){return x*y}, 1 ); // 數組求積 var max = a.reduce( function(x,y){return (x>y)?x:y; } ); //求最大值
reduce()需要兩個參數。第一個是執行化簡操作的函數。化簡函數的任務就是用某種方法把兩個值組合或化簡成一個值,並返回化簡后的值。當不指定初始值調用reduce()時,它將使用數組的第一個元素作為其初始值。
reduceRight()的工作原理和reduce()一樣,不停的是他按照數組索引從高到低處理數組。例如:
var a = [2, 3, 4]; // 計算2^(3^4)。 var big = a.reduceRight(function(accumulator, value){ return Math.pow(value, accumulator); });
7.9.6 indexOf()和lastIndexOf()
indexOf()和lastIndexOf()搜索整個數組中具有給定值的元素,返回找到的第一個元素的索引或者如果沒有找到就返回-1。indexOf()從頭至尾搜索,而lastIndexOf()則反向搜索。
第一個參數是需要搜索的值,第二個參數是可選的:它指定數組中的一個索引,從那里開始搜索。如果省略該參數,indexOf()從頭開始搜索,而lastIndexOf()從末尾開始搜索。
如下函數在一個數組中搜索指定的值並返回包含所有匹配的數組的索引的一個數組:
function findall(a, x) { var results = [], // 將會返回的數組 len = a.length, // 待搜索數組的長度 pos = 0; // 開始搜索的位置 while (pos < len) { pos = a.indexOf(x, pos); if (pos == -1) break; results.push(pos); pos = pos + 1; } return results; }
7.10 數組類型
在ECMAScript 5 中, 可以使用Array.isArray()函數來判定一個未知的對象是否為數組。
ECMAScript 3 中isArray()函數的代碼可以這樣書寫:
var isArray = Function.isArray || function(o) { return typeof o === "object" && Object.prototype.toString.call(o) === "[object Array]"; };
7.12 作為數組的字符串
在ECMAScript 5 中,字符串的行為類似於只讀的數組。除了使用charAt()方法來訪問單個的字符以外,還可以使用方括號。
字符串的行為類似於數組的事實使得通用的數組方法可以應用到字符串上。
請記住,字符串是不可變值,故當把它們當做數組看待時,它們是只讀的。如push()、sort()、reverse()和splice等數組方法會修改數組,它們在字符串上是無效的。
第8章 函數
JavaScript的函數可以嵌套在其他函數中定義,這樣它們就可以訪問它們被定義時所處的作用域的任何變量。這意味着JavaScript函數構成了一個閉包(closure),它給JavaScript帶來了非常強勁的編程能力。
8.1 函數定義
函數聲明語句:
// 計算階乘的遞歸函數 function factorial(x) { if (x <= 1) return 1; return x * factorial(x-1); }
函數定義表達式:
data.sort(function(a,b){return a-b;});
注意,以表達式方式定義的函數,函數的名稱是可選的。函數定義表達式特別適合用來定義那些只會用到一次的函數。
函數聲明語句“被提前”到外部腳本或外部函數作用域的頂部。
嵌套函數
在JavaScript里,函數可以嵌套在其它函數里。例如:
function hypotenuse(a,b) { function square(x) { return x*x; } return Math.sqrt(square(a)+square(b)); }
嵌套函數的有趣之處在於它的變量作用域規則:它們可以訪問嵌套他們的函數的參量和變量。例如,在上面的代碼里,內部函數square()可以讀寫外部函數hypotenuse()定義的參數a和b,這些作用域規則對內嵌函數非常重要。
函數聲明語句並非真正的語句,ECMAScript規范只是允許他們作為頂級語句。他們可以出現在全局代碼里,或者內嵌在其他函數中,但它們不能出現在循環,條件判斷,或者try/catch/finally以及with語句中。注意,此限制僅適用於以語句聲明形式定義的函數。函數定義表達式可以出現在JavaScript代碼的任何地方。
8.2 函數調用
構成函數主題的JavaScript代碼在定義之時並不會執行,只有調用該函數時,它們才會執行。有4中方式來調用JavaScript函數:
- 作為函數
- 作為方法
- 作為構造函數
- 通過它們的call()和apply()方法間接調用
8.2.1 函數形式調用
根據ECMAScript 3 和非嚴格的 ECMAScript 5 對函數調用的規定,調用上下文(this的值)是全局對象。然而,在嚴格模式下,調用上下文則是undefined。
以函數形式調用的函數通常不使用this關鍵字。不過,“this”可以用來判斷當前是否是嚴格模式。
// 定義並調用一個函數來確定當前腳本運行時是否為嚴格模式 var strict = (function(){return !this;}());
8.2.2 方法形式調用
方法無非就是保存在對象的屬性里的JavaScript函數。
方法調用中,調用上下文(this的值)為調用方法的對象。
方法調用可能包含更復雜的屬性訪問表達式:
customer.surname.toUpperCase(); // 調用customer.surname的方法 f().m(); // 在f()調用結束后繼續調用返回值中的方法m()
方法和this關鍵字是面向對象編程范例的核心。任何函數只要作為方法調用,實際上都會傳入一個隱式的實參——這個實參就是調用方法的對象。
方法鏈
當方法的返回值是一個對象,這個對象還可以再調用它的方法。這種方法調用序列中每次的調用結果都是另外一個表達式的組成部分。
當方法並不需要返回值時,最好直接返回this。如果在設計的API中一直采用這種方式(每個方法都返回this),使用API就可以進行“鏈式調用”風格的編程,在這種編程風格中,只要指定一次要調用的對象即可,余下的方法都可以基於此進行調用:
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
和變量不同,關鍵字this沒有作用域的限制,嵌套的函數不會從調用它的函數中this。如果嵌套函數作為方法調用,其this的值指向調用它的對象。如果嵌套函數作為函數調用,其this值不是全局對象(非嚴格模式下)就是undefined(嚴格模式下)。很多人誤以為調用嵌套函數時this會指向調用外層函數的上下文。如果你想訪問這個外部函數的this值,需要將this的值保存在一個變量里,這個變量和內部函數都同在一個作用域內。通常使用變量self來保存this,比如:
var o = { // 對象o m: function() { // 對象中的方法m() var self = this; // 將this的值保存至一個變量中 console.log(this === o); // 輸出true,this就是這個對象o f(); function f() { // 定義一個嵌套函數f() console.log(this === o); // "false":this的值是全局變量或undefined console.log(self === o); // "true":self指外部函數的this值 } } };
o.m(); // 調用對象o的方法m()
8.2.3 構造函數調用
如果構造函數沒有形參,JavaScript構造函數調用的語法是允許省略實參列表和圓括號的。比如,下面這兩行代碼就是等價的:
var o = new Object(); var o = new Object;
構造函數調用創建一個新的對象,這個對象繼承自構造函數的prototype屬性。構造函數試圖初始化這個新創建的對象,並將這個對象用作其調用上下文,因此構造函數可以使用this關鍵字來引用這個新創建的對象。注意,盡管構造函數看起來像一個方法調用,它依然會使用這個新對象作為調用上下文。
8.2.4 間接調用
call()和apply()可以用來間接地調用函數。
8.3.1 可選形參
當調用函數的時候傳入的實參比函數聲明時指定的形參個數要少,剩下的形參都將設置為undefined值。
// 將對象o中可枚舉的屬性名追加至數組a中,並返回這個數組a // 如果省略a,則創建一個新數組並返回這個新數組 function getPropertyNames(o, /* optional */ a) { if (a === undefined) a = []; // 如果未定義,則使用新數組 for (var property in o) a.push(property); return a; } var a = getPropertyNames(0); // 將o的屬性存儲到一個新數組中 getPropertyNames(p, a); // 將p的屬性追加至數組a中
需要注意的是,當用這種可選實參來實現函數時,需要將可選實參放在實參列表的最后。同樣注意在函數定義中使用注釋 /*optional*/來強調形參是可選的。
8.3.2 可變長的實參列表:實參對象
在函數體內,標識符arguments是指向實參對象的引用,實參對象是一個類數組對象,這樣通過數組下標就能訪問傳入函數的實參值。arguments[]對象最適合的應用場景是在這樣一類函數中,這類函數包含固定個數的命名和必須參數,以及隨后個數不定的可選參數。
callee和caller屬性
除了數組元素,實參對象還定義了callee和caller屬性。在ECMAScript 5 嚴格模式中,對這兩個屬性的讀寫操作都會產生一個類型錯誤。而在非嚴格模式下,ECMAScript標准規定callee屬性指代當前正在執行的函數。caller是非標准的,但大多數瀏覽器都實現了這個屬性。
callee屬性在某些時候會非常有用,比如在匿名函數中通過callee來遞歸地調用自身。
var factorial = function(x){ if (x <= 1) return 1; return x*arguments.callee(x-1); }
8.3.3 將對象屬性用作實參
可以通過名/值對的形式來傳入參數,這樣參數的形式就無關緊要了。
為了實現這種風格的方法調用,定義函數的時候,傳入的實參都寫入一個單獨的對象之中,在調用的時候傳入一個對象,對象中的名/值對是真正需要的實參。
這種寫法允許在函數中設置省略參數的默認值。
// 將原始數組的length元素復制至目標數組 function arraycopy(/*array*/ from, /*index*/ from_start, /*array*/ to, /*index*/ to_start, /*integer*/ length ){ // 邏輯代碼 } // from_start和to_start都默認為0 function easycopy(args) { arraycopy(args.from, args.from_start || 0, // 注意這里設置了默認值 args.to, args.to_start || 0, args.length ); } // 來看如果調用easycopy() var a = [1, 2, 3, 4], b=[]; easycopy( { from:a, to:b, length:4} );
自定義函數屬性
JavaScript中的函數並不是原始值,而是一種特殊的對象,也就是說,函數可以擁有屬性。
來看一個例子,下面這個函數使用了自身的屬性(將自身當做數組來對待)來緩存上一次的計算結果:
// 計算階乘,並將結果緩存至函數的屬性中 function factorial(n) { if (isFinite(n) && n>0 && n==Math.round(n)) { if(!(n in factorial)) // 如果沒有緩存結果 factorial[n] = n * factorial(n-1); // 計算結果並緩存之 return factorial[n]; // 返回緩存結果 } else return NaN; // 如果輸入有誤 } factorial[1] = 1; // 初始化緩存以保存這種基本情況
8.5 作為命名空間的函數
JavaScript中的函數作用域:在函數中聲明的變量在整個函數體內都是可見的(包括在嵌套的函數中),在函數的外部是不可見的。不在任何函數內聲明的變量時全局變量,在整個JavaScript程序中都是可見的。在JavaScript中是無法聲明只在一個代碼塊內可見的變量的,基於這個原因,我們常常簡單地定義一個函數用作臨時的命名空間,在這個命名空間內定義的變量都不會污染到全局命名空間。
( function() { // 模塊代碼 }() ); // 結束函數定義並立即調用它
注意,function之前的左圓括號是必須的,因為如果不寫這個左圓括號,JavaScript解釋器會試圖將關鍵字function解析為函數聲明語句。使用圓括號JavaScript解釋器才會正確地將其解析為函數表達式。
8.6 閉包
和其他大多數現代編程語言一樣,JavaScript也采用詞法作用域(lexical scoping),也就是說,函數的執行依賴於變量作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的。為了實現這種詞法作用域,JavaScript函數對象的內部狀態不僅包含函數的代碼邏輯,還必須引用當前的作用域鏈。函數對象可以通過作用域鏈相互關聯起來,函數體內部的變量都可以保存子函數作用域內,這個特性在計算機科學文獻中稱為“閉包”。
理解壁報首先要了解嵌套函數的詞法作用域規則。看一下這段代碼:
var scope = "global scope"; // 全局變量 function checkscope() { var scope = "local scope"; // 局部變量 function f() { return scope; } // 在作用鏈中返回這個值 return f(); } checkscope() // ==> "local scope"
checkscope()函數聲明了一個局部變量,並定義了一個函數f(),函數f()返回了這個變量的值,最后將函數f()的執行結果返回。現在我們對這段代碼做一點改動:
var scope = "global scope"; // 全局變量 function checkscope() { var scope = "local scope"; // 局部變量 function f() { return scope; } // 在作用鏈中返回這個值 return f; } checkscope()() // 返回值是什么?
JavaScript函數的執行用到了作用域鏈,這個作用域鏈是函數定義的時候創建的。嵌套的函數f()定義在這個作用域里,其中的scope一定是局部變量,不管在何時何地執行函數f(),這種綁定在執行f()時依然有效。因此最后一行代碼返回“local scope”,而不是“global scope”。簡言之,閉包的這個特性強大到讓人吃驚:他們可以捕捉到局部變量(和參數),並一直保存下來,看起來像這些變量綁定到了在其中定義他們的外部函數。
閉包可以捕捉到單個函數調用的局部變量,並將這些局部變量用作私有狀態。
像counter一樣的私有變量不是只能用在一個單獨的閉包內,在同一個外部函數內定義的多個嵌套函數也可以訪問它,這多個嵌套函數都共享一個作用域鏈,看一下這段代碼:
function counter() { var n = 0; return { count: function() { return ++n; }, reset: function() { n=0; } }; } var c = counter(), d = counter(); // 創建兩個計數器對象 c.count(); console.log(c.count()); // ==> 2 console.log(d.count()); // ==> 1 c.reset(); // reset()和count()方法共享狀態 console.log(c.count()); // ==> 1:因為我們重置了c console.log(d.count()); // ==> 2:d不受影響
注意,c = counter(); 調用count()函數,並把返回值賦給c,c的類型為使用{}創建的對象,這個對象包含兩個方法屬性——count()和reset();
此外,每次調用counter()都會創建一個新的作用域鏈和一個新的私有變量。
當寫類似這種代碼的時候往往會犯一個錯誤:那就是試圖將循環代碼移入定義這個閉包的函數之內,看一些這段代碼:
// 返回一個函數組成的數組,它們的返回值是0-9 function constfuncs() { var funcs = []; for (var i=0; i<10; i++) funcs[i] = function() { return i; }; return funcs; } var funcs = constfuncs(); funcs[5]() // 返回值是什么 ?
上面這段代碼創建了10個閉包,並將他們存儲到一個數組中。這些閉包都是在同一個函數調用中定義的,因此它們可以共享變量i。當constfuncs()返回時,變量i的值是10,所有的閉包都共享這一個值,因此,數組中的函數的返回值都是同一個值,這不是我們想要的結果。關聯到閉包的作用域鏈都是“活動的”,記住這一點非常重要。嵌套的函數不會將作用域內的私有成員復制一份,也不會對所綁定的變量生成靜態快照。
書寫閉包的時候還需注意一件事情,this是JavaScript的關鍵字,而不是變量。正如之前討論的,每個函數調用都包含一個this值,閉包是無法訪問外部函數中的this的,除非在外部函數中將this轉存為一個變量:var self = this;
綁定arguments的問題類似。arguments並不是一個關鍵字,但在調用每個函數時都會自動聲明它,由於閉包具有自己所綁定的arguments,因此閉包內無法直接訪問外部函數的參數數組,除非外部函數將參數數組保存到另外一個變量中:var outerArguments = arguments;
8.7 函數屬性、方法和構造函數
函數是JavaScript中特殊的對象,他們可以擁有屬性和方法,甚至可以用Function()構造函數來創建新的函數對象。
8.7.1 length屬性
在函數體里,arguments.length表示傳入函數的實參的個數,而函數本身的length屬性則有不同的含義。函數的length屬性是只讀屬性,它是函數定義時給出的形參的個數。
// 這個函數使用arguments.callee,因此它不能在嚴格模式下工作 function check(args) { var actual = args.length; // 實參的個數 var expected = args.callee.length; // 形參的個數 if (actual !== expected) throw Error("Expected " + expected + " args; got " + actual); } function f(x, y, z) { check(arguments); // 檢查實參個數和期望的實參個數是否一致 return x + y +z; // 再執行函數的后續邏輯 }
8.7.2 prototype屬性
每個函數都包含一個prototype屬性,這個屬性是指向一個對象的引用。
8.7.3 call()方法和apply()方法
我們可以將call()和apply()看做是某個對象的方法,通過調用方法的形式來間接調用函數。call()和apply()的第一個實參是要調用函數的母對象,它是調用上下文,在函數體內通過this來獲得對它的引用。
call()方法和apply()方法的返回值就是調用函數的返回值。
對於call()來說,第一個調用上下文實參之后的所有參數就是要傳入待調用函數的值(用逗號隔開)。apply()的實參都放入一個數組當中。
8.7.4 bind()方法
bind()是在ECMAScript 5 中新增的方法,這個方法的主要作用就是將函數綁定至某個對象。當在函數f()上調用bind()方法並傳入一個對象o作為參數,這個方法將返回一個新的函數。(以函數調用的方式)調用新的函數將會把原始的函數f()當做o的方法來調用。傳入新函數的任何實參都將傳入原始函數,比如:
function f(y) { return this.x+y; } // 這個是待綁定的函數 var o = { x:1 }; // 將要綁定的對象 var g = f.bind(o); g(2) // ==>3:通過調用g(x)來調用o.f(x)
可以通過如下代碼輕易地實現這種綁定:
// 返回一個函數,通過調用它來調用o中的方法f(),傳遞它所有的實參 function bind(f, o) { if (f.bind) return f.bind(o); else return function() { return f.apply(o, arguments); } }
ECMAScript 5 中的bind()方法不僅僅將函數綁定至一個對象,它還附帶一些其他應用,除了第一個實參(要綁定函數的對象)之外,傳入bind()的實參也會綁定至函數的實參,這種附帶的應用是一種常見的函數式編程技術,有時也被稱為“柯里化”(currying)。
var sum = function(x, y){ return x+y; }; // 把sum綁定到null中,建立一個新函數 // 新函數的第一個參數被綁定為1, 這個新函數期望只傳入一個實參 var succ = sum.bind(null, 1); succ(2) // ==>3:x綁定到1,並傳入2作為實參y function f(y, z) { return this.x+y+z; }; var g = f.bind( {x:1}, 2 ); // 綁定f()到對象{x:1}並綁定y為2 g(3) // ==> 6:這里x=1,y=2,z=3
// 例8-5: ECMAScript 3版本的Function.bind()方法 if (!Function.prototype.bind) { Function.prototype.bind = function(o /*, args*/ ) { // 將this和arguments的值保存至變量中 var self = this, boundArgs = arguments; // bind()方法的返回值是一個函數 return function() { // 創建一個實參列表,將傳入bind()的第二個及后續的實參都傳入這個函數 var args = [], i; for (i=1; i<boundArgs.length; i++) args.push(boundArgs[i]); for (i=0; i<arguments.length; i++) args.push(arguments[i]); // 現在將self作為o的方法調用,傳入這些實參 return self.apply(o, args); }; }; }
8.7.5 toString()方法
ECMAScript規范規定這個方法返回一個字符串,這個字符串和函數聲明語句的語法相關。實際上,大多數(非全部)的toString()方法的實現都返回函數的完整源碼。內置函數往往返回一個類似“[native code]”的字符串作為函數體。
8.8 函數式編程
8.8.1 使用函數處理數組
假設有一個數組,數組元素都是數字,我們想要計算這些元素的平均值和標准差,使用數組方法map()和reduce()來實現極其簡潔。
// 首先定義兩個簡單的函數 var sum = function(x, y) { return x+y; }; var square = function(x) { return x*x; }; // 然后將這些函數和數組方法配合使用計算出平均數和標准差 var data = [1, 1, 3, 5, 5]; var mean = data.reduce(sum)/data.length; var deviations = data.map(function(x){ return x-mean; }); var stddev = Math.sqrt(deviations.map(square).reduce(sum))/(data.length-1));
8.8.2 高階函數
所謂高階函數(high-order function)就是操作函數的函數,它接收一個或多個函數作為參數,並返回一個新函數,來看這個例子:
function not(f) { return function() { // 返回一個新的函數 var result = f.apply(this, argument); // 調用f() return !result; // 對結果取反 } }
這里是一個更常見的例子,它接收兩個參數f()和g(),並返回一個新的函數用以計算f(g()):
// 返回一個新的可以計算f(g(...))的函數 function compose(f, g) { return function() { // 需要給f()傳入一個參數,所以使用f()的call()方法 // 需要給g()傳入很多參數,所以使用g()的apply()方法 return f.call(this, g.apply(this, arguments)); }; } var square = function(x) { return x*x; }; var sum = function(x) { return x+y; }; var squareofsum = compose(square, sum); squareofsum(2, 3) // ==> 25
8.8.3 不完全函數
function array(a, n) { return Array.prototype.slice.call(a, n||0); } // 這個函數的實參傳遞至左側 function partialLeft(f /*, ... */ ) { var args = arguments; // 保存外部的實參數組 return function() { var a = array(args, 1); // 開始處理外部的第一個args(第0個args是函數f) a = a.concat(array(arguments)); // 然后增加所有的內部參數 return f.apply(this, a); } } // 這個參數的實參傳遞至右側 function partialRight(f /*, ... */ ) { var args = arguments; // 保存這個外部數組 return function() { var a = array(arguments); // 從內部參數開始 a = a.concat(array(args, 1)); // 然后從外部第1個args開始添加 return f.apply(this, a); }; } // 這個函數的實參被用作模板 // 實參列表中的undefined值都被填充 function partical(f /*, ... */ ) { var args = arguments; return function() { var a = array(args, 1); var i=0, j=0; // 遍歷args,從內部實參填充undefined值 for (; i<a.length; i++) { if (a[i]===undefined) { a[i] = arguments[j++]; } } // 現在將剩下的內部實參都追加進去 a = a.concat(array(arguments, j)); return f.apply(this, a); }; } // 這個函數帶有三個參數 var f = function(x, y, z) { return x*(y-z); }; // 注意這三個不完全調用之間的區別 partialLeft(f, 2)(3, 4) // 2*(3-4) partialRight(f, 2)(3, 4) // 3*(4-2) partial(f, undefine, 2)(3, 4) // 3*(2-4)
9.1 類和原型
在JavaScript中,類的所有實例對象都從同一個原型對象上繼承屬性。因此,原型對象是類的核心。
9.2 類和構造函數
調用構造函數的一個重要特征是,構造函數的prototype屬性被用作新對象的原型。這意味着通過同一個構造函數創建的所有對象都繼承自一個相同的對象,因此它們都是同一個類的成員。
9.5 類和類型
9.5.1 instanceof運算符
instanceof運算符,左操作數是待檢測其類的對象,右操作數是定義類的構造函數。如果o繼承自c.prototype,則表達式o instanceof c值為true。
不通過構造函數作為中介,可以使用isPrototypeOf()方法檢測對象r是否為范圍類的成員: range.methods.isPrototypeOf(r); // range.method是原型對象
instanceof運算符和isPrototypeOf()方法的缺點是,我們無法通過對象來獲取類名,只能檢測對象是否屬於指定的類名。
9.6.1 一個例子:集合類
集合(set)是一種數據結構,用以表示非重復值的無序集合。例子9-6用JavaScript實現了一個更加通用的set類,它實現了從JavaScript值到唯一字符串的映射,然后將字符串用作屬性名。對象和函數都不具備如此簡單可靠的唯一字符串表示,因此集合類必須給集合中的每一個對象或函數定義一個唯一的屬性標識。
例9-6:值的任意集合 function Set() { // 這是一個構造函數 this.values = {}; // 集合數據保存在對象的屬性里 this.add.apply(this, arguments); // 把所有參數都添加進這個集合 } // 將每個參數都添加至集合中 Set.prototype.add = function() { for (var i=0; i<arguments.length; i++) { var val = arguments[i]; // 待添加到集合中的值 var str = Set._v2s(val); // 把它轉換為字符串 if (!this.value.hasOwnProperty(str)) { this.values[str] = val; this.n++; } } return this; // 支持鏈式方法調用 }; // 從集合刪除元素,這些元素由參數指定 Set.prototype.remove = function() { for (var i=0; i<arguments.length; i++) { var str = Set._v2s(arguments[i]); if (this.values.hasOwnProperty(str)) { delete this.values[str]; this.n--; } } return this; // 支持鏈式方法調用 }; // 如果集合包含這個值,則返回true;否則,返回false; Set.prototype.contains = function(value) { return this.values.hasOwnProperty(Set._v2s(value)); }; // 返回集合的大小 Set.prototype.size = function() { return this.n; }; // 遍歷調用集合中的所有元素,在指定的上下文中調用f Set.prototype.foreach = function(f, context) { for (var s in this.values) { if (this.values.hasOwnProperty(s)) { f.call(context, this.values[s]); } } }; // 這是一個內部函數,用以將任意JavaScript值和唯一的字符串對應起來 Set._v2s = function(val) { switch(val) { case undefined: return 'u'; case null: return 'n'; case true: return 't'; case false: return 'f'; default: switch(typeof val) { case 'number': return '#' + val; // 數字都帶有#前綴 case 'string': return '"' + val; // 字符串都帶有" 前綴 default: return '@' + objectId(val); // objs and funcs get @ } } // 對於任意對象來說,都會返回一個字符串 // 針對不同的對象,這個函數會返回不同的字符串 // 對於同一個對象的多次調用,總是返回相同的字符串 function objectId(o) { var prop = "|**objectid**|"; // 私有屬性,用以存放id if (!o.hasOwnProperty(prop)) { o[prop] = Set._v2s.next++; } return o[prop]; } }; Set._v2s.next = 100; // 設置初始id的值
9.6.3 標准轉換方法
最重要的方法首當toString()。這個方法的作用是返回一個可以表示這個對象的字符串。在希望使用字符串的地方調用對象的話,JavaScript會自動調用這個方法。
toLocaleString()和toString()極為類似,如果需要為對象到字符串的轉換定義toString()方法,那么同樣需要定義toLocaleString()方法用以處理本地化的對象到字符串的轉換。
valueOf()方法用於將對象轉換為原始值。比如,當數學運算符(除了“+”運算符)和關系運算符作用於數字文本表示的對象時,會自動調用valueOf()方法。
toJSON()方法是由JSON.stringify()自動調用的。JSON格式用於序列化良好的數據結構,而且可以處理JavaScript原始值、數組和純對象。它和類無關,當對一個對象執行序列化操作時,他會忽略對象的原型和構造函數.




10.2 用於模式匹配的String方法
最簡單的是search()。它的參數是一個正則表達式,返回第一個與之匹配的子串的起始位置,如果找不到匹配的子串,它將返回-1。如果,下面的調用返回值為4:
"JavaScript".search(/script/i);
如果search()的參數不是正則表達式,則首先會通過RegExp構造函數將它轉換成正則表達式,search()方法不支持全局檢索,因為他忽略正則表達式參數中的修飾符g;
13.1 客戶端JavaScript
Window對象是所有客戶端JavaScript特性和API的主要接入點。在客戶端JavaScript中,Window對象也就是全局對象。
13.6.2 同源策略(P348)
腳本只能讀取和所屬文檔來源相同的窗口和文檔的屬性。
文檔的來源包含協議、主機以及載入文檔的URL端口。從不同Web服務器載入的文檔具有不同的來源。通過同一主機的不同端口載入的文檔具有不同的來源。使用http:協議載入的文檔和使用https:協議載入的文檔具有不同的來源,即使它們來自同一個服務器。
第14章 window對象
14.1 計時器
setTimeout()和clearTimeout();
setInterval()和clearInterval();
14.2 瀏覽器定位和導航
14.2.1 解析URL
Window對象的location屬性引用的是Location對象,它表示該窗口中當前顯示的文檔的URL,並定義了方法來使窗口載入新的文檔。
Location對象的href屬性是一個字符串,其包含URL的完整文本。Location對象的toString()方法返回href屬性的值。
其他屬性——protocol、host、hostname、post、pathname和search,分別表示URL的各個部分。它們稱為"URL分解"屬性。search()屬性返回問號之后的URL,這部分通常是某種類型的查詢字符串。
14.2.2 載入新的文檔
Location對象的assign()方法可以使窗口載入並顯示你指定的URL中的文檔。replace()方法在載入新文檔之前會從瀏覽歷史中把當前文檔刪除。reload()方法可以讓瀏覽器重新載入當前文檔。
14.3 瀏覽歷史
Window對象的history屬性引用的是該窗口的History對象。History對象的back()和forward()方法與瀏覽器的“后退”和“前進”按鈕一樣:它們使瀏覽器在瀏覽歷史中前后跳轉一格。第三個方法go()接受一個整數參數,可以在歷史列表中向前(正參數)或向后(負參數)跳過任意多個頁。
14.4 瀏覽器和屏幕信息
14.4.1 Navigator對象
Window對象的navigator屬性引用的是包含瀏覽器廠商和版本信息的Navigator對象。
在需要解決存在於某個特定的瀏覽器的特定版本中的特殊的bug時,瀏覽器嗅探仍然有價值。
14.4.2 Screen對象
Window對象的screen屬性引用的是Screen對象。它提供有關窗口顯示的大小和可用的顏色數量的信息。屬性width和height指定的是以像素為單位的窗口大小。屬性availWidth和availHeight指定的是實際可用的顯示大小,它們排除了像桌面任務欄這樣的特性所占用的空間。
14.5 對話框
Window對象提供了3個方法來向用戶顯示簡單的對話框。alert()向用戶顯示一條消息並等待用戶關閉對話框。confirm()也顯示一條消息,要求用戶單擊“確定”或“取消”按鈕,並返回一個布爾值。prompt()同樣顯示一條消息,等待用戶輸入字符串,並返回那個字符串。showModalDialog()顯示一個包含HTML格式的“模態對話框”。
14.8 多窗口和窗體
一個Web瀏覽器窗口可能在桌面上包含多個標簽頁。每一個標簽頁都是獨立的“瀏覽器上下文”(browsing context),每一個上下文都有獨立的Window對象,而且互相之間互不干擾。
但是窗口並不總是和其他窗口完全沒關系。一個窗口或標簽頁中的腳本可以打開新的窗口或標簽頁,當一個腳本這樣做時,這樣多個窗口或窗口與另一個窗口的文檔之間就可以互操作。
14.8.1 打開或關閉窗口
使用Window對象的open()方法可以打開一個新的瀏覽器窗口(或標簽頁,這通常和瀏覽器的配置選項相關)。Window.open()載入指定的URL到新的或已經存在的窗口中,並返回代表那個窗口的Window對象。它有4個可選的參數:
open()的第一個參數是要在新窗口中顯示的文檔的URL。如果這個參數省略了(也可以是空字符串),那么會使用空頁面的URL about:blank ;
open()的第二個參數是新打開的窗口的名字。如果指定的是一個已存在的窗口的名字(並且腳本允許跳轉到那個窗口),會直接使用已存在的窗口。否則,會打開新的窗口,並將這個指定的名字賦值給它。如果省略此參數,會使用指定的名字“_blank”打開一個新的、未命名的窗口。
open()的第三個參數是非標准的,HTML5規范也主張瀏覽器應該忽略它。
open()的返回值是代表命名或新創建的窗口的Window對象。可以在自己的JavaScript代碼中使用這個Window對象來引用新創建的窗口:
var w = window.open(); // 打開一個新的空白窗口 w.alert("About to visit http://example.com"); // 調用alert()方法 w.location = "http://example.com"; // 設置它的location屬性
在由window.open()方法創建的窗口中,opener屬性引用的是打開它的腳本的Window對象。在其他窗口中,opener為null;
關閉窗口
方法close()將關閉一個窗口。如果已經創建了一個Window對象w,可以使用w.close()將其關掉; 運行在當前的窗口可以使用window.close()關閉;
第15章 腳本化文檔
每一個Web瀏覽器窗口、標簽頁和框架由一個Window對象表示。每個Window對象有一個document屬性引用了Document對象。Document對象表示窗口的內容,它是本章的主題,盡管如此,Document對象並非獨立的,它是一個巨大的API的核心對象,叫做文檔對象模型(Document Object Model,DOM),它代表和操作文檔的內容。
15.2 選取文檔元素
- 用指定的id屬性(document.getElementById());
- 用指定的name屬性(document.getElementsByName());
- 用指定的標簽名字(document.getElementsByTagName());
- 用指定的CSS類(document.getElementsByClassName());
- 匹配指定的CSS選擇器;
15.2.2 通過名字選取元素
HTML的name屬性最初打算為表單元素分配名字,在表單數據提交到服務器時使用該屬性的值。name屬性的值不是必須唯一。在表單中,單選和復選按鈕通常是這樣情況。而且,name屬性只在少數的HTML元素中有效,包括表單,表單元素,<iframe>和<img>元素。
getElementsByName()定義在HTMLDocument類中,而不在Document類中,所以它只針對HTML文檔可用,在XML文檔中不可用。
15.2.3 通過標簽名選取元素
Element類也定義了getElementsByTagName()方法,其原理和Document版本的一樣,但是它只選取調用該方法的元素的后代元素。因此,要查找文檔中第一個<p>元素里面的所有<span>元素,代碼如下:
var firstpara = document.getElementsByTagName("p")[0]; var firstParaSpans = fristpara.getElementsByTagName("span");
document.body是一個HTML文檔的<body>元素,document.head是<head>元素。
15.3.1 作為節點樹的文檔
Document對象、它的Element對象和文檔中表示文本的Text對象都是Node對象。Node對象定義了以下重要屬性:
- parentNode
- childNodes
- firstChild、lastChild
- nextSibling、previousSibling
- nodeTyde:節點的類型。9代表Document節點,1代表Element節點,3代表Text節點,8代表Comment節點;
- nodeValue:text節點或Commend節點的文本內容
基於元素的文檔遍歷:
- firstElementChild、lastElementChild
- nextElementSibling、previousElementSibling
- childElementCount
Node類型定義了attributes屬性。針對非Element對象的任何節點,該屬性為null。對於Element對象,attributes屬性是只讀的類數組對象,它代表元素的所有屬性。類似NodeLists,attributes對象也是實時的。
15.3.2 作為元素樹的文檔
將文檔看做是Element對象樹,忽略Text、空白和Comment節點。
1. children屬性
2. element屬性
firstElementChild, lastElementChild.
nextElementSibling, previousElementSibling
childElementCount

