本文主要從原理入手分享變量和做用域的相關知識,最後結合本文所分享知識,再次深刻了解下閉包的運行原理。html
主要參考《JS高級程序設計》 《JS權威指南》 《高性能 JS》 三本書。前端
目錄閉包
1 變量函數
1.1 變量的聲明性能
1.2 變量類型的特色學習
2 執行環境和做用域測試
3 再談談閉包this
對 JavaScript 稍微有點了解的同窗都知道,JavaScript 中的變量與其餘語言的變量有很大區別。spa
JS 的弱類型的特性決定了變量只是在特定時間(生命週期,做用域中)用於保存特定值的一個名字而已。翻譯
一個變量能夠在生命週期內任意改變類型。(太靈活了以致於好複雜)
JavaScript 變量能夠用來保存兩種類型的值: 基本類型值和引用類型值。
在JavaScript 程序中,使用一個變量以前應該先聲明它。變量使用 var 關鍵字聲明的。
若是在聲明的時候沒有進行初始化,那麼默認初始化爲undefined。
重複聲明和遺漏聲明
使用 var 關鍵字重複聲明變量時合法的並且無害的,若是重複聲明帶有初始化,那麼該聲明和一條普通的賦值語句沒什麼兩樣。
在非嚴格模式下,給未聲明的變量賦值,JS 會在全局做用域中建立一個同名變量,並賦值。(這會形成不少bug,所以應該使用 var 來聲明變量。)
保存引用類型值的變量咱們能夠爲其添加、改變和刪除其屬性和方法。
var person = new Object (); person.name = "tony"; console.log (person.name); // tony person.name = "googny"; console.log (person.name); // googny delete person.name; console.log (person.name); // undefined
而基本數據類型值卻不能動態添加、修改和刪除其屬性(話說它也沒屬性啊。。。)
var googny = "boy"; googny.girlfriend = "candice"; console.log (googny.girlfriend); // undefined 沒有女友。。。
基本類型值源自如下5種基本數據類型:Undefined、Null、Boolean、Number和String。
// 簡單測試一下 var num1 = 10; var num2 = num1; console.log (num1 === num2); // true num2 +=10; console.log (num1 === num2); // false
var googny = new Object (); googny.girl = "candice"; var tony = googny; console.log (googny.girl); // candice console.log (tony.girl); // candice tony.girl = 'candice lee'; console.log (googny.girl); // candice lee console.log (tony.girl); // candice lee
var s = "googny"; var i = 10; var b = true; var o = new Object(); var a = new Array(); var u; var n = null; console.log( typeof s); // string console.log( typeof i); // number console.log( typeof b); // boolean console.log( typeof o); // object console.log( typeof a); // object console.log( typeof u); // undefined console.log( typeof n); // object
// instanceof var person = new Object(); var girls = new Array(); console.log( person instanceof Object); // true console.log( girls instanceof Array); // true
(《JS 高級程序設計》裏對執行環境和做用域的講解讓我暈暈的,有好多疑問,因此參考了其它的書目,好比《高性能 JavaScript 》下載地址 (翻譯的實在不敢恭維,我都懷疑這個電子書和紙質書不同))
這裏就經過《高性能 JavaScript 》一書中對做用域的原理講解來反過來掌握做用域鏈、變量對象,執行環境,活動對象等相關概念和標識符解析的過程。
做用域對JavaScript有許多影響,從肯定哪些變量能夠被函數訪問,到肯定this的值。(關於做用域與性能的話題,請讀者參考《高性能 JavaScript 》)
下面咱們就經過實例來了解做用域的原理:
一些概念:
JS中 函數也是對象,對象就有屬性,屬性又分爲代碼能訪問的,和代碼不能訪問(後者被稱爲內部屬性)。內部屬性是供 JavaScript 引擎使用的。函數的其中一個內部屬性是[[scope]] (通常代碼不可訪問的屬性都帶有[[]]).
該屬性包含了一系列對象表明函數的做用域。這個集合就被稱爲做用域鏈,它決定了哪些變量能訪問,哪些函數能調用。
此函數做用域鏈中的每一個對象都被稱爲變量對象(variable object),這些對象裏面存放着 key-value 對的條目。
當函數被建立的時候,它的做用域鏈就被外部執行環境(父執行環境和爺執行環境)的變量對象「初始化」,這些變量對象中包含該函數可以訪問的數據。
舉個例子:
首先定義一個全局函數:
function add (num1, num2) { var sum = num + num2; return sum; }
當 add () 函數被建立後,它的做用域鏈被一個變量對象初始化,即全局對象(全部定義在全局做用域中的變量和函數都是改對象的成員)。
如圖所示:
var total = add(5, 10);
函數運行的時候,會觸發建立一個內部對象,稱爲執行環境,它定義了一個函數運行時的環境。執行環境和函數的執行一一對應(每次執行完函數,執行環境會被銷燬,下次執行,會從新建立。)
執行環境有本身的做用域鏈,該做用域鏈用於標識符解析。當環境變量被建立的時候,它的做用域鏈被函數的[[Scope]]屬性中包含的對象依次(按順序喲)初始化。
這時,一個被稱爲「活動對象」的新對象就爲執行環境建立好了,這個活動對象就做爲執行環境的變量對象,它包含了全部的本地變量(arguments對象,this)。
而後,這個對象被放在做用域鏈的最前端,當執行環境被銷燬時(函數執行完畢),活動對象也被銷燬。
如圖所示:
在函數運行過程當中,每遇到一個變量,標識符識別過程要決定從哪裏得到或者存儲數據。
此過程搜索執行環境的做用域鏈,查找同名的標識符。搜索工做從運行函數的活動對象(做用域鏈的前端)開始。
若是找到了,那麼就使用這個具備指定標識符的變量;若是沒找到,搜索工做將進入做用域鏈的下一個對象。
此過程持續運行,直到標識符被找到,或者沒有更多對象可用於搜索,這種狀況下標識符將被認爲是未定義的。
(至於搜索過程的效率和做用域之間的關係等話題,也有不少值得學習的,請讀者參考《高性能 JavaScript》)
閉包能夠訪問函數做用域外的變量和方法。
舉個例子:
var increment = function ( ) { var count = 0 ; return function () { count ++; return count; }; }();
每次調用increment() 函數就會返回一個增量的值,但是count 是在外層函數中定義的,爲何increment 函數能訪問的到呢?
increment 函數 在匿名外部函數執行時建立,能夠訪問外部做用域的count變量。閉包能訪問count是由於一個特殊的做用域鏈被建立。
當匿名外部函數執行時,一個活動對象被建立,根據上面的知識,咱們知道它做爲做用域鏈上的第一個對象,全局對象是第二個。
當閉包被建立時,它的[[scope]]屬性被這些對象依次初始化(由於這些相對閉包來講都是外部執行環境啊)。
因爲閉包的[[Scope]]屬性包含與執行環境做用域鏈相同的對象引用,會產生反作用。一般,一個函數的活動對象與執行環境一同銷燬。
當涉及閉包時,活動對象就沒法銷燬了,由於引用仍然存在於閉包的[[Scope]]屬性中。
這意味着腳本中的閉包與非閉包函數相比,須要更多內存開銷。在大型網頁應用中,這多是個問題。
當閉包被執行時,一個執行環境將被建立,它的做用域鏈被[[Scope]]中的做用域鏈的兩個對象初始化,而後一個新的活動對象添加到執行環境做用域鏈的前端。
如圖所示
談一點兒性能的問題,如上圖所示,閉包中要對count進行++ 操做,但是閉包的活動對象中沒有count,因此每次調用都得從下一個變量對象中查找,因此性能是有點損失的。
詳細的性能方面的考慮,還得參考《高性能 JavaScript》。
更多內容盡在這裏:相關博客一覽表