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