代碼為什麼要這樣寫? 而不這樣? 很明顯 我們都知道第一個性能要比第二個好,為什麼呢? 首先要瞭解 js引擎的 Data Access 數據訪問。這裡只說 非優化的js引擎。 1、訪問 Literal values 直接量 eg:字元串,數字,布爾值,對象,數組,函數,正則表達式,具有特殊意義的空值 ...
代碼為什麼要這樣寫?
function initUI(){ var doc = document, bd = doc.body, links = doc.getElementsByTagName_r("a"), i = 0, len = links.length; while(i < len){ update(links[i++]); } doc.getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active"; }
而不這樣?
//avoid
function initUI(){ var bd = document.body, links = document.getElementsByTagName_r("a"), i = 0, len = links.length; while(i < len){ update(links[i++]); } document.getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active"; }
很明顯 我們都知道第一個性能要比第二個好,為什麼呢?
首先要瞭解 js引擎的 Data Access 數據訪問。這裡只說 非優化的js引擎。
1、訪問 Literal values 直接量 eg:字元串,數字,布爾值,對象,數組,函數,正則表達式,具有特殊意義的空值,以及未定義。
2、訪問 Variables 變數 var創建用於存儲數據值。
3、訪問 Array items 數組項 具有數字索引的數組對象。
4、訪問 Object members 對象成員 具有字元索引的js對象。
從上面的例子我們也能猜到 訪問 局部變數比訪問對象成員和數組項更快。
那麼為什麼訪問對象成員 和數組項要慢呢? 書中說的很詳細,我只介紹一下自己的理解和摘錄。
大多數 JavaScript 代碼以面向對象的形式編寫。無論通過創建自定義對象還是使用內置的對象,諸如
文檔對象模型(DOM)和瀏覽器對象模型(BOM)之中的對象。
對象成員包括屬性和方法,在 JavaScript 中,二者差別甚微。對象的一個命名成員可以包含任何數據類
型。既然函數也是一種對象,那麼對象成員除傳統數據類型外,也可以包含一個函數。當一個命名成員引
用了一個函數時,它被稱作一個“方法”,而一個非函數類型的數據則被稱作“屬性”。JavaScript中的對象是基於原形的。原形是其他對象的基礎,定義並實現了一個新對象所必須具有的成
員。這一概念完全不同於傳統面向對象編程中“類”的概念,它定義了創建新對象的過程。原形對象為所有
給定類型的對象實例所共用,因此所有實例共用原形對象的成員。一個對象通過一個內部屬性綁定到它的原形。Firefox,Safari,和 Chrome 向開發人員開放這一屬性,稱
作__proto__;其他瀏覽器不允許腳本訪問這一屬性。任何時候你創建一個內置類型的實例,如 Object 或
Array,這些實例自動擁有一個 Object 作為它們的原形。因此,對象可以有兩種類型的成員:實例成員(也稱作“own”成員)和原形成員。實例成員直接存在於
實例自身,而原形成員則從對象原形繼承。
這些很難理解,但是看了後很透徹。緊接著最後一句話,你調用實例成員肯定比調用原型成員要快,比如
var book = { title: "High Performance JavaScript", publisher: "Yahoo! Press" }; alert(book.toString()); alert(book.title);
book 本身是沒有toString 的 但是程式編譯並不報錯,因為toString是它的原型成員。
假設book.title === book.toString() book.toString() 依舊比 book.title 更慢。
book創建後 其 原型綁定在 _proto_ 內部屬性上 , 觀察這個原型可以看到 toString() 這個標識符 book 本身並沒有toString這個function
而是從它的原型上繼承得來。其實在調用 book.title 的時候,js引擎如何知道 title是否是未定義的呢?
在函數執行過程中,每遇到一個變數,標識符識別/解析這些變數的方法是按順序搜索運行期上下文的作用域鏈查找同名標識符,如果找不到就是未定義,
所以通常返還未定義註定是檢索了整個作用域鏈。正是這種搜索影響了性能。
如果在作用域鏈里 toString 比 title更靠後那麼就 可以確定title 會比toString更先找到。
什麼是運行期上下文,什麼是作用域鏈?
每一個 JavaScript 函數都被表示為對象。進一步說,它是一個函數實例。函數對象正如其他對象那樣,
擁有你可以編程訪問的屬性,和一系列不能被程式訪問,僅供 JavaScript 引擎使用的內部屬性。其中一個
內部屬性是[[Scope]],由ECMA-262 標準第三版定義。內部[[Scope]]屬性包含一個函數被創建的作用域中對象的集合。此集合被稱為函數的作用域鏈,它決定
哪些數據可由函數訪問。此函數作用域鏈中的每個對象被稱為一個可變對象,每個可變對象都以“鍵值對”的形式存在。當一個函數創建後,它的作用域鏈被填充以對象,這些對象代表創建此函數的環境中可訪問
的數據。例如下麵這個全局函數:function add(num1, num2){
var sum = num1 + num2;
return sum;
}
圖中 只給出了 全局對象 在作用域鏈的位置,並沒有不包括所有的。
註意:scope chain (作用域鏈) 里的 這些可變對象都是以鍵值對的形式存在的。
圖中的 activation object 被譯為 激活對象 此對象是在 此add函數被調用時創建的 例如:運行此代碼 var s = add(1,2);
運行此段代碼時會創建一個內部對象(之前已經提到,形如[[xxx]])稱作 execution context 就是上面講的運行期上下文。 它定義了一個函數運行是的環境。
而值得一提的是 對函數的每次運行而言,每個運行期上下文都是獨一的,所以多次調用同一個函數就會導致多次創建運行期上下文。當函數執行完畢,運行期上下文就被銷毀.
運行期上下文也是有scope chain的。這個作用域鏈被用於 標識符解析 。
當 運行期上下文被創建的時候 它的作用域鏈被初始化 連同 函數的[[scope]]屬性 中所包含的對象 會按照順序被覆制到 運行期上下文的作用域鏈里。最後你看到的就是
激活對象了。 如圖它被推向了作用域鏈的前端。 而標識符識別是從前到後的。
簡單講最終結果是 toString 標識符所在的位置 是作用域的後端 而 title 標識符實在作用域鏈的更前端,所以toString會更慢。
回到最開始的話題你會發現 你僅僅是 把 document 緩存到一個 局部變數 doc 里就可以減少 一次或者更多次非常深的 標識符掃描。
同時可以意識到 成員嵌套越深掃描作用域鏈越深。成員嵌套越深,訪問速度越慢。location.href總是快於 window.location.href
這有點類似於 尾遞歸的作用了。
書中詳細說了 標識符性能、 動態作用域、閉包可能導致記憶體泄漏、改變作用域鏈。以及有關原型、原型鏈。總之受益匪淺。