Javascript由以下三部分組成:
-
核心(ECMAScript)
-
文檔對象模型(DOM)
-
瀏覽器對象模型(BOM)
ECMAScript組成部分:
語法、類型、語句、關鍵字、保留子、操作符、對象。
按照慣例,外部 JavaScript 文件帶有.js 擴展名。但這個擴展名不是必需的,因為 瀏覽器不會檢查包含 JavaScript 的文件的擴展名。這樣一來,使用 JSP、PHP 或其他 服務器端語言動態生成 JavaScript 代碼也就成為了可能。但是,服務器通常還是需要 看擴展名決定為響應應用哪種 MIME 類型。如果不使用.js 擴展名,請確保服務器能 返回正確的 MIME 類型。
無論如何包含代碼,只要不存在 defer 和 async 屬性,瀏覽器都會按照<script>元素在頁面中出現的先后順序對它們依次進行解析。
ECMAScript 5 引入了嚴格模式(strict mode)的概念:"use strict”;嚴格模式下,JavaScript 的執行結果會有很大不同。
控制語句中使用代碼塊({...})——即使代碼塊中只有一條語句。
ECMAScript 中有 5 種簡單數據類型(也稱為基本數據類型):
Undefined、Null、Boolean、Number 和 String。
還有 1 種復雜數據類型——Object,
Object 本質上是由一組無序的名值對組成的。
字面值 undefined 的主要目的是用於比較。
對於尚未聲明過的變量,只能執行一項操作,即使用 typeof 操作符檢測其數據類型。
即便未初始化的變量會自動被賦予 undefined 值,但顯式地初始化變量依然是明智的選擇。
從邏輯角度來看,null 值表示一個空對象指針,而這也正是使用 typeof 操作符檢測 null 值時會返回"object”的原因。
如果定義的變量准備在將來用於保存對象,那么最好將該變量初始化為 null 而不是其他值。這樣 一來,只要直接檢查 null 值就可以知道相應的變量是否已經保存了一個對象的引用。
實際上,undefined 值是派生自 null 值的:
alert(null == undefined); //true
alert(null === undefined); //false
相等操作符(==)出於比較的目的會轉換其操作數。
只要意在保存對象的變量還沒有真正保存對象,就應該明確地讓該變量保存 null 值。這樣做不僅可以 體現 null 作為空對象指針的慣例,而且也有助於進一步區分 null 和 undefined。
八進制字面量在嚴格模式下是無效的,會導致支持的 JavaScript 引擎拋出錯誤。
在默認情況下,ECMASctipt 會將那些小數點后面帶有 6 個零以上的浮點數值轉換為以 e 表示法 表示的數值(例如,0.0000003 會被轉換成 3e7)。
浮點數值的最高精度是 17 位小數,但在進行算術計算時其精確度遠遠不如整數。例如,0.1 加 0.2 的結果不是 0.3,而是 0.30000000000000004。因此,永遠不要測試某個特定的浮點數值。
關於浮點數值計算會產生舍入誤差的問題,有一點需要明確:這是使用基於 IEEE754 數值的浮點計算的通病,ECMAScript 並非獨此一家;其他使用相同數值格 式的語言也存在這個問題。
使用 isFinite()函數判斷一個數值是不是有窮的(是不是位於最小[-Infinity]和最大[Infinity]的數值之間)。
訪問 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以 得到負和正 Infinity 的值。
可以想見,這兩個屬性中分別保存着-Infinity 和 Infinity。
NaN 本身有兩個非同尋常的特點。
首先,任何涉及 NaN 的操作(例如 NaN/10)都會返回 NaN,這 個特點在多步計算中有可能導致問題。
其次,NaN 與任何值都不相等,包括 NaN 本身。例如,下面的代 碼會返回 false:
alert(NaN == NaN); //false
針對 NaN 的這兩個特點,ECMAScript 定義了 isNaN()函數。
只有 0 除以 0 才會返回 NaN,正數除以 0 返回 Infinity,負數除以 0 返回-Infinity。
盡管有點兒不可思議,但 isNaN()確實也適用於對象。在基於對象調用 isNaN() 函數時,會首先調用對象的 valueOf()方法,然后確定該方法返回的值是否可以轉 換為數值。如果不能,則基於這個返回值再調用 toString()方法,再測試返回值。 而這個過程也是 ECMAScript 中內置函數和操作符的一般執行流程。
有 3 個函數可以把非數值轉換為數值:Number()、parseInt()和 parseFloat()。
一元加操作符的操作與 Number()函數相同。
由於 parseFloat()只解析十進制值,因此它沒有用第二個參數指定基 數的用法。最后還要注意一點:如果字符串包含的是一個可解析為整數的數(沒有小數點,或者小數點后 都是零),parseFloat()會返回整數。
var lang = "Java”;
lang = lang + "Script";
變量 lang 開始時包含字符串"Java"。而第二行代碼把 lang 的值重新定義為"Java" 與"Script"的組合,即"JavaScript"。實現這個操作的過程如下:首先創建一個能容納 10 個字符的 新字符串,然后在這個字符串中填充"Java"和"Script",最后一步是銷毀原來的字符串"Java"和字符串"Script",因為這兩個字符串已經沒用了。這個過程是在后台發生的,而這也是在某些舊版本的瀏覽器(例如版本低於 1.0 的 Firefox、IE6 等)中拼接字符串時速度很慢的原因所在。但這些瀏覽器后 來的版本已經解決了這個低效率問題。
要把一個值轉換為一個字符串有兩種方式。第一種是使用幾乎每個值都有的 toString()方法。數值、布爾值、對象和字符串值(沒錯,每個字符串也都有一個 toString()方法,該方法返回字符串的一個副本)都有 toString()方法。
但 null 和 undefined 值沒有這個方法,因為它們沒有對應的包裝器類,從而沒有屬性和方法。
使用轉型函數 String(),這個 函數能夠將任何類型的值轉換為字符串。String()函數遵循下列轉換規則:
-
如果值有 toString()方法,則調用該方法(沒有參數)並返回相應的結果;
-
如果值是 null,則返回"null”;
-
如果值是 undefined,則返回"undefined"。
ECMAScript 中的對象其實就是一組數據和功能的集合。
對象可以通過執行 new 操作符后跟要創建 的對象類型的名稱來創建。
在 ECMAScript 中,如果不給構造函數傳遞參數,則可 以省略后面的那一對圓括號。
var o = new Object; // 有效,但不推薦省略圓括號
Object 的每個實例都具有下列屬性和方法:
-
constructor:保存着用於創建當前對象的函數。
-
hasOwnProperty(propertyName):用於檢查給定的屬性在當前對象實例中(而不是在實例的原型中)是否存在。其中,作為參數的屬性名(propertyName)必須以字符串形式指定。
-
isPrototypeOf(object):用於檢查傳入的對象是否是傳入對象的原型。
-
propertyIsEnumerable(propertyName):用於檢查給定的屬性是否能夠使用 for-in 語句。
-
toLocaleString():返回對象的字符串表示,該字符串與執行環境的地區對應。
-
toString():返回對象的字符串表示。
-
valueOf():返回對象的字符串、數值或布爾值表示。通常與 toString()方法的返回值相同。
由於在 ECMAScript 中 Object 是所有對象的基礎,因此所有對象都具有這些基本的屬性和方法。
在對非數值應用一元加操作符時,該操作符會像 Number()轉型函數一樣對這個值執行轉換。
一元減操作符遵循與一元加操作符相同的規則,最后再將得到的數值轉換為負數。
ECMAScript 中的所有數 值都以 IEEE-754 64 位格式存儲,但位操作符並不直接操作 64 位的值。而是先將 64 位的值轉換成 32 位 的整數,然后執行操作,最后再將結果轉換回 64 位。
計算一個數值的二進制補碼,需要經過下列 3 個步驟:
-
求這個數值絕對值的二進制碼(例如,要求-18 的二進制補碼,先求 18 的二進制碼);
-
求二進制反碼,即將 0 替換為 1,將 1 替換為 0;
-
得到的二進制反碼加 1。
如果對非數值應用位操作符,會先使用 Number()函數將該值轉換為一個數值(自動完成),然后 再應用位操作。得到的結果將是一個數值。
按位非操作的本質:操作數的負值減 1。
例如:~25 == -25-1。
由於按位非是在數值表示的最底層執行操作,因此速度更快。
注意,左移不會影響操作數的符號位。
有符號的右移操作符由兩個大於號(>>)表示,這個操作符會將數值向右移動,但保留符號位(即正負號標記)。
無符號右移操作符由 3 個大於號(>>>)表示,這個操作符會將數值的所有 32 位都向右移動。對正 數來說,無符號右移的結果與有符號右移相同。但是對負數來說,情況就不一樣了。首先,無符號右移是以 0 來填充空位,而不是像有符號右移那 樣以符號位的值來填充空位。
邏輯非操作符也可以用於將一個值轉換為與其對應的布爾值。
使用兩個邏輯非操作符,實際 上就會模擬 Boolean()轉型函數的行為。
其中,第一個邏輯非操作會基於無論什么操作數返回一個布爾值,而第二個邏輯非操作則對該布爾值求反,於是就得到了這個值真正對應的布爾值。
alert(!!"blue”); //true
alert(!!NaN); //false
不能在邏輯與操作中使用未定義的值。
利用邏輯或的短路特性來避免為變量賦 null 或 undefined 值。
例如:var myObject = preferredObject || backupObject;
兩組操作符:相等和不相等——先轉換再比較,全等和不全等——僅比較而不轉換。
由於相等和不相等操作符存在類型轉換問題,而為了保持代碼中數據類型的完整性,我們推薦使用全等和不全等操作符。
for-in 語句是一種精准的迭代語句,可以用來枚舉對象的屬性(非原生屬性)。
ECMAScript 對象的屬性沒有順序。因此,通過 for-in 循環輸出的屬性名的順序是不可預測的。 具體來講,所有屬性都會被返回一次,但返回的先后次序可能會因瀏覽器而異。在使用 for-in 循環之前,先檢測確認該對象的值不是 null 或 undefined。
with(location){
var qs = search.substring(1);
var hostName = hostname;
var url = href;
}在 with 語句的代碼塊 內部,每個變量首先被認為是一個局部變量,而如果在局部環境中找不到該變量的定義,就會查詢 location 對象中是否有同名的屬性。如果發現了同名屬性,則以 location 對象屬性的值作為變量的值。由於大量使用 with 語句會導致性能下降,同時也會給調試代碼造成困難,因此在開發大型應用程序時,不建議使用 with 語句。
在 switch 語句中使用任何數據類型(在很多其他語言中只能使用數值),無論是字符串,還是對象都沒有 問題。其次,每個 case 的值不一定是常量,可以是變量,甚至是表達式。
switch 語句在比較值時使用的是全等操作符,因此不會發生類型轉換(例如,字符串"10"不等於數值 10)。
arguments 的值永遠與對應命名參數的值保持同步。
function doAdd(num1, num2) {
arguments[1] = 10;
alert(arguments[0] + num2);
}
每次執行這個 doAdd()函數都會重寫第二個參數,將第二個參數的值修改為 10。因為 arguments 對象中的值會自動反映到對應的命名參數,所以修改 arguments[1],也就修改了 num2,結果它們的 值都會變成 10。不過,這並不是說讀取這兩個值會訪問相同的內存空間;它們的內存空間是獨立的,但它們的值會同步。另外還要記住,如果只傳入了一個參數,那么為 arguments[1]設置的值不會反應到命名參數中。這是因為 arguments 對象的長度是由傳入的參數個數決定的,不是由定義函數時的命名 參數的個數決定的。
嚴格模式對如何使用 arguments 對象做出了一些限制。首先,像前面例子中那樣的賦值會變得無效。也就是說,即使把 arguments[1]設置為 10,num2 的值仍然還是 undefined。其次,重寫 arguments 的值會導致語法錯誤(代碼將不會執行)。
ECMAScript 變量可能包含兩種不同數據類型的值: 基本類型值和引用類型值。
基本類型值指的是 簡單的數據段,而引用類型值指那些可能由多個值構成的對象。
如果從一個變量向另一個變量復制基本類型的值,會在變量對象上創建一個新值,然后把該值復制 到為新變量分配的位置上。
當從一個變量向另一個變量復制引用類型的值時,同樣也會將存儲在變量對象中的值復制一份放到 為新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一 個對象。復制操作結束后,兩個變量實際上將引用同一個對象。因此,改變其中一個變量,就會影響另 一個變量。
ECMAScript 中所有函數的參數都是按值傳遞的。也就是說,把函數外部的值復制給函數內部的參 數,就和把值從一個變量復制到另一個變量一樣。基本類型值的傳遞如同基本類型變量的復制一樣,而 引用類型值的傳遞,則如同引用類型變量的復制一樣。有不少開發人員在這一點上可能會感到困惑,因 為訪問變量有按值和按引用兩種方式,而參數只能按值傳遞。在向參數傳遞基本類型的值時,被傳遞的值會被復制給一個局部變量(即命名參數,或者用 ECMAScript 的概念來說,就是 arguments 對象中的一個元素)。在向參數傳遞引用類型的值時,會把 這個值在內存中的地址復制給一個局部變量,因此這個局部變量的變化會反映在函數的外部。
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
如果 person 是按引用傳遞的,那么 person 就會自動被修改為指向其 name 屬性值 為"Greg"的新對象。但是,當接下來再訪問 person.name 時,顯示的值仍然是"Nicholas"。這說明 即使在函數內部修改了參數的值,但原始的引用仍然保持未變。實際上,當在函數內部重寫 obj 時,這 個變量引用的就是一個局部對象了。而這個局部對象會在函數執行完畢后立即被銷毀。
可以把 ECMAScript 函數的參數想象成局部變量。
確定一個值是哪種基本類型可以使用 typeof 操作符,而確定一個值是哪種引用類型可以使用 instanceof 操作符。
JavaScript 沒有塊級作用域。
垃圾收集機制的原理其實很簡單:找出那些不再繼續使用的變 量,然后釋放其占用的內存。為此,垃圾收集器會按照固定的時間間隔(或代碼執行中預定的收集時間), 周期性地執行這一操作。
兩個策略:
-
JavaScript 中最常用的垃圾收集方式是標記清除(mark-and-sweep)。主流瀏覽器都是標記清除式的 垃圾收集策略(或類似的策略),只不過垃圾收集的時間間隔互有不同。
-
另一種不太常見的垃圾收集策略叫做引用計數(reference counting)。例如Objective-C。
將變量設置為 null 意味着切斷變量與它此前引用的值之間的連接。
分配給 Web 瀏覽器的可用內存數量通常要比分配給桌面應用程序的少。這樣做的目的主要是出於安全方面的考慮, 目的是防止運行 JavaScript 的網頁耗盡全部系統內存而導致系統崩潰。內存限制問題不僅會影響給變量 分配內存,同時還會影響調用棧以及在一個線程中能夠同時執行的語句數量。
一旦數據不再有用,最好通過將其值設置為 null 來釋放其引用——這個做法叫做解除引用(dereferencing)。不過,解除一個值的引用並不意味着自動回收該值所占用的內存。解除引用的真正作用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收。
所有變量(包括基本類型和引用類型)都存在於一個執行環境(也稱為作用域)當中,這個執行環境決定了變量的生命周期,以及哪一部分代碼可以訪問其中的變量。以下是關於執行環境的幾 點總結:
-
執行環境有全局執行環境(也稱為全局環境)和函數執行環境之分;
-
每次進入一個新執行環境,都會創建一個用於搜索變量和函數的作用域鏈;
-
函數的局部環境不僅有權訪問函數作用域中的變量,而且有權訪問其包含(父)環境,乃至全局環境;
-
全局環境只能訪問在全局環境中定義的變量和函數,而不能直接訪問局部環境中的任何數據;
-
變量的執行環境有助於確定應該何時釋放內存。
對象字面量也是向函數傳遞大量可選參數的首選方式。
JavaScript訪問對象屬性:
-
點表示法;
-
方括號法。主要優點是可以通過變量來訪問屬性,
例如:var propertyName = "name";alert(person[propertyName]); //"Nicholas"
如果屬性名中包含會導致語法錯誤的字符,或者屬性名使用的是關鍵字或保留字,也可以使用方括 號表示法。通常,除非必須使用變量來訪問屬性,否則我們建議使用點表示法。
在使用 Array 構造函數時也可以省略 new 操作符。
var colors = Array(3); // 創建一個包含 3 項的數組
與對象一樣,在使用數組字面量表示法時,也不會調用 Array 構造函數。
數組的 length 屬性很有特點——它不是只讀的。
因此,通過設置這個屬性,可以從數組的末尾移除項或向數組中添加新項。請看下面的例子:
var colors = ["red", "blue", "green"]; // 創建一個包含 3 個字符串的數組
colors.length = 2;
alert(colors[2]); //undefined
如果將其 length 屬性設置為大於數組 項數的值,則新增的每一項都會取得 undefined 值,如下所示:
colors.length = 4;
alert(colors[3]); //undefined
利用 length 屬性也可以方便地在數組末尾添加新項,如下所示:
var colors = ["red", "blue", "green"];// 創建一個包含 3 個字符串的數組
colors[colors.length] = "black”; //(在位置3)添加一種顏色
colors[colors.length] = "brown”; //(在位置4)再添加一種顏色
sort()方法會調用每個數組項的 toString()轉型方法,然后比較得到的字符串,以 確定如何排序。即使數組中的每一項都是數值,sort()方法比較的也是字符串。
var values = [0, 1, 5, 10, 15];
values.sort();
alert(values); //0,1,10,15,5
正則表達式中的元字符包括: 11 ( [ { \ ^ $ | ) ? * + .]}
這些元字符在正則表達式中都有一或多種特殊用途,因此如果想要匹配字符串中包含的這些字符,
就必須對它們進行轉義。
由於函數是對象,因此函數名實際上也是一個指向函數對象的指針,不會與某個函數綁定。
最后一種定義函數的方式是使用 Function 構造函數。Function 構造函數可以接收任意數量的參數, 但最后一個參數始終都被看成是函數體,而前面的參數則枚舉出了新函數的參數。
var sum = new Function("num1", "num2", "return num1 + num2"); // 不推薦
將函數名想象為指針,也有助於理解為什么 ECMAScript 中沒有函數重載的概念。
兩個同名函數,而結果則是后面的函數覆蓋了前面的函數。
解析器在向執行環境中加載數據時,對函數聲明和函數表達式並非一視同仁。
解析器會率先讀取函數聲明,並使其在執行任何代碼之前可用(可以訪問);
至於函數表達式,則必須等到解析器執行到它所在的代碼行,才會真正被解釋執行。
在函數內部,有兩個特殊的對象:arguments 和 this。
雖然 arguments 的主要用途是保存函數參數, 但這個對象還有一個名叫 callee 的屬性,該屬性是一個指針,指向擁有這個 arguments 對象的函數。
function factorial(num){
if (num <=1) {
return 1;
} else {
return num * arguments.callee(num-1)
} }
this 引用的是函數據以執行的環境對象——或者也可以說是 this 值(當在網頁的全局作用域中調用函數時, this 對象引用的就是 window)。
一定要牢記,函數的名字僅僅是一個包含指針的變量而已。因此,即使是在不同的環境中執行,全局的 sayColor()函數與 o.sayColor()指向的仍然是同一 個函數。
ECMAScript 5 也規范化了另一個函數對象的屬性:caller。這個屬性中保存着調用當前函數的函數的引用, 如果是在全局作用域中調用當前函數,它的值為 null。
每個函數都包含兩個屬性:length 和 prototype。
其中,length 屬性表示函數希望接收的命名參數的個數,
每個函數都包含兩個非繼承而來的方法:apply()和 call()。
這兩個方法的用途都是在特定的作用域中調用函數,實際上等於設置函數體內 this 對象的值。
首先,apply()方法接收兩個參數:
一個是在其中運行函數的作用域,另一個是參數數組。
其中,第二個參數可以是 Array 的實例,也可以是 arguments 對象。
call()方法與 apply()方法的作用相同,它們的區別僅在於接收參數的方式不同。對於 call() 方法而言,第一個參數是 this 值沒有變化,變化的是其余參數都直接傳遞給函數。換句話說,在使用 call()方法時,傳遞給函數的參數必須逐個列舉出來。
傳遞參數並非 apply()和 call()真正的用武之地;
它們真正強大的地方是能夠擴充函數賴以運行的作用域。
使用 call()(或 apply())來擴充作用域的最大好處,就是對象不需要與方法有任何耦合關系。
每個函數繼承的 toLocaleString()和 toString()方法始終都返回函數的代碼。返回代碼的格式則因瀏覽器而異。
每當讀取一個基本類型值的時候,后台就會創建一個對應的基本包裝類型的對象,從而讓我們能夠調用一些方法來操作這些數據。
引用類型與基本包裝類型的主要區別就是對象的生存期。使用 new 操作符創建的引用類型的實例, 在執行流離開當前作用域之前都一直保存在內存中。而自動創建的基本包裝類型的對象,則只存在於一 行代碼的執行瞬間,然后立即被銷毀。這意味着我們不能在運行時為基本類型值添加屬性和方法。來看 下面的例子:
var s1 = "some text";
s1.color = "red";
alert(s1.color); //undefined
使用 new 調用基本包裝類型的構造函數,與直接調用同名的轉型函數是不一樣的。
var value = "25";
var number = Number(value); //轉型函數
alert(typeof number); //“number"
var obj = new Number(value); //構造函數
alert(typeof obj); //"object"
基本類型與引用類型的布爾值還有兩個區別。首先,typeof 操作符對基本類型返回"boolean", 而對引用類型返回"object"。其次,由於 Boolean 對象是 Boolean 類型的實例,所以使用 instanceof 操作符測試 Boolean 對象會返回 true,而測試基本類型的布爾值則返回 false。
ECMAScript 提供了三個基於子字符串創建新字符串的方法:slice()、substr()和 substring()。
encodeURI()主要用於整個 URI,而 encodeURIComponent()主要用於對 URI 中的某一段。
它們的主要區別在於,encodeURI()不會對本身屬於 URI 的特殊字符進行編碼,例如冒號、正斜杠、 問號和井字號;而 encodeURIComponent()則會對它發現的任何非標准字符進行編碼。
一般來說,我們使用 encodeURIComponent()方法的時候要比使用 encodeURI()更多,因為在實踐中更常見的是對查詢字符串參數而不是對基礎 URI 進行編碼。
與 encodeURI()和 encodeURIComponent()方法對應的兩個方法分別是 decodeURI()和 decodeURIComponent()。其中,decodeURI()只能對使用 encodeURI()替換的字符進行解碼。
decodeURIComponent()能夠解碼使用 encodeURIComponent()編碼的所有字符,即它可以解碼任何特殊字符的編碼。
在 eval()中創建的任何變量或函數都不會被提升,因為在解析代碼的時候,它們被包含在一個字 符串中;它們只在 eval()執行的時候創建。
能夠解釋代碼字符串的能力非常強大,但也非常危險。因此在使用 eval()時必 須極為謹慎,特別是在用它執行用戶輸入數據的情況下。否則,可能會有惡意用戶輸 入威脅你的站點或應用程序安全的代碼(即所謂的代碼注入)。
舍入為整數的幾個方法:Math.ceil()、Math.floor()和 Math.round()。 這三個方法分別遵循下列舍入規則:
-
Math.ceil()執行向上舍入,即它總是將數值向上舍入為最接近的整數;
-
Math.floor()執行向下舍入,即它總是將數值向下舍入為最接近的整數;
-
Math.round()執行標准舍入,即它總是將數值四舍五入為最接近的整數。
利用 Math.random() 從某個整數范圍內隨機選擇一個值。
值 = Math.floor(Math.random() * 可能值的總數 + 第一個可能的值)
舉例來說,如果你想選擇一個 1 到 10 之間的數值,可以像下面這樣編寫代碼:
var num = Math.floor(Math.random() * 10 + 1);
或 num = Math.random() * 10 + 1|0;
ECMA-262 把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或者函數。”我們可以把 ECMAScript 的對象想象成散列表:無非就是一組名值對,其中值可以是數據或函數。
我們創建的每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象, 而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。
使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構造函數中定義對象實例的信息,而是 可以將這些信息直接添加到原型對象中。
無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個 prototype 屬性,這個屬性指向函數的原型對象。在默認情況下,所有原型對象都會自動獲得一個 constructor (構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。
當調用構造函數創建一個新實例后,該實例的內部將包含一個指針(內部 屬性),指向構造函數的原型對象。ECMA-262 第 5 版中管這個指針叫[[Prototype]]。雖然在腳本中 沒有標准的方式訪問[[Prototype]],但 Firefox、Safari 和 Chrome 在每個對象上都支持一個屬性 __proto__;而在其他實現中,這個屬性對腳本則是完全不可見的。不過,要明確的真正重要的一點就 是,這個連接存在於實例與構造函數的原型對象之間,而不是存在於實例與構造函數之間。
使用 delete 操作符則可以完全刪除實例屬性,從而讓我們能夠重新訪問原型中的屬性。
使用 hasOwnProperty()方法可以檢測一個屬性是存在於實例中,還是存在於原型中。
有兩種方式使用 in 操作符: 單獨使用和在 for-in 循環中使用。
在單獨使用時,in 操作符會在通過對象能夠訪問給定屬性時返回 true,無論該屬性存在於實例中還是原型中。
要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript 5 的 Object.keys()方法。這個方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。
當使用字面量來寫原型對象時,constructor 屬性默認指向Object,所以要重置。
調用構造函數時會為實例添加一個指向最初原型的 [[Prototype]]指針,
而把原型修改為另外一個對象就等於切斷了構造函數與最初原型之間的聯系。
請記住:實例中的指針僅指向原型,而不指向構造函數。
不推薦在產品化的程序中修改原生對象的原型。如果因 某個實現中缺少某個方法,就在原生對象的原型中添加這個方法,那么當在另一個支 持該方法的實現中運行代碼時,就可能會導致命名沖突。而且,這樣做也可能會意外 地重寫原生方法。
構造函數、原型和實例的關系:
每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。
在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。因為這樣做就會重寫原型鏈。
組合繼承:
指的是將原型鏈和借用構造函數的 技術組合到一塊,從而發揮二者之長的一種繼承模式。其背后的思路是使用原型鏈實現對原型屬性和方 法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數 復用,又能夠保證每個實例都有它自己的屬性。
寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背 后的基本思路是:不必為了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型 原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型 的原型。
寄生組合式繼承的基本模式如下所示。
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);//創建對象
prototype.constructor = subType;//增強對象
subType.prototype = prototype;//指定對象
}
YUI 的 YAHOO.lang.extend()方法采用了寄生組合繼承,從而讓這種模式首次 出現在了一個應用非常廣泛的 JavaScript 庫中。
關於函數聲明,它的一個重要特征就是函數聲明提升(function declaration hoisting),意思是在執行代碼之前會先讀取函數聲明。這就意味着可以把函數聲明放在調用它的語句后面。
在編寫遞歸函數時,使用 arguments.callee 總比使用函數名更保險。
但在嚴格模式下,不能通過腳本訪問 arguments.callee,訪問這個屬性會導致錯誤。不過,可 以使用命名函數表達式來達成相同的結果。例如:
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
} });
以上代碼創建了一個名為 f()的命名函數表達式,然后將它賦值給變量 factorial。即便把函數 賦值給了另一個變量,函數的名字 f 仍然有效,所以遞歸調用照樣能正確完成。這種方式在嚴格模式和 非嚴格模式下都行得通。
當某個函數被調用時,會創建一個執行環境(execution context)及相應的作用域鏈。 然后,使用 arguments 和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域 鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,......直至作為作用域鏈終點的全局執行環境。
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。
過度使用閉包可能會導致內存占用過多,我們建議讀者只在絕對必要時再考慮使用閉 包。
雖然像 V8 等優化后的 JavaScript 引擎會嘗試回收被閉包占用的內存,但請大家 還是要慎重使用閉包。
外部函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,當外部函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;直到匿名函數被銷毀后,外部函數的活動對象才會被銷毀。
閉包只能取得包含函數中任何變量的最 后一個值。別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
每個函數在被調用時都會自動取得兩個特殊變量:this 和 arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域中的 this 對象保存在一個閉包能夠訪問 到的變量里,就可以讓閉包訪問該對象了。
用作塊級作用域(通常稱為私有作用域)的匿名函數的語法如下所示。
(function(){ //這里是塊級作用域
})();
函數表達式的后面可以跟圓括號。
這種做法可以減少閉包占用的內存問題,因為沒有指向匿名函數的引用。只要函數執行完畢,就可以立即銷毀其作用域鏈了。
任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部訪問這些變量。 私有變量包括函數的參數、局部變量和在函數內部定義的其他函數。
在 JavaScript 編程中,函數表達式是一種非常有用的技術。使用函數表達式可以無須對函數命名, 從而實現動態編程。匿名函數,也稱為拉姆達函數,是一種使用 JavaScript 函數的強大方式。
在后台執行環境中,閉包的作用域鏈包含着它自己的作用域、包含函數的作用域和全局作用域。
當函數返回了一個閉包時,這個函數的作用域將會一直在內存中保存到閉包不存在為止。
創建並立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。
先設計最通用的方案,然后再使用特定於瀏覽器的技術增強該方案。
一般應優先考慮使用能力檢測。怪癖檢測是確定應該如何處理 代碼的第二選擇。而用戶代理檢測則是客戶端檢測的最后一種方案,因為這種方法對用戶代理字符串具 有很強的依賴性。
對 arguments 對象使用 Array.prototype.slice()方法可以 將其轉換為數組。
而采用同樣的方法,也可以將 NodeList 對象轉換為數組。
function convertToArray(nodes){
var array = null;
try {
array = Array.prototype.slice.call(nodes, 0); //針對非 IE 瀏覽器
} catch (ex) {
array = new Array();
for (var i=0, len=nodes.length; i < len; i++){
array.push(nodes[i]);
}
}
return array;
}
Document 類型為此提供了兩個方 法:getElementById()和 getElementsByTagName()。
第三個方法,也是只有 HTMLDocument 類型才有的方法,是 getElementsByName()。
document 對象還有一些特殊的集合。這些集合都是 HTMLCollection 對象, 為訪問文檔常用的部分提供了快捷方式,包括:
-
document.anchors,包含文檔中所有帶 name 特性的<a>元素;
-
document.forms,包含文檔中所有的<form>元素,與 document.getElementsByTagName("form")得到的結果相同;
-
document.images,包含文檔中所有的<img>元素,與document.getElementsByTagName("img")得到的結果相同;
-
document.links,包含文檔中所有帶 href 特性的<a>元素。
var style = document.createElement("style"); style.type = "text/css";
try{
style.appendChild(document.createTextNode("body{background-color:red}"));
} catch (ex){
style.styleSheet.cssText = "body{background-color:red}";
}
var head = document.getElementsByTagName("head")[0];
head.appendChild(style);
使用了 try-catch 語句來捕獲 IE 拋出的錯誤,然后再 使用針對 IE 的特殊方式來設置樣式。
理解 DOM 的關鍵,就是理解 DOM 對性能的影響。DOM 操作往往是 JavaScript 程序中開銷最大的 部分,而因訪問 NodeList 導致的問題為最多。NodeList 對象都是“動態的”,這就意味着每次訪問 NodeList 對象,都會運行一次查詢。有鑒於此,最好的辦法就是盡量減少 DOM 操作。
Selectors API Level 1 的核心是兩個方法:querySelector()和 querySelectorAll()。在兼容的瀏 覽器中,可以通過 Document 及 Element 類型的實例調用它們。
querySelector()方法接收一個 CSS 選擇符,返回與該模式匹配的第一個元素,如果沒有找到匹
配的元素,返回 null。
通過 Document 類型調用 querySelector()方法時,會在文檔元素的范圍內查找匹配的元素。而 通過 Element 類型調用 querySelector()方法時,只會在該元素后代元素的范圍內查找匹配的元素。
querySelectorAll()方法接收的參數與 querySelector()方法一樣,都是一個 CSS 選擇符,但 返回的是所有匹配的元素而不僅僅是一個元素。這個方法返回的是一個 NodeList 的實例。
具體來說,返回的值實際上是帶有所有屬性和方法的 NodeList,而其底層實現則類似於一組元素 的快照,而非不斷對文檔進行搜索的動態查詢。這樣實現可以避免使用 NodeList 對象通常會引起的大多數性能問題。
要強制瀏覽器以某種模式渲染頁面,可以使用 HTTP 頭部信息 X-UA-Compatible,或通過等價的 <meta>標簽來設置:
<meta http-equiv="X-UA-Compatible" content="IE=IEVersion”>
Edge:始終以最新的文檔模式來渲染頁面。忽略文檔類型聲明。對於 IE8,始終保持以 IE8 標 准模式渲染頁面。對於 IE9,則以 IE9 標准模式渲染頁面。
div.innerText = "Hello & welcome, <b>\"reader\"!</b>";
運行以上代碼之后,會得到如下所示的結果。
<div id="content">Hello & welcome, <b>"reader"!</b></div>
將 innerText 設置為等於 innerText,這樣就可以去掉所有 HTML 標簽,比如:
div.innerText = div.innerText;
執行這行代碼后,就用原來的文本內容替換了容器元素中的所有內容(包括子節點,因而也就去掉 了 HTML 標簽)。
function getInnerText(element){
return (typeof element.textContent == "string") ?
element.textContent : element.innerText;
}
function setInnerText(element, text){
if (typeof element.textContent == "string"){
element.textContent = text;
} else {
element.innerText = text;
}
}
無論在哪個瀏覽器中,最重要的一條是要記住所有計算的樣式都是只讀的;不能修改計算后樣式對 象中的 CSS 屬性。此外,計算后的樣式也包含屬於瀏覽器內部樣式表的樣式信息,因此任何具有默認值 的 CSS 屬性都會表現在計算后的樣式中。
所有這些偏移量屬性都是只讀的,而且每次訪問它們都需要重新計算。因此,應 該盡量避免重復訪問這些屬性;如果需要重復使用其中某些屬性的值,可以將它們保 存在局部變量中,以提高性能。
“DOM2級事件”規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。
“DOM2 級事件”定義了兩個方法,用於處理指定和刪除事件處理程序的操作:
addEventListener() 和 removeEventListener()。
所有 DOM 節點中都包含這兩個方法,並且它們都接受 3 個參數:要處理的事件名、作為事件處理程序的函數和一個布爾值。最后這個布爾值參數如果是 true,表示在捕獲 階段調用事件處理程序;如果是 false,表示在冒泡階段調用事件處理程序。
其中第二個參數還可以是對象: element.addEventListener(event, object [,capture] );
事件會自動在傳入對象中尋找handleEvent方法,也就是 object.handleEvent。
這樣,在 element 觸發event事件后,調用的是handleEvent 方法,
注意這里面的 this 是指向對象本身, 而普通的函數,this傳入函數里面的this 是指向事件的。
IE 實現了與 DOM 中類似的兩個方法:
attachEvent()和 detachEvent()。
這兩個方法接受相同 的兩個參數:事件處理程序名稱與事件處理程序函數。由於 IE8 及更早版本只支持事件冒泡,所以通過 attachEvent()添加的事件處理程序都會被添加到冒泡階段。
在 IE 中使用 attachEvent()與使用 DOM0 級方法的主要區別在於事件處理程序的作用域。在使 用 DOM0 級方法的情況下,事件處理程序會在其所屬元素的作用域內運行;在使用 attachEvent()方 法的情況下,事件處理程序會在全局作用域中運行,因此 this 等於 window。
對“事件處理程序過多”問題的解決方案就是事件委托。
事件委托利用了事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。
另外,在不需要的時候移除事件處理程序,也是解決這個問題的一種方案。
最適合采用事件委托技術的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。 雖然 mouseover 和 mouseout 事件也冒泡,但要適當處理它們並不容易,而且經常需要計算元素的位置。
在使用事件時,需要考慮如下一些內存與性能方面的問題。
-
有必要限制一個頁面中事件處理程序的數量,數量太多會導致占用大量內存,而且也會讓用戶感覺頁面反應不夠靈敏。
-
建立在事件冒泡機制之上的事件委托技術,可以有效地減少事件處理程序的數量。
-
建議在瀏覽器卸載頁面之前移除頁面中的所有事件處理程序。
解決重復提交表單的辦法有兩個:
在第一次提交表單后就禁用提交按鈕,
或者利用 onsubmit 事件處理程序取消后續的 表單提交操作。
跨文檔消息傳送(cross-document messaging),有時候簡稱為 XDM,指的是在來自不同域的頁面間傳遞消息。
使用 try-catch 最適合處理那些我們無法控制的錯誤。
JSONP 是 JSON with padding(填充式 JSON 或參數式 JSON)的簡寫,是應用 JSON 的一種新方法, 在后來的 Web 服務中非常流行。JSONP 看起來與 JSON 差不多,只不過是被包含在函數調用中的 JSON, 就像下面這樣。
callback({ "name": "Nicholas" });
JSONP 由兩部分組成:回調函數和數據。回調函數是當響應到來時應該在頁面中調用的函數。回調 函數的名字一般是在請求中指定的。而數據就是傳入回調函數中的 JSON 數據。
惰性載入表示函數執行的分支僅會發生一次。有兩種實現惰性載入的方式,
第一種就是在函數被調用時再處理函數。
第二種實現惰性載入的方式是在聲明函數時就指定適當的函數。
與函數綁定緊密相關的主題是函數柯里化(function currying),它用於創建已經設置好了一個或多 個參數的函數。函數柯里化的基本方法和函數綁定是一樣的:使用一個閉包返回一個函數。兩者的區別 在於,當函數被調用時,返回的函數還需要設置一些傳入的參數。
JavaScript 中的柯里化函數和綁定函數提供了強大的動態函數創建功能。
數組分塊(array chunking)的技術,小塊小塊地處理數組,通 常每次一小塊。基本的思路是為要處理的項目創建一個隊列,然后使用定時器取出下一個要處理的項目
進行處理,接着再設置另一個定時器。
setTimeout(function(){
//取出下一個條目並處理
var item = array.shift(); process(item);
//若還有條目,再設置另一個定時器
if(array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
一旦某個函數需要花 50ms 以上的時間完成,那么最好看看能否將任務分割為一系列可以使用定時器的小任務。
函數節流背后的基本思想是指,某些代碼不可以在沒有間斷的情況連續重復執行。第一次調用函數, 創建一個定時器,在指定的時間間隔之后運行代碼。當第二次調用該函數時,它會清除前一次的定時器 並設置另一個。如果前一個定時器已經執行過了,這個操作就沒有任何意義。然而,如果前一個定時器 尚未執行,其實就是將其替換為一個新的定時器。目的是只有在執行函數的請求停止了一段時間之后才 執行。
節流在 resize 事件中是最常用的。
function throttle(method, context) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(context);
}, 100);
}
只要應用的某個部分過分依賴於另一部分,代碼就是耦合過緊,難於維護。
典型的問題如:對象直接引用另一個對象,並且當修改其中一個的同時需要修改另外一個。
緊密耦合的軟件難於維護並且需要經常重寫。
-
解耦 HTML/JavaScript
-
解耦 CSS/JavaScript
-
解耦應用邏輯/事件處理程序
以下是要牢記的應用和業務邏輯之間松散耦合的幾條原則:
-
勿將 event 對象傳給其他方法;只傳來自 event 對象中所需的數據;
-
任何可以在應用層面的動作都應該可以在不執行任何事件處理程序的情況下進行;
-
任何事件處理程序都應該處理事件,然后將處理轉交給應用邏輯。
雖然命名空間會需要多寫一些代碼,但是對於可維護的目的而言是值得的。
顯示在用戶界面上的字符串應該以允許 進行語言國際化的方式抽取出來。URL 也應被抽取出來,因為它們有隨着應用成長而改變的傾向。基本 上,有着可能由於這樣那樣原因會變化的這些數據,那么都會需要找到函數並在其中修改代碼 。而每次 修改應用邏輯的代碼,都可能會引入錯誤。可以通過將數據抽取出來變成單獨定義的常量的方式,將應用邏輯與數據修改隔離開來。
關鍵在於將數據和使用它的邏輯進行分離。要注意的值的類型如下所示。
-
重復值——任何在多處用到的值都應抽取為一個常量。這就限制了當一個值變了而另一個沒變 的時候會造成的錯誤。這也包含了 CSS 類名。
-
用戶界面字符串 —— 任何用於顯示給用戶的字符串,都應被抽取出來以方便國際化。
-
URLs —— 在 Web 應用中,資源位置很容易變更,所以推薦用一個公共地方存放所有的 URL。
-
任意可能會更改的值 —— 每當你在用到字面量值的時候,你都要問一下自己這個值在未來是不是會變化。如果答案是“是”,那么這個值就應該被提取出來作為一個常量。
性能:
注意作用域:
-
避免全局查找:使用全局變量和函數肯定要比局部的開銷更大,因為要涉及作用域鏈上的查找。將在一個函數中會用到多次的全局對象存儲為局部變量總是沒錯的。
-
避免 with 語句:會增加其中執行的代碼的作用域鏈的長度。
選擇正確方法:
-
避免不必要的屬性查找:一旦多次用到對象屬性,應該將其存儲在局部變量中。第一次訪問該值會是 O(n),然而后續的訪問 都會是 O(1),就會節省很多。
-
優化循環:減值迭代、簡化終止條件、簡化循環體、使用后測試循環。
-
展開循環:Duff 裝置技術(針對大數據集)
-
避免雙重解釋
-
性能的其他注意事項:原生方法較快、Switch 語句較快、位運算符較快
最小化語句數:
-
多個變量聲明
-
插入迭代值
-
使用數組和對象字面量
優化DOM交互:
-
最小化現場更新
-
使用 innerHTML
-
使用事件代理
-
注意 HTMLCollection:記住,任何時候要訪問 HTMLCollection,不管它是一個屬性還是一個方法,都是在文檔上進 行一個查詢,這個查詢開銷很昂貴。
JSLint 可以查找 JavaScript 代碼中的語法錯誤以及常見的編碼錯誤。