1.問題 首先把問題放出來,昨天看了一個網友發的一個問題,然後跟我同事一起研究了一下,沒找出來是為什麼,然後我回來一直在想為什麼,然後各種找資料研究,從各個方面找為什麼,比如js上下文,作用域,js垃圾回收,堆棧調用情況等等。 2.js斷點調試找答案 首先如果不看上面的圖,以你現在知道的js知識,你 ...
1.問題
首先把問題放出來,昨天看了一個網友發的一個問題,然後跟我同事一起研究了一下,沒找出來是為什麼,然後我回來一直在想為什麼,然後各種找資料研究,從各個方面找為什麼,比如js上下文,作用域,js垃圾回收,堆棧調用情況等等。
2.js斷點調試找答案
首先如果不看上面的圖,以你現在知道的js知識,你覺得列印出來應該是什麼。第二張圖其實列印出來的結果在意料之中,原因就是函數聲明提升,沒問題,但是第一張圖為什麼呢?這裡可以發散一下思維,比如說是不是在塊作用域中,變數和函數之間存在某種互相覆蓋的問題啊,或者說先在塊中聲明的會被掛載到全局的window對象下麵,後面聲明的就掛載不上去了,並且不會覆蓋,然後可以把代碼稍微改改,驗證一下你的思想,很有意思。然後下麵我們斷點調試看下:
首先進花括弧一步都沒走的時候,但是a和b已經掛載到全局變數的window對象下麵了,這就說明代碼塊中隱式生命的變數是全局變數,代碼相當於這樣:此時我們再看a在塊作用域中就已經是方法了,註意此時我function a(){}
這段代碼還沒走完呢,這就說明函數聲明在js解析(註意是解析不是執行)的時候代碼塊中被提升到了代碼塊頂部,進花括弧的那一刻起,函數就已經被聲明瞭,我們再往下麵一步走
此時a不管在塊作用域還是全局作用域中都變成了a函數,那這裡是都可以理解為運行上面一行代碼,然後就給全局變數下的a賦值為函數呢,我們再看下一步
當a=50;
走完之後,也就出了塊作用域,此時我們看到沒有了塊作用域,因為已經出了塊作用域了,然後全局對象window裡面a還是函數,並不是50,但是如果你在塊作用域a後面加一行的斷點看的話,此時塊作用域裡面的a的值為50,問題就在這裡,為什麼此時塊作用域裡面的a的值跟全局window對象下麵的值結果不一樣呢?
然後我們再往下走一步:
然後進第二個塊作用域,發現跟前面進第一個塊作用域一樣,還沒執行第一行,塊作用域裡面的b已經是函數了,原因也跟第一個一樣js解析的時候函數聲明提升,然後我們再往下走一步:
這一步走完我們發現塊作用域裡面的b已經變成50了,但是全局window對象下麵的b還是undefined,這我也不知道為什麼,那我也就只能說此時b是定義在塊作用域中的內部變數了,再往下走一步
但是當我走出塊作用域的時候,b竟然在全局對象下變成了50,那就證明我上面說的不對,b不是塊作用域中的內部變數,因為此時執行完方法立馬就出塊作用域了,我們看的不是很清楚,我們在方法下麵加一行代碼,方便調試看結果:
確實是當我b函數那一步走完,塊作用域和全局對象window下麵的b都變成了50,那我這裡我就認為是函數b在js解析的時候就被提升到了塊作用域的最上面,執行到b函數那一步其實在之前就已經執行過了,相當於js執行的時候代碼變成下麵這樣:
{ function b() {}; b = 50; }
我們再看這個代碼不正是上面a那一個塊作用域的代碼嗎,所以在塊作用域中js執行的時候上下兩個塊作用域中是一樣的,所以在塊作用域中列印a,b得到的結果都是50,然後下一步:
出了塊作用域,就只有全局對象window了,然後window對象下麵的b還是50,所以最後列印出來也是50。走到這一步就所有的步驟都走完了,那麼我們再回頭看上面的a為什麼塊作用域中的值跟window對象下麵的a的值不一樣,通過走完下麵一個代碼塊我們發現上面代碼塊跟下麵代碼塊只有函數放的位置不一樣,結果就不一樣,那我們就看一下這裡函數聲明提升到底是怎麼提升的。
3.塊作用域中的函數聲明提升
然後我就找到阮一峰博客裡面寫的關於es6塊級作用域的文章:
http://es6.ruanyifeng.com/#docs/let#%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F
另一篇關於js變數的生命周期的文章:
https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
在第一篇文章中,看見裡面有真麽一段話:
然後找到這麼一句話,我就點鏈接進去看: http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics 發現es6規範裡面真的有對代碼塊作用域的一些規定,但是我也沒太看懂,反正能確定跟這有關係。然後我又點擊了另一個行為方式的鏈接進去看: https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6#comment50817344_31419897 然後在這個回答里找到這麼一個回答,這個回答說代碼塊作用域中定義的函數類似於var定義的變數,(前面阮一峰博客里也是這麼說的),然而第二個綁定僅在塊內部可見,也就是說,第二個綁定在外面是訪問不到的,那用這段話來解釋我們代碼的話就是先不管代碼塊中的函數聲明提升,然後從上面往下運行,看見第一個就綁定到全局的window對象上,第二個就只在函數作用域內可見,那這樣的話我如果在代碼塊內部列印,那結果應該是誰在後面定義我們就列印誰啊,而列印的結果卻是證明瞭函數提升存在的。所以這個好像也解釋不通。然後從這個回答裡面我又找到一個這個鏈接: https://github.com/estools/escope/issues/73 然後的然後我就不知道該怎麼去看這個問題了,但是我相信應該接近答案了,或許答案就藏在上面es6規範B3.3裡面的某個點。當然也可以從調用棧,js垃圾回收,js上下文,js引擎執行解析過程,函數與變數聲明創建原理等等各個方面去分析,這應該是一個值得去分析思考的問題,同時也是很有意思的一個問題,相信你是能學到一些東西的,下麵有一些參考鏈接,如果感興趣可以研究一下,在平常寫代碼的時候可能永遠也不會遇到,很有意思的問題。 參考資料: 1.http://es6.ruanyifeng.com/#docs/let#%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F 2.http://www.ecma-international.org/ecma-262/6.0/index.html#sec-block-level-function-declarations-web-legacy-compatibility-semantics 3.https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6#comment50817344_31419897 4.https://github.com/estools/escope/issues/73 5.https://dmitripavlutin.com/javascript-hoisting-in-details/ 6.https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/ 7.https://fangyinghang.com/let-in-js/ 8.https://eslint.org/docs/rules/no-inner-declarations 9.https://www.cnblogs.com/liuhe688/p/5891273.html 10.https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/133