js变量后面加问号是什么_js没那么简单(1)-- 执行上下文
前言
我為什么寫這個(gè)文章?也許換個(gè)耳熟能詳?shù)脑掝}會有更多人看吧。之前發(fā)了個(gè)tls感覺閱讀量不行。
要講ecma語法嗎?我覺得還是不了吧,畢竟這些繁瑣,枯燥,而且門檻低。
那講什么好?講一點(diǎn)我自己覺得大家都知道,但是可能理解不到位都東西。
我自己理解到位嗎?我想不一定很到位,但是一定很有思考價(jià)值。
這是一個(gè)系列?它可能是一個(gè)系列,就從執(zhí)行上下文和運(yùn)行開始吧。
js難不難?看你自己都目標(biāo)吧,我覺得沒有簡單的東西,當(dāng)你思考越多,就會看到更多東西,相對以前的理解就是難的。
那就開始吧
正文
js或者ecmascript?
大家用了那么久js,有沒有搞清楚規(guī)范和實(shí)現(xiàn)的區(qū)別呢?ECMA這個(gè)組織定義了這個(gè)語言的規(guī)范,Javascript是這個(gè)規(guī)范的一個(gè)實(shí)現(xiàn)。這意味著,他可以有很多實(shí)現(xiàn)的可能,只是Javascript是其中一個(gè)最熱門的實(shí)現(xiàn)。我們通常說的ecma規(guī)范,那指的是一種口頭協(xié)議規(guī)范,通常我們說javascript語言,那指的是已經(jīng)實(shí)現(xiàn)了ecma某個(gè)規(guī)范的一種語言。
在這個(gè)基礎(chǔ)上,執(zhí)行上下文就是ecma規(guī)范里面提到的一個(gè)抽象概念。這意味著,這東西不是一個(gè)具體已經(jīng)實(shí)現(xiàn)出來的東西,他僅僅只是一個(gè)抽象模型,具體在計(jì)算機(jī)內(nèi)部是怎么編譯運(yùn)行,以什么樣的面向?qū)ο蟠a呈現(xiàn),那應(yīng)該是引擎(v8)實(shí)現(xiàn)的細(xì)節(jié)的內(nèi)容。
那么執(zhí)行上下文的意義在于,它可以給一個(gè)抽象模型,讓我們更簡單的預(yù)測js的運(yùn)行機(jī)制。同時(shí),執(zhí)行上下文對后續(xù)理解js內(nèi)存,垃圾回收,閉包等具有深刻意義,他可以幫助我們在不需要很了解基礎(chǔ)底層情況下去分析內(nèi)存,執(zhí)行過程。
js代碼是如何工作的?
為了不復(fù)雜化思路,我們可以暫時(shí)把js運(yùn)行過程分成上圖三個(gè)大步驟。 1. 獲取js代碼 2. 編譯 3. 運(yùn)行
編譯階段:js代碼在編譯階段(序列化-->抽象語法樹-->可執(zhí)行代碼)被編譯成機(jī)器可識別大可執(zhí)行代碼
運(yùn)行:運(yùn)行代碼
執(zhí)行上下文(Execution Contexts)
執(zhí)行上下文(Execution Contexts)是ECMA規(guī)范262第八章節(jié)中提出的抽象概念。這個(gè)概念定義了,js代碼在運(yùn)行時(shí),所處的上下文環(huán)境。在簡單的代碼中,我們可以簡單的理解上下文環(huán)境結(jié)構(gòu)由:詞法環(huán)境(Lexical Environments)和 變量環(huán)境(Variable Environment)兩個(gè)部分。我們這里只需要關(guān)注這兩部分:
詞法環(huán)境: 詞法環(huán)境定義了由代碼編譯過程中,ecma規(guī)范詞法對應(yīng)的一些關(guān)系,比如記錄函數(shù)內(nèi)部的this內(nèi)容,不對外暴露,可以理解為ecma內(nèi)部自己的語法關(guān)系。
變量環(huán)境:變量環(huán)境指的的是在詞法環(huán)境中,代碼運(yùn)行時(shí)生成的變量關(guān)系,可以理解為由我們創(chuàng)建的變量。
另外,我們寫的代碼,包括函數(shù)里的代碼執(zhí)行,在規(guī)范中叫可執(zhí)行代碼。于是,我們可以把代碼的運(yùn)行流程,更細(xì)致的概括為,那么執(zhí)行上下文和可以執(zhí)行代碼會伴隨在js的運(yùn)行周期里:
這我們在進(jìn)一步的理解執(zhí)行上下文,在js中,有三個(gè)比較場景會生成上下文對象: 1. 全局上下文 2. 函數(shù)上下文 3. eval上下文
所以,JS只有三種環(huán)境下會生成執(zhí)行上下文,這意味著js不像c語言那樣,具有單獨(dú)塊作用域的概念,只有函數(shù)作用域和全局作用域
執(zhí)行上下文的生成時(shí)機(jī)
上面我們把代碼的過程抽象成編譯時(shí)和運(yùn)行時(shí)。而執(zhí)行上下文會在編譯時(shí)就確定上下文關(guān)系,所以可以認(rèn)為,在編譯過程中,在解析js代碼所對應(yīng)的詞法關(guān)系時(shí)候,編譯器就已經(jīng)確定了代碼中每個(gè)環(huán)境對應(yīng)的執(zhí)行上下文的關(guān)系,只是說,這時(shí)候還沒被激活。雖然這里還沒提到作用域鏈,但是我們通常把這種在詞法階段確定的關(guān)系叫做靜態(tài)。由于執(zhí)行上下文中也會有作用域鏈,所以JS通常被稱為詞法作用域或者靜態(tài)作用域。
這意味著,js在編譯階段其實(shí)已經(jīng)做好了很多事情,當(dāng)然也包括我們常說的變量提升。 讓我們看下真實(shí)代碼中如何體現(xiàn):
運(yùn)行過程和調(diào)用棧
既然加執(zhí)行上下文,那它必然和執(zhí)行時(shí)候密切相關(guān)。相信大部分人都知道,我們常說的會說的名詞,函數(shù)調(diào)用棧。這個(gè)函數(shù)調(diào)用棧其實(shí)就是執(zhí)行上下文的調(diào)用棧。我們上面提到,還有全局環(huán)境會生成全局上下文,eval環(huán)境會生成eval上下文。所以,這些上下文都會在激活時(shí)候進(jìn)入調(diào)用棧。
比如,全局上下文,在編譯完,代碼開始運(yùn)行時(shí)候就開始入棧,因?yàn)槿汁h(huán)境是最先開始運(yùn)行的。
function a(){} function b(){}a() b()如圖,對于全局環(huán)境來說,可執(zhí)行代碼如圖。實(shí)際在運(yùn)行時(shí),內(nèi)存里應(yīng)該以機(jī)器碼形式存在。當(dāng)運(yùn)行到a(),a函數(shù)到執(zhí)行上下文會生成然后入棧。
從執(zhí)行上下文中看變量提升
變量提升是一個(gè)我們經(jīng)常關(guān)注的內(nèi)容,我們通常把變量提升解釋為,在js預(yù)編譯階段會對變量做一個(gè)提升,這里可以用一個(gè)簡單對demo來重現(xiàn)這一經(jīng)典現(xiàn)象:
console可以看到,在變量聲明前使用它,完全沒有問題。對于經(jīng)常使用js的人,這代碼并沒有任何稀奇。
但是,如果我們更深一層的去思考,變量提升的本質(zhì)是什么。我們回想上面js的運(yùn)行過程。從一段js代碼,編譯成可執(zhí)行代碼。我們把這個(gè)代碼帶到這個(gè)流程中去,我可以進(jìn)一步把上面的代碼抽象成這樣:
入圖所示,以上js腳本代碼,通過詞法解析,編譯器會確認(rèn)為該段代碼具有兩個(gè)不同的上下文環(huán)境,每一個(gè)環(huán)境中對應(yīng)的內(nèi)容我也標(biāo)記出來了。比如全局上下文中,對應(yīng)可執(zhí)行代碼是:
console.log(val) add(1, 2) var val = 1其對應(yīng)的環(huán)境變量是val和add函數(shù)指針,函數(shù)指針值得是其對應(yīng)的是靜態(tài)代碼區(qū)域的可執(zhí)行代碼。實(shí)際上是函數(shù)上下文中對應(yīng)的可執(zhí)行代碼。
那么在運(yùn)行時(shí)候,全局上下文首先激活入棧,然后全局的腳本代碼開始執(zhí)行:
當(dāng)執(zhí)行到console.log時(shí)候,我們看到,雖然我們腳本代碼中val在console.log后面,但是依然打出來了undifined,而不是報(bào)錯(cuò)。這是得力于詞法環(huán)境到功勞。因?yàn)閖s在編譯時(shí)候就幫我們生成對應(yīng)到變量,只不過,其還沒有對應(yīng)到值而已。
然后當(dāng)游標(biāo)執(zhí)行到add(1, 2)時(shí),由于函數(shù)變量也已經(jīng)生成,并且由于時(shí)函數(shù)聲明形式。所以編譯器時(shí)知道函數(shù)對應(yīng)到可執(zhí)行代碼所處到指針,于是調(diào)用了函數(shù),然后激活函數(shù)到上下文,并且入棧。其調(diào)用棧正如上文所示。即使函數(shù)在代碼中是在執(zhí)行代碼到后面,但是得力于詞法解析到功勞,add函數(shù)變量在編譯時(shí)已經(jīng)生成。
最后當(dāng)執(zhí)行到val = 1時(shí)候,函數(shù)先出棧,然后變量環(huán)境到val也會對應(yīng)得到賦值。
這里需要說下,就是為了能夠大家看懂,我用js的方式展示執(zhí)行代碼。但是實(shí)際上編譯完成的執(zhí)行代碼應(yīng)該是機(jī)器碼。可以看到圖中,變量環(huán)境中,兩個(gè)變量val,和add分別是undefined和一個(gè)指向函數(shù)的一個(gè)引用。
到這里,我相信應(yīng)該就很容易理解,為什么會存在變量提升這樣的現(xiàn)象。本質(zhì)上是因?yàn)閖s在編譯過程中的詞法解析階段,就已經(jīng)生成了執(zhí)行上下文的關(guān)系,所以代碼還沒運(yùn)行時(shí)候,變量的環(huán)境已經(jīng)創(chuàng)建好了,而在代碼運(yùn)行時(shí)候。即使我們的執(zhí)行代碼是比變量更前的,依然可以拿到變量的引用,在代碼運(yùn)行時(shí),上下文對象才會激活。
所以這一章節(jié)重點(diǎn)就是:上下文對象生成時(shí)機(jī)在詞法解析階段,而上下文對象激活時(shí)機(jī)在運(yùn)行階段
eval環(huán)境
Eval代碼在運(yùn)行時(shí),上下文中會多一個(gè)調(diào)用所處環(huán)境多上下文引用。
變量提升的問題
變量提升可以認(rèn)為是最初js設(shè)計(jì)上的一些不足,因?yàn)橛缮厦娴拿枋龅弥?#xff0c;這種從簡的設(shè)計(jì)導(dǎo)致了變量提升。這種提升會在一些可能的塊作用域中產(chǎn)生一些影響。比如while,for循環(huán)。對于那些曾經(jīng)接觸過c或者java這類語言的人來說,js這樣簡單的只有函數(shù)作用域塊的特點(diǎn)會很難以理解。在for循環(huán)和while里面變量的提升,都會導(dǎo)致變量在全局情況下被覆蓋,無法緩存的問題。
當(dāng)然后面es6也有l(wèi)et和const的概念去解決塊作用域的問題。但是本質(zhì)上來說,變量提升不是一個(gè)很好的特性。
最后,可以通過上下文對象試著去想,閉包多本質(zhì)是怎么樣的,后續(xù)有時(shí)間在討論。
總結(jié)
以上是生活随笔為你收集整理的js变量后面加问号是什么_js没那么简单(1)-- 执行上下文的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: js 获取域名_确定你会使用JS操作Ur
- 下一篇: 原码一位乘法器设计_对原码、反码和补码的