- JavaScript前端開發模塊化教程
- 趙建保
- 11833字
- 2019-12-20 19:23:28
1.3 核心知識
1.3.1 JavaScript演進
理解JavaScript的最好方法之一,就是了解JavaScript的歷史。
JavaScript為互聯網而生,緊隨著瀏覽器的出現而問世。回顧它的歷史,就要從瀏覽器的歷史說起。1990年12月25日,英國計算機科學家伯納·李爵士(Tim Berners-Lee)發明了萬維網(World Wide Web)。1993年,美國國家超級計算機應用中心(NCSA)開始開發一個獨立的瀏覽器Mosaic。1994年10月,Mosaic通信公司(Mosaic Communications)成立,不久后改名為Netscape。1994年12月,Navigator發布了1.0版,市場份額一舉超過90%。Netscape同微軟公司在瀏覽器市場的競爭異常激烈,爭相給瀏覽器添加新功能,獲取競爭優勢。1995年2月,Netscape公司發布Netscape Navigator 2瀏覽器,該公司的布蘭登·艾奇(Brendan Eich)為Navigator 2瀏覽器開發了LiveScript腳本語言,主要目的是處理表單數據驗證,避免由服務器端驗證導致的延時問題。LiveScript語法借鑒Java,函數借鑒Scheme,原型繼承借鑒Self,正則表達式特性則借鑒Perl。當時正逢Sun公司的股票飛漲,為搭上Java成功的順風車,Netscape將LiveScript改名為JavaScript。由于JavaScipt 1.0獲得了巨大成功,Netscape公司隨即在Netscape Navigator 3中又發布了JavaScript 1.1版本。
在Netscape Navigator 3發布后不久,微軟不甘示弱,緊跟著Netscape在Internet Explorer 3中加入JavaScript腳本語言,為了避免與Netscape的JavaScript產生糾紛,微軟特意將其命名為JScript,這種語言和JavaScript很像,瀏覽器大戰就此爆發。自微軟在操作系統中內置Internet Explorer 3,Netscape就面臨著即將喪失瀏覽器腳本語言主導權的局面。1996年11月,網景公司決定將JavaScript提交給國際標準化組織ECMA,希望JavaScript能夠成為國際標準,以此抵抗微軟。1997年,歐洲計算機制造商協會(ECMA)以JavaScript 1.1為藍本制訂了ECMA-262新腳本語言的標準,并命名為ECMAScript,這個版本就是ECMAScript 1.0版,它規定了瀏覽器腳本語言的標準。之所以不叫JavaScript,一方面是由于商標的關系,Java是Sun公司的商標,根據一份授權協議,只有Netscape公司可以合法地使用JavaScript這個名字,且JavaScript已經被Netscape公司注冊為商標;另一方面也是想體現這門語言的制定者是ECMA,不是Netscape,這樣有利于保證這門語言的開放性和中立性。因此,ECMAScript和JavaScript的關系是,前者是后者的規格,后者是前者的一種實現。1998年6月,ECMAScript 2.0版發布。1999年12月,ECMAScript 3.0版發布,成為JavaScript的通行標準,得到了廣泛支持。2007年10月,ECMAScript 4.0版草案發布,對3.0版做了大幅升級,由于4.0版的目標過于激進,各方對于是否通過這個標準發生了嚴重分歧。以Yahoo、Microsoft、Google為首的大公司,反對JavaScript的大幅升級,主張小幅改動;以JavaScript創造者Brendan Eich為首的Mozilla公司,則堅持當前的草案。2008年7月,由于對于下一個版本應該包括哪些功能,各方分歧太大,爭論過于激進,ECMA開會決定,中止ECMAScript 4.0的開發,將其中涉及現有功能改善的一小部分發布為ECMAScript 3.1,而將其他激進的設想擴大范圍,放入以后的版本,由于會議的氣氛,該版本的項目代號起名為Harmony(和諧)。2009年12月,ECMAScript 5.0版正式發布。Harmony項目則一分為二,一些較為可行的設想定名為JavaScript.next繼續開發,后來演變成ECMAScript 6,2015年6月,ECMAScript 6發布了正式版本,并更名為ECMAScript 2015。如圖1-2所示為JavaScript演進路線。

