又有好長時間沒有寫博客了,今天想起來之前的那篇博客還沒有寫完,然後就開始接著寫,本來想把《高性能JavaScript》這本書的知識都羅列進來的,但是......太多了,哎,還是慢慢來,於是就打算分開來寫。 本人JavaScript水平並不是特別高,也只是把自己閱讀《高性能JavaScript》的部分 ...
-------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------
載入和執行
-
腳本阻塞
由於javascript語言是單線程的,決定了javascript語言是單線程的,因此在瀏覽器上UI的刷新與javascript代碼的邏輯執行就無法同步執行。
瀏覽器中遇到界面上的<script> 標簽,瀏覽器無法判斷script標簽中的代碼是否會更行dom節點(若需要更新dom則會引起界面的二次重繪)。因此在瀏覽器中,遇script標簽的時候,將會暫停dom的繪製和渲染以及界面的交互響應,直到javascript被載入(包括外鏈下載的過程),執行。
腳本的合併能夠減少系統發送http請求的次數,能夠減少建立http連接的次數,從而減少阻塞的時間。
-
實現無阻塞載入的方法(nonblocking script)
-
使用script標簽的defer和async屬性,顯式通知瀏覽器引入的script標簽不會改變dom結構,瀏覽器在載入的時候將會非同步載入
-
defer與async載入的比較:兩者都能夠實現腳本的非同步載入,即下載的過程中不會阻塞頁面的渲染。區別在於執行的時機,帶有async屬性的script標簽在下載完成後將會自動執行,而然帶有defer屬性的script標簽則要等到頁面載入完成後才執行。
-
defer屬性只有在script標簽存在src屬性時才會生效,即僅對外聯腳本有效
-
-
動態腳本載入,通過JavaScript代碼生成一個頁面script標簽,併為其指定src,文件的下載將不會阻塞頁面
let script = document.createElement('script'); script.type = 'text/javascript'; script.src = `${src}`; document.getElementsByTagName('head')[0].appendChild(script);
-
使用動態方式載入腳本時,當js文件下載完成後將會自動執行,但如果有多個腳本文件註入時,由於js文件的下載速度不同,可能導致文件執行的順序錯亂,尤其當後一個腳本文件依賴於上一個腳本文件的執行時將會出錯。
// 解決方法:通過監聽script標簽的載入狀態並觸發回調,在回調函數中載入依賴的javascript文件 // 載入腳本的方法 var loadScript(url, callback) { var script = document.createElement('script'); script.src = url; // 在非IE瀏覽器中(主流瀏覽器),script標簽載入完成時會觸發onload事件,而IE瀏覽器中script的載入狀態通過readyState實現,readyState有以下 值:'uninitialized'(初始狀態),'loading'(下載中),'loaded'(下載完成),'interactive'(數據下載完成但尚不可用),'complete'(所有數據準備就緒),在IE中需要監聽readyState的狀態改變,以便在合適的時機觸發回調 if(script.readyState) { // IE舊版本相容 script.onreadyStatechange = function(){ if(script.readyState === 'loaded' || readyState === 'complete') { script.onreadyState = null; callback(); } } } else { script.onload = function () { callback() } } } // callback中可以載入下一個順序執行的javascript文件
註意:當有多個文件需要順序執行時,將會有多層回調函數的嵌套,容易形成回調地獄
loadScript('file1.js', function(){ loadScript('file2.js', function(){ loadScript('file3.js', function () { loadScript('files.js'); }) }) }) // 載入順序: file1.js --> file2/js --> file3.js --> file4.js // 當多文件嵌套時,儘可能將多個文件按照載入順序合併成一個文件
-
-
XMLHttpRequest腳本註入
-
通過XHR對象非同步下載javascript文件
var xhr = new XMLHttpRequest(); xhr.open('get', `${url}`); shr.onreadystatechange = function () { if(xhr.readyState===4) { if(xhr.state>=200 && xhr.state < 300 || xhr.state == 304) { var script = document.createElement('script'); script.type = 'text/javascript'; script.text = xhr.responseText; document.getElementsByTagName('head')[0].appendChild(script); } } } xhr.send(null);
註意:該方法不能夠進行跨域操作
-
-
數據存取
-
明確javascript中有四種數據存取位置
-
字面量 --> 相當於常量吧,例如 var a = 100,其中100就是字面量,javascript中的字面量包括:字元串,數字,布爾值,對象字面量,數組字面量,函數,正則字面量以及null,undefined,他們只代表字面量本身的值而不具備特定的存儲位置
-
本地變數,使用var,let等關鍵字聲明的變數,將字面量存儲在特定的變數上,如var a = 100,其中a就是本地變數
-
數組元素,存儲在javascript數組中的成員,通過下標獲取。註意:此處的數組元素與數組字面量是不同的概念,如[1, 2, 3, 4],整體表示的是是數組字面量,而不涉及到數組內部元素的存取;[1,2,3,4] [0]則涉及到數組內部元素的訪問
-
對象成員,對象與對象字面量的區別與數組類似
通常情況下,訪問字面量或者局部變數的性能消耗相對較小,而對於數組元素和對象成員的訪問代價相對較大,如果對於運行速度有較高的要求,那就儘可能使用字面量和局部變數,減少使用數組和對象,當然當下主流瀏覽器對於變數的訪問進行了優化,對數組和對象的訪問速度的差異已經沒有那麼明顯了。
-
-
函數作用域鏈對變數訪問的影響
-
javascript中是通過鏈的方式來保存作用域的,當一個函數被創建時,該函數的執行期上下文將會被添加到作用域鏈的頂端。
const name = 'zhang'; let test = function () { let a = 100; console.log(name); } test (); // test執行時需要訪問變數name,而在自身的函數作用域中不存在變數name,因此會向上層作用域進行回溯。當然作用域鏈的層數不一定只有兩層,當多層作用域連接時,訪問變數會一次沿著作用域鏈向上搜索。
// 當然也有可能訪問完所有的作用域,依然找不到變數,此時就會拋出一個ReferenceError,也就是說,當你在函數中訪問一個不存在的變數時,此時將會沿著作用域鏈向上搜索,直至頂層作用域,效率很低。 -
-
由於全局變數時存放在全局作用域中的,位於作用域鏈的最底部,因此對於全局變數的訪問將會很大程度上影響性能 ,在javascript中考慮性能優化時需要儘量避免訪問全局變數,當需要多次訪問全局變數時,可以將其保存到局部變數之中,以加快訪問的速度
-
let visitDom () { const dom1 = document.getElementById('test1'); const dom2 = document.getElementById('test2'); const dom3 = document.getElementById('test3'); const dom4 = document.getElementById('test4'); ....... } // 其中,document就是一個全局變數,當訪問document時,函數將需要沿著作用域鏈層層回溯,如果函數所在的層次更深一些,那麼訪問的效率就更低 let visitDom2 () { const doc = docuemnt; const dom1 = doc.getElementById('test1'); const dom2 = doc.getElementById('test2'); const dom3 = doc.getElementById('test3'); const dom4 = doc.getElementById('test4'); } // 在上述方法中,使用一個局部變數將需要頻繁訪問的變數document保存起來,此時只需要沿著作用域鏈搜索一次,其餘的操作只要訪問當前作用域即可得到 // 通過以局部變數保存全局變數的方式,減小搜索的深度。 改變作用域鏈 with()語句 const visitDom () { const a = 100, b = 200, c = 300; with (document) { // with語句強制性將document對象添加到函數的作用域鏈的頂部,即在當前with函數域中document是頂級作用域 const dom1 = getElementById('test1'); const dom2 = getElementById('test2'); const dom3 = getElementById('test3'); const dom4 = getElementById('test4'); console.log(a); console.log(b); console.log(c); } // with()語句執行結束時,document對象將會從作用域鏈頂部移除 } // 看似訪問效率提高了,可是在[[with()函數作用域①]]中,對於局部變數的訪問效率將會降低 // 此處個人的理解可能存在偏差,特此更新,為記錄自己的錯誤理解,因此採用更新的方式糾正 // 在with語句中訪問a,b,c時,若沒有with語句,就是訪問局部變數;可是有了with語句,由於當前的作用域被document代替,因此,需要向上層作用域進行搜索,此時是的對於局部變數的訪問成本增加。 // 當然,在上面這個案例中,完全可以避開這種缺陷,即將console.log語句移動到with之外,就可以避免局部變數的訪問,但有些時候是無法將其分離的,比如說在getelementById()函數中使用局部變數,此時就無法分離開來。
-
with()語句可能會增加對局部變數的訪問成本,因此需要謹慎使用,而且在ES5嚴格模式中已不推薦使用with()語句改變作用域鏈了。
****************************************************************************************
①今天看了《JavaScript高級程式設計》,裡面有一節延長作用域鏈,看了裡面的代碼,運行了一下,結果是能跑通的,於是乎,就感覺自己之前的理解有誤
with()語句看起來像一個函數,但其本質上與函數作用域是存在區別的,其本質上是一條語句
function buildUrl () { var qs = '?name=zhang'; with (location) { var url = href + qs; } return url; // 如果with()是一個函數作用域的話,此處訪問url將會報錯,但實際上並不會,因此,with()語句是不會識別為作用域的 } document.write(buildUrl());
-
更新時間:2019-04-15 10:52:52
-
****************************************************************************************
-
catch()語句
catch語句中需要接受一個異常參數,並對異常進行處理,但cath語句會將異常對象推入到作用域鏈頂端,就如同with一樣,會造成局部變數訪問的性能開銷
try { do something ... } catch (ex) { // 此時ex對象作為catch函數作用域鏈中的頂層作用域 handle exception ... } // 不同於with()語句,有些時候catch語句的作用是無法被替代的,因而很難避免使用catch語句,我們需要獲取ex異常對象,進行一些異常處理,那麼方法來了,可以聲明一個專門的異常處理函數,將ex對象作為參數傳入 try { do something ... } catch (ex) { handerExp(ex); } const handlerExp = function (ex) { const a = 100, b = 200, c = 300; handle exception ... } // 此時訪問局部變數就不會存在回溯作用域鏈的情況了
-
動態作用域
with(),catch()以及eval()語句都屬於動態作用域,動態作用域只存在於代碼執行的過程中,因此無法通過靜態分析檢測
function excute (code) { eval (code); function subroutine () { return window; } var w = subroutine; console.log(w); } // 此時w的值其實是無法靜態分析的,只有在code值確定時方能確定 excute('var window = 100'); // w --> 100 excute('var a = 100'); // w --> window(全局對象)
案例引自《高性能JavaScript》 作者:[美]Nicholas C.Zakas; 丁琛譯
使用eval()會使得瀏覽器自身的靜態分析優化失效
(部分瀏覽器自身實現了通過代碼分析確定哪些變數會被訪問,從而避開搜索作用域鏈,進而達到優化變數搜索的功能)
因而,一般儘量避免使用動態作用域
-
-
對象成員的訪問
-
對象原型
javascript是通過原型的方式實現繼承
hasOwnProperty()與in操作符的比較:
兩者都是用來判斷某一個key是否存在於對象內部
不同點在於:hasOwnProperty()方法只會遍歷對象本身的屬性,而in操作符會遍歷對象原型上的屬性
因此,頻繁的使用in操作符會降低搜索的效率
-
原型鏈
function Book (title, publisher) { this.title = title; this.publisher = publisher; } Book.prototype.sayTitle () { alert(this.title); } const book1 = new Book('High Performance JavaScript', 'Yahoo! Press'); const book2 = new Book('JavaScript:The Good Parts', 'Yahoo! Press'); alert(book1 instanceof Book); // true alert(book2 instanceof Object); // true book1.sayTitle(); // High Performance JavaScript alert(book1.toString()); // [object Object]
當需要訪問原型鏈上某個對象上的變數時,同樣需要類似於作用域鏈那樣一層層向上搜索。對於原型鏈的搜索會帶來類似於函數作用域鏈那樣的開銷。
-
a.b.c.d --> 對象成員嵌套的層數越多,訪問的開銷越大
-
對於一個多次訪問但是不發生修改更新操作的對象,可以將其緩存為局部變數,以減小性能的開銷
const a = { b: { c: { d: { method () { do something ... } } } } } // 當我們需要頻繁使用method方法時,就需要重覆沿著嵌套對象一層一層往下找 適 const d = a.b.c.d d.method(); d.method(); // 這是一種折中的方案,此時method方法中的this指向d,同時也避免了不必要的嵌套搜索
-
----------------------------------------------------------------------------
好了,今天《JS高程》又到了,感覺自己水平還是不太夠,有些基礎還有待加強哈!!
自勉
-----------------------------------------------------------------------------
2019-04-11 15:34:23