變量對象
js 解析器是如何找到咱們定義的函數和變量的?
實際是經過VO(Varible Object)來存儲執行上下文所須要存儲的好比變量、函數聲明、函數參數。進一步咱們還須要區分全局變量對象和函數變量對象。
咱們定義一個全局變量就會有一個全局變量對象,裏面有剛定義的全局變量
定義函數,除了有全局變量對象外,還有函數變量對象。html
簡單說分爲全局做用域、和局部做用域。全局做用域對應全局變量,局部做用域對應局部變量。一般理解的局部做用域是定義在函數內部的做用域。數組
當咱們定義一個全局變量的時候,它就在全局環境裏,做用域鏈只有一條。當咱們定義一個函數,首先在定義的時候,或者說在js流執行的時候,會將函數和變量申明提早,並且函數申明比變量更提早。在申明函數的時候,會生成一個scope屬性。此時,[[scope]]裏面只包含了全局對象【Global Object】。而若是, 咱們在A的內部定義一個B函數,那B函數一樣會建立一個[[scope]]屬性,B的[[scope]]屬性包含了兩個對象,一個是A的活動對象【Activation Object】【對於函數來講】一個是全局對象,A的活動對象上面,全局對象在下面。以此類摧,每一個函數的都在定義的時候建立本身的[[scope]],裏面保存着一個相似於棧的格式的數據。瀏覽器
// 外部函數 function A(){ // 內部函數 function B(){ } }
瞭解函數執行環境前先看下三個概念。閉包
咱們一般能夠在代碼第一行使用相似String、Math等這樣的全局函數,爲什麼能直接使用到?
是由於咱們在初始化代碼之前,js引擎會把全局的一些東西,我理解成[[global]]會初始化到VO裏面。
僞代碼:ide
[[global]] = { Math : ..., String : ..., window : global, // 執行全局對象自己 } String(10) // [[global]].String(10) window.a = 10 // [[global]].window.a = 10 this.b = 10 // [[global]].b = 10
Active Object 縮寫AO,該對象有函數的arguments類數組對象和this對象。
**慕課網這一章節說,函數裏面 AO === VO
慕課網--Js深刻淺出--閉包章節函數
VO或者AO按照以下順序初始化:字體
經過一個實例來講明變量初始化階段this
function test (a, b) { var c = 10 function d(){} var e = function _e(){} (function(){}) // 括號括起來的匿名函數,但並沒執行,此時函數申明不會提早 // 這裏多說下,括起來的不論是匿名函數,仍是正常申明的函數,不只不會申明提早,還會 // 造成一個閉包,只有經過自執行才能調用。在全局做用域或在申請的局部做用域調用都 // 是undefined.這裏一個簡單粗暴的解釋是爲什麼沒有提早,若是提早就留下一個括號,豈不 // 是很奇怪。哈哈。 b = 20 } test(10) AO(test) = { a: 10, // a 已傳入,因此有值 b: undefined, // b 未傳入,undefined,而且局部變量b發生命名衝突,但被忽略 c: undefined, // 局部變量 d: <ref to func "d">, // 函數申明 e: undefined // 命名函數_e,賦值給了局部變量e,undefined } // 分析發現,局部變量c和e兩個局部變量都是undefined,由於開頭就說了,變量申明被前置,因此在初始化的時候就是undefined,好比咱們在var e = function _e() {} 前面調用e //() 確定會報錯語法錯誤,e不是一個函數。
每一個函數運行時都會產生一個執行環境,並且同一個函數運行屢次時,會屢次產生不一樣的執行環境。js爲每個執行環境關聯了一個變量對象。環境中定義的全部變量和函數都保存在這個對象中。 全局執行環境是最外圍的執行環境,它所關聯的對象就是咱們熟知的window對象。js的執行順序是根據函數的調用來決定的,當一個函數被調用時,該函數環境的變量對象就被壓入一個環境棧中。而在函數執行以後,棧將該函數的變量對象彈出,把控制權交給以前的執行環境變量對象。spa
var scope = "global"; function fn1(){ return scope; } function fn2(){ return scope; } fn1(); fn2();
當函數被執行的時候,就是進入這個函數的執行環境,首先會建立一個它本身的活動對象【Activation Object】(這個對象中包含了this、參數(arguments)、局部變量(包括命名的參數)的定義,固然全局對象是沒有arguments的)和一個變量對象的做用域鏈[[scope chain]],而後,把這個執行環境的[scope]按順序複製到[[scope chain]]裏(也有博客說把做用域[[scope chain]]鏈賦值給[[scope]]),最後把這個活動對象推入到[[scope chain]]的頂部。這樣[[scope chain]]就是一個有序的棧,這樣保了對執行環境有權訪問的全部變量和對象的有序訪問。.net
分析下:
當執行fn1的時候,VO中fn1會指向fn1的執行環境,由於函數申明前置,因此在VO中已經存在function。
能夠看到fn1活動對象裏並無scope變量,因而沿着做用域鏈(scope chain)向後尋找,結果在全局變量對象裏找到了scope,因此就返回全局變量對象裏的scope值。
引入閉包代碼
function outer(){ var scope = "outer"; return function inner(){ return scope; } }
調用outer()
這裏有點不解的是?若是outer()直接調用,而後再調用inner即outer()()不會形成閉包,但把outer賦值給一個全局變量,上面閉包代碼的寫法,就會成爲一個閉包。
var fn = outer(); fn();
分析下:
執行outer函數,在VO對象中outer會指向它的執行環境,當由於變量申明前置,fn在初始化變量的時候是undefined。因此在outer函數執行中的時候,他依然是undefined.
如圖,outer執行完後,執行環境再也不被VO中的outer所指向,即執行環境已被銷燬。
當第一次執行fn到底發生了什麼?執行fn即執行inner函數,此時outer的執行環境已被銷燬(並且只能有而且只有一個執行環境),而outer()執行後又賦值給了fn,因此能夠這麼理解,inner執行的時候執行環境也賦值給了fn。因爲fn這個全局變量一直存在,除非你手動置爲null或關閉瀏覽器,因此能夠認爲inner這個函數的執行環境就一直存在,那麼inner函數執行環境的相關變量就一直存在。
經過上面加粗文字的理解,趁熱打鐵,再來看一個函數:
function outer() { var a = 1 return function inner() { a ++ } } var f = outer() console.log(f()) // 2 console.log(f()) // 3
爲什麼f()第二次執行的時候,會是3。結合上面加粗字體的理解,f()在執行完後,outer的AO並無被釋放,當第二次執行f()的時候,因爲inner函數AO並無變量a,因此沿着[[scope]]chain 中查找,結果在outer的AO中找到了變量a,但此時變量a已是2了,因此再a ++ 後就是3了。除非咱們關閉瀏覽器,或者將f = null,再次執行f(),就會回到初始化的狀態。
理解了閉包再來看下面這個代碼就很好理解了。
function outer(){ var result = new Array(); for(var i = 0; i < 2; i++){ //定義一個帶參函數 result[i] = function(num){ // num 形參 // 實際num這個形參拷貝了實參i這個變量的一個副本到arguments裏。 // i 的變化就不會影響到這個副本的變化。 console.log('arguments', arguments) // innerarg 的執行環境裏面引用了result[i]這個數組函數的活動對象 // 當執行innerarg 的時候,做用域鏈會去找num,在result[i]這個數組活動對象的 // arguments 找到了 return function innerarg(){ console.log('num', num) return num; } }(i);//預先執行函數寫法 //把i當成參數傳進去 實參 } console.log('result', result) return result; } var fn = outer() fn[0]() fn[1]()
起初我以爲上面那個代碼實在是太複雜,爲什麼要在result[i]這個函數數組裏再返回一個函數。起初我是這麼寫的。結果發現我錯了,但又對匿名函數自執行,又從新正確的認識了下。
function outer(){ var result = new Array(); for(var i = 0; i < 2; i++){//注:i是outer()的局部變量 result[i] = function(num){ console.log(num) return i; }(i) // result[i]表面上是被一個函數所賦值,但這是一個自執行函數,自執行函數又 // 返回了它的實參。因此若是咱們調用fn[0]()就會提示fn[0]不是一個function. // 你修改爲result[i] = (function(num){...}(i))也是一個自執行,只不過 // 多了一層閉包。 } console.log('result', result) return result;//返回一個函數對象數組 //這個時候會初始化result.length個關於內部函數的做用域鏈 } var fn = outer(); fn[0] fn[1]