圖1-2 JavaScript演進路線
隨著JavaScript再次成為關注的焦點,很多杰出的編程人員致力于改善JavaScript解釋器,極大地改善了其運行階段的性能。隨著Gmail、GoogleMaps這一類客戶端應用和Ajax的相繼出現,大大促進了JavaScript開發模式的變革,也把JavaScript催生成為一種成熟的、某些方面獨一無二的、擁有強大原型體系的面向對象語言。
1.3.2 JavaScript介紹
程序設計語言分為解釋型和編譯型兩大類。Java和C++等語言需要一個編譯器(Compiler)。編碼器是一種程序,能夠把用Java等高級語言編寫出來的源代碼翻譯為直接在計算機上執行的文件,代碼錯誤在編譯階段就能被發現。解釋型程序設計語言不需要編譯器,運行僅需要解釋器。JavaScript語言則由Web瀏覽器負責完成有關的解釋和執行工作。JavaScript是Web頁面中的一種腳本編程語言,也是一種通用的、跨平臺的、基于對象和事件驅動并具有安全性能的解釋型腳本語言,不但可用于編寫客戶端的腳本程序,由Web瀏覽器解釋執行,還可以編寫在服務器端執行的腳本程序,在服務器端處理用戶提交的信息并動態地向客戶端瀏覽器返回處理結果。瀏覽器中的JavaScript解釋器將直接讀入源代碼并執行,代碼錯誤只能等到解釋器執行到有關代碼時才能被發現。如果瀏覽器中沒有解釋器,JavaScript代碼就無法執行。
JavaScript腳本語言的主要特點如下。
(1)解釋性。不同于一些編譯性程序語言(如C、C++),JavaScript源代碼不需要經過編譯,而是直接嵌入在HTML頁面中,使得前端頁面支持用戶交互并響應相應事件,在瀏覽器中運行時被解釋。
(2)基于對象。許多功能運用腳本環境對象的方法與腳本的相互作用來完成。
(3)事件驅動。JavaScript可以直接對用戶頁面的操作行為(鼠標、鍵盤、手勢等)做出響應,無須經過Web服務器處理。
(4)跨平臺。JavaScript依賴于瀏覽器本身,與操作環境無關,只要計算機能運行瀏覽器并支持JavaScript的瀏覽器,就可以正確執行。
(5)安全性。JavaScript是一種安全性語言,不允許訪問本地硬盤,不能將數據存入服務器上,不允許對網絡文檔進行修改和刪除,只能通過瀏覽器實現信息瀏覽或動態交互,以防止數據丟失和篡改。
JavaScript由一項主要適用于瀏覽器客戶端的計算機技術,從一個簡單的輸入驗證器逐漸發展成為一種多功能的強大的程序設計語言,甚至連服務端也能用它來編寫,能夠處理復雜的計算和交互,擁有閉包、匿名函數等,甚至元編程特性。現在已經具備了與瀏覽器窗口及其內容等幾乎所有方面交互的能力。在互聯網發展的早期,JavaScript就已經成為了支撐網頁內容交互體驗的基礎技術,現在JavaScript毫無疑問已經成為了Web的核心技術,能夠地理定位、播放視頻和音頻、繪圖等,速度更快,性能更強。
JavaScript是世界上最流利的編程語言之一。使用JavaScript不僅可以開發瀏覽器應用程序,也可以開發手機應用(APP)和服務器端程序。JavaScript可實現以下功能。
(1)創建擁有強大而豐富功能的Web應用程序,多數運行于Web瀏覽器,基于HTML5可開發應用緩存、本地存儲、本地數據庫等Web應用。
(2)使用Node.js編寫服務器端腳本。
(3)開發移動設備應用程序。比如基于HTML5+的移動APP開發。
1.3.3 Web頁面渲染過程
Web技術的根基是HTML、CSS和JavaScript,其中HTML控制內容和結構,CSS控制表現,而JavaScript則控制交互行為。換句話說,JavaScript是讓HTML和CSS協同運作的黏合劑。一個完整的JavaScript實現應該包括以下三個不同部分:核心(ECMAScript)、文檔對象模型(DOM)和瀏覽器對象模型(BOM),如圖1-3所示。

圖1-3 JavaScript實現
ECMAScript規定了JavaScript的語法、類型、語句、關鍵字、保留字、操作符和對象,它是語言的核心部分,包括變量、函數、循環等,獨立于瀏覽器,并可以在其他環境中使用。文檔對象模型(DOM)是用于操作HTML和XML文檔的應用程序接口(Application Programming Interface,API),提供了一種與HTML、XML文檔交互的方式。在瀏覽器中,主要用來操作HTML文檔。HTML和DOM把整個頁面映射為一個多層節點結構。HTML或XML頁面中的每個組成部分都是某種類型的節點,這些節點又包含著不同類型的數據。DOM1級(DOM Level1)于1998年10月成為W3C推薦標準,包括DOM核心(DOM Core)和DOM HTML。DOM核心規定如何映射基于XML的文檔結構,以便簡化對文檔中任意部分的訪問和操作。DOM HTML模塊添加了針對HTML的對象和方法。DOM2包括DOM視圖、DOM事件、DOM樣式、DOM遍歷和范圍。瀏覽器對象模型(BOM)實際上是一個與瀏覽器環境有關的對象集合,使開發人員可以控制瀏覽器顯示的頁面以外的部分,BOM只處理瀏覽器窗口和框架。
Web頁面渲染過程如圖1-4所示,圖中實線表示先后關系,虛線表示渲染過程可能調用的模塊。如頁面下載時需要使用網絡和存儲,計算布局和繪圖時需要使用2D/3D圖形模塊生成可視化結果。具體渲染過程是:網頁內容輸入到HTML解釋器,HTML解釋器在解析后構建DOM樹,如果遇到JavaScript代碼則交給JavaScript引擎去處理,如果網頁包含CSS則交給CSS解釋器去解釋,沒有被定義CSS的元素渲染引擎將默認樣式應用到HTML元素上。當DOM建立的時候,渲染引擎接收來自CSS解釋器的樣式信息,構建一個新的內部繪圖模型。該模型由布局模塊計算模型內部各元素的位置和大小信息,最后由繪圖模塊完成從模型到圖像的繪制。
C、C++、Java等典型的編程語言,執行代碼前必須先編譯,而JavaScript只需直接在網頁中編寫代碼,再在瀏覽中加載網頁,瀏覽器JavaScript引擎就能直接執行代碼。運行時,JavaScript不會修改服務器端的HTML文件,而是操控瀏覽器基于HTML文件構建的DOM樹。

圖1-4 Web頁面渲染過程
1.3.4 Visual Studio Code介紹
2015年4月29日,微軟在舊金山展覽中心正式舉行了Build 2015開發者大會,會上,微軟發布了免費、跨平臺編輯器Visual Studio Code,它是一個輕便、強大、性能優秀、功能完備的源代碼編輯器,主要用于Web和云應用開發,并為Web前端開發進行優化。Visual Studio Code可運行于Windows、Mac OS和Linux平臺,支持JavaScript、TypeScript和Node.js開發,借助豐富的擴展庫可支持C++、C#、Java、Python、PHP和Go開發,支持.NET和Unity運行庫,圖1-5所示為VSCode擴展庫。注意,Visual Studio Code并非Visual Studio。

圖1-5 VSCode擴展庫
1.3.5 Visual Studio Code快捷鍵
快捷鍵,又叫快速鍵或熱鍵,指通過某些特定的按鍵、按鍵順序或按鍵組合來完成一個操作,很多快捷鍵往往與Ctrl鍵、Shift鍵、Alt鍵、Fn鍵及Windows平臺下的Windows鍵和Mac機上的Meta鍵等配合使用。利用快捷鍵可以代替鼠標快速選取命令,減少鼠標移動距離,提高代碼開發速度。表1-1所示為Visual Studio Code快捷鍵。
表1-1 Visual Studio Code快捷鍵

1.3.6 EMMET語法
在Visual Studio Code、WebStorm等編程環境中,構建HTML文檔結構時,常用EMMET語法,EMMET語法主要有后代>、兄弟+、上級^、分組()、乘法*、自增符號$,具體用法如表1-2所示。
表1-2 EMMET常用語法

1.3.7 Node.js介紹
Node.js發布于2009年5月,由Ryan Dahl開發,使用高效且輕量級的事件驅動、非阻塞I/O模型,是一個基于Chrome V8引擎的JavaScript運行環境和平臺。Node.js實質是對Chrome V8引擎進行了封裝,V8引擎執行JavaScript的速度非常快,性能非常好,非常適合在分布式設備上運行密集型數據的實時應用,用于方便地搭建響應速度快、易于擴展的網絡應用。包管理器npm是目前世界上最大的開源庫生態系統。
1.3.8 http-server介紹
http-server是一個簡單的零配置命令行HTTP服務器,非常適合日常測試、本地開發等環境。啟動http-server的命令是:"http-server[path][options]",其中path是網站發布根目錄文件夾路徑,options是服務器配置參數,具體如表1-3所示。
表1-3 http-server參數說明

1.3.9 在HTML中使用JavaScript
HTML頁面由定義頁面結構和內容的HTML、定義布局和外觀的CSS和改變交互行為的JavaScript腳本相互結合而成。在HTML中使用JavaScript時,使用<script>元素分為包含外部JavaScript文件和直接嵌入JavaScript代碼兩種方式,依照Web標準和方便獨立開發的原則,應將JavaScript代碼獨立于HTML文件。
通過使用<script>元素包含外部JavaScript文件時,src屬性是必需的,屬性值指向一個外部JavaScript文件鏈接,可以包含來自外部站點的JavaScript文件,比如鏈接到jQuery CDN。引用外部JavaScript文件時,</script>結束標記不能省略。帶有src屬性的<script>元素如果在<script></script>之間嵌入JavaScript代碼,嵌入的代碼會被忽略,即不能在引用JavaScript文件的同時內嵌JavaScript代碼。需要同時存在時,分別使用兩個<script>元素實現,如:
<!--鏈接到外部服務器JavaScript文件--> <script src="https://code.jquery.com/jquery.js"></script> <!--包含本地JavaScript文件--> <script src="js/bootstrap.min.js"></script>
使用<script>元素直接嵌入JavaScript代碼時,直接將代碼寫到<script>和</script>之間,JavaScript是所有現在瀏覽器及HTML5默認的腳本語言,在script標簽中不必使用type=“text/javascript”。商業開發應盡量避免把JavaScript代碼直接嵌入文檔,但對于練習來說,倒是一種簡捷的測試手段。在腳本內部出現</script>字符串時使用轉義字符“\”解決,如:
<script type="text/javascript"> alert("<\/script>") </script>
JavaScript代碼是否寫到HTML文件中,并不存在硬性規定,但使用外部文件是最佳實踐,具有以下好處。
(1)可維護性。遍及不同HTML頁面的JavaScript會造成維護問題,若把所有JavaScript文件都放在一個文件夾中,維護起來就輕松多了,否則JavaScript開發人員會受HTML標記干擾,難于集中精力開發JavaScript。
(2)可緩存。瀏覽器根據設置緩存鏈接的外部JavaScript文件。如果兩個頁面都使用同一個文件,那么這個文件只需下載一次,可以加快頁面的加載速度。
1.3.10 高性能JavaScript
隨著Web應用日趨豐富,越來越多的JavaScript被運用到網頁中,前端性能對用戶體驗的影響備受關注,成為開發者所面臨的最嚴重的可用性問題。JavaScript性能涉及語言特性、數據結構、瀏覽器機理、網絡傳輸等導致性能問題的原因。在本書相關任務中將陸續介紹如何關注并提升JavaScript性能,實現高性能JavaScript開發。
IE8、Firefox 3.5、Chrome2都允許并行下載JavaScript文件,所以<script>不會阻塞其他標簽下載。遺憾的是,JS(JavaScript)下載過程依然會阻塞其他資源的下載,比如圖片。盡管最新的瀏覽器通過允許并行下載提高了性能,但是腳本阻塞仍然是一個問題。因此,推薦將所有的<script>標簽放在<body>標簽的底部,以盡量減少對整個頁面渲染的影響,避免用戶看到一片空白。
多個<script>標簽會影響頁面渲染速度,減少頁面渲染所需的HTTP是網站提速的一條經典法則。所以,在產品環境下合并所有的JS文件會減少請求數,從而加快頁面渲染速度。除了合并JS文件,還可以壓縮JS文件。壓縮是指將文件中與運行無關的部分進行剝離。剝離內容包括空白字符和注釋。該操作通常可以將文件大小減半,還有一些壓縮工具會將局部變量的長度減小。
在緩存JS文件時,緩存HTTP組件能極大地提升網站回訪的用戶體驗。Web服務器通過“ExpiresHTTP響應頭”來告訴客戶端一個資源應該緩存多長時間。當然,緩存也有自己的缺陷:當應用升級時,你需要確保用戶下載到最新的靜態內容。這個問題可以通過改變靜態資源的文件名來解決。你可能在產品環境下看到瀏覽器引用jsapplication-20151123201212.js,這種就是以時間戳的方式保存新的JS文件,從而解決緩存不更新問題。
1.3.11 JavaScript執行順序
JavaScript程序按照在HTML文件中出現的順序逐行執行,有時語句的執行過程可能很復雜,要等到其他語句或函數運行完成才能執行,如greeting變量值要等greetUser()執行完成才能獲得。當一條語句必須調用其他一些代碼才能完成任務時,執行順序就會改變。函數體內的代碼不會被立即執行,只有當所有函數被其他程序調用時,該代碼才會被執行。調用函數的語句和函數處于同一執行上下文時,能正常執行。在解釋器中,每個執行上下文都包含variables對象(variables對象無法訪問,是瀏覽器的內部機制),它保存該執行上下文中的所有變量、函數和參數。每個執行上下文還可以訪問其上層的variables對象。函數作用域會被連接到其所屬的外層對象,其作用域覆蓋范圍是當前執行上下文的variables對象加上它所有外層執行上下文的variables對象。子對象可以訪問父對象的變量信息,但父對象卻無法獲取子對象的變量。每個子對象都會從相同的父對象那里拿到相同的信息。如果某個變量不在當前上下文的variables對象中,就會在其外層執行上下文的variables對象中繼續尋找,查找會影響性能,最好在使用變量時創建變量。JavaScript函數執行順序如下:
function greetUser(userName){ return ' HelloSean!'; } var greeting=greetUser(); alert(greeting);
1.3.12 腳本位置
HTML4規范指出script標簽可以放在HTML文檔的head標簽和body標簽中,并允許出現多次。傳統做法是把所有<script>元素放在<head>元素中,目的是把CSS文件、JavaScript文件等外部文件鏈接都集中到相同的地方,方便維護管理。多數瀏覽器使用單一進程來處理用戶界面刷新和JavaScript腳本執行,當瀏覽器在執行JavaScript代碼時,不能同時做其他任何事情。JavaScript執行過程耗時越久,瀏覽器等待響應的時間就越長。瀏覽器在加載網頁時,將先加載<head>元素內的所有內容,再加載<body>元素內的所有內容,如果在文檔的<head>元素中包含所有JavaScript文件,必須等到全部JavaScript代碼被下載、解析和執行完成,在下載和解釋JavaScript文件時,頁面的處理會暫停。無論當前JavaScript代碼是內嵌的還是包含在外鏈文件中,只要不存在defer和async屬性,頁面的下載和渲染都必須停下來等待腳本執行完成。瀏覽器都會按照<script>元素在頁面中出現的先后順序依次解析執行。瀏覽器在解析到body標簽之前,不會渲染頁面的任何部分,把腳本放在頁面頂部將會導致明顯的延遲,通常表現為延遲期間的瀏覽器窗口顯示空白頁面,用戶無法瀏覽內容,也無法與頁面進行交互。IE8、Firefox3.5、Safari4和Chrome2都允許并行下載JavaScript文件,下載過程本身不會阻塞其他script標簽,但會阻塞其他資源的下載,比如圖片。盡管腳本的下載過程不會相互影響,但頁面仍然必須等待所有JavaScript代碼下載并執行完成才能繼續。由于腳本會阻塞頁面其他資源的下載,為了避免這個問題,現在Web應用程序一般把全部JavaScript引用放在<body>元素中頁面內容的后面,將所有script標簽盡可能放到body標簽的底部,也就是結束標記</body>的前面鏈接(或插入)JavaScript代碼,以盡量減少對整個頁面下載的影響。這樣在解析包含的JavaScript代碼之前,頁面的內容將完全呈現在瀏覽器中,而用戶也會因為瀏覽器窗口顯示空白頁面的時間縮短而感到打開頁面的速度加快了。
1.3.13 組織腳本
由于每個script標簽初始下載都會阻塞頁面渲染,所以減少頁面內嵌腳本和外鏈腳本script標簽數量有助于改善這一情況。瀏覽器在解析HTML頁面的過程中每遇到一個script標簽,都會因執行腳本而導致一定的延時,因此最小化延遲時間將會明顯改善頁面的總體性能。放在link標簽之后的內嵌腳本會導致頁面阻塞去等待樣式表的下載,這樣做是為了確保內嵌腳本在執行時能獲得最精準的樣式信息,因此有人建議不要把內嵌腳本緊跟在link標簽的后面。
在處理外鏈JavaScript文件時,HTTP請求會帶來額外的性能開銷,下載單個100KB的文件將比下載4個25KB的文件更快,減少頁面中外鏈腳本的數量將會改善性能。大型網站和網絡應用多依賴于數個JavaScript文件,通過把多個文件合并成一個可以減少性能消耗。
1.3.14 無阻塞腳本
JavaScript傾向于阻止瀏覽器的某些處理過程,如HTTP請求和用戶界面更新,這是開發者所面臨的最顯著的性能問題。減少JavaScript文件大小并限制HTTP請求數僅僅是創建響應迅速的Web應用的第一步。Web應用的功能越豐富,所需要的JavaScript代碼就越多,所以精簡源代碼并不總是可行的。盡管下載單個較大的JavaScript文件只產生一次HTTP請求,卻會鎖死瀏覽器一大段時間。為避免這種情況,向頁面逐步加載JavaScript文件,在某種程度上不會阻塞瀏覽器。無阻塞腳本的秘訣在于在頁面加載完成后才加載JavaScript代碼,也就是window對象的load事件觸發后再下載腳本。
1. 延遲腳本
HTML4為script標簽定義了一個擴展屬性defer,該屬性指明元素所含的腳本不會修改DOM,因此代碼能安全地延遲執行。定義了defer屬性的script標簽可以放置在文檔的任何位置,src指定的JavaScript文件將在頁面解析到script標簽時開始下載,但并不會執行,直到DOM加載完成(window對象的load事件被觸發前),也不會阻塞瀏覽器的其他進程,因此這類文件可以與頁面中其他資源并行下載。
<script src="js/file1.js" defer></script>
HTML5提供的async屬性用于異步加載腳本,async與defer的相同點是采用并行下載,在下載過程中不會產生阻塞,區別在于執行時機,async是加載完成后自動執行,而defer需要等待頁面加載完成后執行。
2. 使用動態腳本
一段腳本是計算機能夠一步一步遵照執行的一系列指令。每一條單獨的指令或步驟叫一條語句。使用動態腳本有兩種方式:一種方式是通過修改script對象的src屬性值來更換一個外部JavaScript文件;另一種方式是使用document.write()方法輸出JavaScript腳本,這些動態輸出的腳本會被馬上執行,例如:
<script> document.write("<script>"); document.write("alert(' 程序運行完成!')"); document.write("<\/script>"); </script>
document.write()方法輸出的JavaScript字符串必須放在<script>標簽中,否則JavaScript解釋器因為不能識別JavaScript代碼而作為普通字符串顯示在頁面文檔中。
采用DOM可以動態創建HTML中的所有內容,可以用標準的DOM創建一個新的script元素,代碼如下:
<script> var script=document.createElement("script"); script.src="file1.js"; document.getElementsByTagName("head")[0].appendChild(script); </script>
新創建的script標簽src屬性所指定的file1.js文件,無論在何時啟動下載,文件的下載和執行過程不會阻塞頁面其他進程,動態腳本也可以插入到head標簽內而不會影響頁面的其他部分。
使用動態腳本節點下載文件時,返回的代碼通常會立刻執行。但是當代碼中包含供頁面其他腳本調用的接口時,必須跟蹤并確保腳本下載完成且準備就緒。可以在動態腳本的元素上注冊load事件,通過監聽此事件來獲得腳本加載完成時的狀態。動態腳本加載狀態檢測代碼如下:
function loadScript(url, callback) { var script = document.createElement("script"); script.src = url; document.getElementsByTagName("head")[0].appendChild(script); if (script.readyState) { script.onreadystatechange = function () { if (script.readyState == "load" || script.readyState == "complete") { script.onreadystatechange = null; callback(); } }; } else { script.onload = function () { callback(); } } }
這個函數接受兩個參數,JavaScript文件的URL和完成加載后的回調函數。函數中使用了特征檢測來決定在腳本處理過程中監聽哪個事件,再給src屬性賦值,然后將script標簽插入到頁面。loadScript()函數的用法如下:
// 加載單個js文件 loadScript('file1.js', function () { alert(' 文件已經加載!') }) // 確保按順序加載file2.js 、file3.js 和file4.js loadScript('file2.js', function () { loadScript('file3.js', function () { loadScript('file4.js', function () { alert(' 所有文件完成加載!') }) }) })
可以動態加載盡可能多的JavaScript文件到頁面上,但要注意文件的加載順序。在所有主流瀏覽器中,只有Firefox和Opera能保證腳本會按照指定的順序執行,其他瀏覽器將會按照從服務端返回的順序下載和執行代碼。解決加載順序依賴的更好的做法是按正確順序合并成一個文件,下載這個文件就能獲得所有執行代碼。如果script標簽設定了defer屬性,文件大并不會有顯著的性能影響。動態腳本加載憑借跨瀏覽器和易用的優勢,成為最通用的無阻塞加載解決方案。
3. XMLHttpRequest腳本注入
首先創建XMLHttpRequest(XHR)對象,然后用它下載JavaScript文件,最后通過創建動態script元素將代碼注入頁面中,代碼如下:
var xhr = newXMLHttpRequest() xhr.open('get', 'file1.js', true) xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { var script = document.createElement('script') script.text = xhr.responseText document.body.appendChild(script) } } } xhr.send(null)
以上代碼發送一個get請求獲取file1.js文件。事件處理函數onReadyStateChange檢查readyState是否為4,同時檢驗HTTP狀態碼是否有效(2XX表示有效響應,304表示從緩存讀取)。如果收到有效響應,就會創建一個script元素,設置該元素的text屬性為從服務器上接收到的responseText。
這種方法的優點是可以下載JavaScript代碼但不會立即執行,由于代碼是script標簽之外返回的,因此它下載后不會自動執行,開發者可以控制執行時機;另一個優點是代碼瀏覽器兼容性好。該方法的主要局限性是JavaScript文件必須與所請求的頁面處于相同的域,JavaScript文件不能從CDN下載,大型Web應用通常不會采用XHR腳本注入技術。
4. 推薦的阻塞模式
向頁面中添加大量JavaScript的推薦做法是先添加動態加載所需的代碼,然后加載初始化頁面所需的剩下的代碼。因為第一部分的代碼盡量精簡,甚至可能只包含loadScript()函數,下載執行都很快,不會對頁面有太多影響。一旦動態加載代碼就位,就可以用它加載剩余的JavaScript。將代碼放在body結束標簽之前,確保JavaScript在執行過程中不會阻礙頁面其他內容的顯示。其次,當第二個JavaScript文件完成下載時,應用所需要的所有DOM結構已經創建完畢,能夠執行交互準備,從而避免需要檢測頁面是否加載完成。使用阻塞模式加載JavaScript的代碼如下:
function loadScript(url, callback){ var script = document.createElement("script"); script.src = url; document.getElementsByTagName("head")[0].appendChild(script); if (script.readyState) { script.onreadystatechange = function () { if (script.readyState == "load" || script.readyState == "complete") { script.onreadystatechange = null; callback(); } }; } else { script.onload = function () { callback(); } } } loadScript("rest.js", function () { //回調函數 })
我們也可以采用Yahoo工程師Ryan Grove創建的更為通用的延遲加載工具LazyLoad,網址是http://github.com/rgrove/lazyload/,LazyLoad是loadScript()函數的增強版,該文件壓縮后約為1.5KB。
1.3.15 選取DOM對象
document對象由HTML元素構成,HTML元素用于描述網頁的結構,一個元素包含起始標簽和結束標簽及標簽內的內容。起始標簽可以包含屬性,屬性通常描述該元素的更多信息,全局屬性適用于每個元素,例如class、id、title屬性。屬性有名稱和值,值通常包含在一對雙引號中,如圖1-6所示。
有少數不包含任何內容的元素(例如img、br),它們有自結束的標簽。可以通過HTML元素的id屬性、類屬性和標簽名稱來選取DOM元素,還可以通過W3C Selectors API規范中的querySelector()和querySelectorAll()方法來選取元素。其中用于選取單個元素的方法有getElementById()、querySelector(),用于多個元素選擇的方法有getElementsByClassName()、getElementsByTagName()和querySelectorAll()。

圖1-6 HTML元素結構
document對象在瀏覽器窗口載入Web頁面時建立,包含了頁面的屬性(如title)、頁面事件(如load、click)和方法(如getElementById),使用document對象可以訪問或修改用戶在頁面上看到的內容,并根據用戶與頁面的交互方式進行響應。其中,title屬性就是head標簽內的title標簽值。lastModified屬性是頁面最后修改時間。需要注意的是,選擇元素時盡量明確界定元素搜索范圍,比如通過指定祖先元素、指定層級關系來限定查找范圍,提高搜索效率。
1. getElementById
getElementById方法是document對象特有的方法,JavaScript語言是區分字母大小寫的,寫成“getElementsById”“GetElementById”和“getElementByID”等將無法選取元素。函數名后的圓括號設置要選取元素的id屬性值,id屬性值必須放在單引號或雙引號里,getElementById返回帶有指定id屬性值的元素節點對應的對象,這個對象對應著document對象里的一個獨一無二的元素,比如document.getElementById("btn-alert")。一般來說,無須為文檔里的每一個元素都定義一個獨一無二的id值,DOM提供了另外的方法來獲取那些沒有id屬性的對象。支持的瀏覽器最低版本為IE5.5、Opera、所有版本的Chrome、Firefox和Safari。
2. getElementsByClassName
getElementsByClassName能夠通過元素頭標記中的class屬性的類名來訪問元素,該方法只接受查找的類名作為參數,語法格式為document.getElementsByClassName("class")。查找帶有多個類名的元素時,通過在類名之間使用空格來分隔類名,語法格式為document.get ElementsByClassName("class1class"),匹配與類的指定順序無關,與是否全部匹配無關,只要包含了指定的類名就匹配,該方法返回一個具有相同類名的元素數組。可通過數組索引值來訪問個別元素,或者通過使用數組長度length屬性構建循環遍歷每個元素,并執行相同的操作,比如綁定鼠標事件或者設置樣式。支持的瀏覽器最低版本為IE9、Opera10、所有版本的Chrome4、Firefox3和Safari4。為了兼顧老式瀏覽器,程序員需要使用已有的方法實現document.get ElementsByClassName,代碼如下:
function getElementsByClassName(node, classname){ if (node.getElementsByClassName) { return node.getElementsByClassName(classname); } else { var results = newArray(); var elems = node.getElementsByTagName(" * "); for (var i = 0; i < elems.length; i++) { if (elems[i].className.indexOf(classname) != -1) { results[results.length] = elems[i]; } } return results; } }
該函數接受node和classname兩個參數,node表示DOM樹的搜索起點,classname是要搜索的類名。如果瀏覽器支持getElementsByClassName函數就直接返回節點列表,否則,循環遍歷所有標簽,查找具有類為classname的元素,然后以返回節點列表。
3. getElementsByTagName
getElementsByTagName方法返回指定標簽的元素節點對應的對象數組,即使整個文檔里只有一個元素,也返回一個長度為1的數組,函數名后的圓括號設置要獲得的元素標簽名稱,每個對象分別對應著文檔里有著給定標簽的一個元素,getElementsByTagName允許把一個通配符(*)作為它的參數,返回文檔中元素節點的總數,用length屬性查出這個數組的元素個數,也可以把getElementsByTagName賦值給一個變量來改善代碼的可讀性。例如:
var para = document.getElementsByTagName('* ') for (var i = 0;i < para.length;i++) { console.log(para[i].nodeName) }
可通過數組索引值來訪問個別元素,或者通過使用數組長度length構建循環,對每個選取的對象執行相同的操作,比如綁定鼠標事件或者設置樣式。可以把getElementById和getElementsByTagName結合起來,通過id元素節點限定標簽查詢范圍,從而精確地獲取某個特定元素包含的子元素對象。例如:
var myCarousel = document.getElementById('carousel') var items = myCarousel.getElementsByTagName('div') for (var i = 0;i < items.length;i++) { console.log(items[i].nodeName) }
以上兩條語句執行完畢后,items數組將只包含id屬性值是carousel的元素所包含的div元素。支持的瀏覽器最低版本為IE6、Opera10、所有版本的Chrome、Firefox3和Safari4。
4. querySelector和querySelectorAll
W3C Selectors API規范中定義了querySelector和querySelectorAll方法,這種方式自然比使用JavaScript和DOM來遍歷查找元素更快,其作用是根據CSS選擇器規范,便捷地定位文檔中指定的元素。querySelector和querySelectorAll的參數須是符合CSS選擇器的字符串。querySelector()能夠獲得第一個匹配的節點。querySelector()和querySelectorAll()都是DOM對象節點的屬性,document.querySelectorAll()可以查詢整個文檔,而el.querySelectorAll()在子節點中進行查詢。querySelectorAll()使用CSS選擇器作為參數并返回一個NodeList,NodeList包含著匹配節點的類數組對象,而不是HTML集合,返回的節點不會對應實時的文檔結構。例如獲取頁面id屬性為slide的元素:
document.querySelector("#slide")
或者用
document.querySelectorAll("#slide")[0] 。
querySelectorAll()處理大量組合查詢時效率更高,用法是:
var divs=document.querySelectorAll("div.wrap,div.menu");
如果不使用querySelectorAll,要獲得相同的結果則復雜得多,需要選擇所有的div元素,遍歷剔除那些不符合條件的部分,然后將獲得的元素添加到元素集合數組,代碼如下:
var errs = [], divs = document.getElementsByTagName("div"), classname = ""; for (var i = 0, len = divs.length; i < len; i++) { classname = divs[i].className; if (classname === "wrap" || classname === "menu") { errs.push(divs[i]) } }
支持的瀏覽器最低版本為IE8、Opera10、所有版本的Chrome、Firefox3.5和Safari4。
5. 獲取兄弟元素
先獲取此元素的父節點的所有子節點,因為所有子節點也包括此元素自己,所以要從結果中去掉自己,再將符合條件的兄弟元素加入元素集合數組并返回。代碼如下:
function siblings (elm) { var arr_siblings = [] var p = elm.parentNode.children for (var i = 0,pl = p.length;i < pl;i++) { if (p[i] !== elm)a.push(p[i]) } return a }
還有另外一種看起來比較奇特的方法:先找到此元素的父節點的第一個子節點,然后循環查找此節點的下一個兄弟節點,一直到查找完畢。如jQuery里面獲取兄弟節點的代碼如下:
function sibling (elem) { var r = [] var n = elem.parentNode.firstChild for (;n;n = n.nextSibling) { if (n.nodeType === 1 && n !== elem) { r.push(n) } } return r }
1.3.16 addEventListener
要給一個對象指定一個或多個事件處理程序時,就不能使用屬性(如onload)來完成,而必須使用方法addEventListener。對對象調用addEventListener來注冊一個事件處理程序,如document.addEventListener("plusready",plusReady,false),表示對document對象添加plusready事件(第一個參數),第二個參數plusReady指向事件處理程序的引用,第三個參數false指定事件是否向上傳遞給父元素。對于頂層對象無關緊要,但如果元素有嵌套,并希望觸發了被嵌套元素時同時觸發外層嵌套的元素時,就要將第三個參數設置為true。可以使用removeEventListener刪除事件處理程序。對于IE8和更早版本,為確保事件能正常工作,可使用以下代碼:
if (element.addEventListener) { // 如果瀏覽器支持addEventListener ,就使用addEventListener 方法添加事件處理程序 element.addEventListener('click', handler, false) }else { //如果瀏覽器不支持addEventListener ,就使用attachEvent 方法添加事件處理程序, //不接受第3 個參數,事件名稱用"onclick" element.attachEvent('onclick', handler) }
1.3.17 讀寫HTML DOM style對象屬性
style對象代表一個單獨的樣式聲明。可從應用樣式的文檔或元素讀取和設置style對象的屬性值,使用style對象屬性的語法如下:
document.getElementById("id").style.property=" 值"
在JavaScript中,任何支持style屬性的HTML元素都有一個對應的style屬性。這個style對象是CSS Style Declaration的實例,它包含著通過HTML的style屬性指定的所有樣式信息,對象代表一個單獨的樣式聲明,其語法為element.style.property="值",style對象支持的屬性有背景、邊框和邊距、布局、列表、雜項、定位、打印、滾動條、表格、文本和標準屬性(dir、lang、title)。
使用style對象修改的是該元素的具體樣式聲明,如本任務的表格行tr背景顏色backgroundColor,而不是元素的類樣式名稱,如修改class="tr-hover"的tr-hover值。另外,使用“style.屬性名”設置元素聲明時,元素的樣式屬性帶有一個或者多個“-”連接符時,就與JavaScript語言的減法操作符沖突,JavaScript會解釋為減號,不能再用作屬性名稱,需改寫為駝峰命名法,把樣式屬性名稱中的“-”移除,將“-”后的字母大寫,如font-size改寫為fontSize,font-family改寫為fontFamily,margin-top-width改寫為marginTopWidth。多數情況下,都可以通過上面的規則簡單地轉換屬性名,但float不能直接轉換,因為float是JavaScript保留字,因此不能用作屬性名,Firefox、Safari、Opera和Chrome都支持DOM2級規定的樣式名cssFloat,而IE支持的則是styleFloat。
(1)讀取樣式。style屬性包含著元素的樣式,查詢style屬性將返回一個對象而不是一個簡單的字符串,樣式存放在該style對象的屬性中。使用element.style.property來獲取樣式,比如元素element的color屬性使用element.style.color獲得。獲取的樣式屬性所采用的單位并不總是與CSS樣式表里的設置相同,比如顏色值的返回值會轉化為RGB分量表示。獲取樣式支持對CSS簡寫屬性的解析,比如style="font:14px/1.5'TimesNewRoman'"。
(2)寫入樣式。style對象的各個屬性都是可以讀寫的,可以通過style屬性來獲取和設置樣式,語法如下:
element.style.property=value
value對象的屬性值永遠是一個字符串,必須放在單引號或者雙引號中,支持速記樣式,比如element.style.font="2em 'time'"。如果沒有引號,JavaScript會把style屬性值解釋為變量。使用DOM設置的樣式雖然比較容易,但能做什么事并不意味著應該做什么事。就像不應該利用DOM去創建頁面HTML結構,在絕大多數場合應該使用CSS去聲明重要樣式。在不方便使用CSS的場合,再使用DOM把它們檢索出來進行樣式設置和更新。
1.3.18 cssText
通過JS來覆寫對象的樣式是比較典型的一種銷毀原樣式并重建的過程,這種銷毀和重建都會增加瀏覽器的開銷。采用style屬性設置元素多條樣式時,樣式一多,代碼就很多,導致代碼冗余且會導致頁面重新渲染,一般情況下用JS設置元素對象的樣式會使用以下形式:
var element=document.getElementById("id"); element.style.width="20px"; element.style.height="20px"; element.style.border="solid 1px red";
采用cssText設置HTML元素的style屬性值可減少代碼冗余,盡量避免頁面重新渲染,提高頁面性能。語法為obj.style.cssText="樣式",例如:
element.style.cssText="width:20px;height:20px;border:solid1pxred; "; element.style.cssText="color:red;font-size:13px;";
但是,這樣會有一個問題:會把原有的cssText清掉,比如原來的style中有"display:none;",那么執行完上面的JS后,display就被刪掉了。為了解決這個問題,可以采用cssText累加的方法:
element.style.cssText+=";width:100px;height:100px;top:100px;left:100px; "