一 概念解析:
1 JS編譯
JS不是提前編譯的,而是在執(zhí)行前由JS引擎在幾微秒內(nèi)進行編譯
編譯三部:
1浮庐、詞法分析 (分析有啥詞并整理成詞法單元流(由詞構(gòu)成的數(shù)組))
2、語法分析(詞法單元流整理成抽象語法樹(abstract syntax code,AST))
3会傲、代碼生成(樹整理成機器指令磅废,等待執(zhí)行)
JS在整個編譯過程中的語法分析和代碼生成階段有特定步驟對運行性能進行優(yōu)化缕探。如:對冗余元素優(yōu)化等。
這就是JS被稱之為動態(tài)語言的原因
2 作用域
變量生效的范圍被稱為作用域还蹲。
作用域有兩種主要的工作模型:
1爹耗、詞法作用域耙考,在編譯過程的語法分析階段確定作用域,定義在詞法階段的作用域潭兽,詞法作用域是由變量倦始、塊或函數(shù)作用域定義位置來決定的。
2山卦、動態(tài)作用域鞋邑,在執(zhí)行過程中確定作用域,例如eval可以接受字符串用來欺騙原有作用域账蓉,導(dǎo)致JS引擎在編譯階段的性能優(yōu)化動作失效枚碗,并且難以維護。
js中有三種類型作用域:
a.全局作用域(全局變量擁有全局作用域铸本,在JS代碼中的任何地方都是有定義的)
b.函數(shù)作用域
c.塊級作用域(let const tryCatch with eval可以生成塊級作用域)
同一作用域內(nèi)的所有對象以及變量可以被作用域 內(nèi)的代碼訪問肮雨。
2.1 作用域的由來
ES5 只有全局作用域和函數(shù)作用域,沒有塊級作用域箱玷。
ES6 明確規(guī)定怨规,如果區(qū)塊中存在let和const命令,這個區(qū)塊對這些命令聲明的變量锡足,從一開始就形成了封閉作用域波丰。凡是在聲明之前就使用這些變量,就會報錯舶得。
let和const實際上為 JavaScript 新增了塊級作用域掰烟。
ES5 規(guī)定,函數(shù)只能在頂層作用域和函數(shù)作用域之中聲明沐批,不能在塊級作用域聲明媚赖。
// 情況一
if (true) {
function f() {}
}
// 情況二
try {
function f() {}
} catch(e) {
// ...
}
但是,瀏覽器沒有遵守這個規(guī)定珠插,為了兼容以前的舊代碼惧磺,還是支持在塊級作用域之中聲明函數(shù),因此上面兩種情況實際都能運行捻撑,不會報錯磨隘。
ES6 引入了塊級作用域,明確允許在塊級作用域之中聲明函數(shù)顾患。ES6 規(guī)定番捂,塊級作用域之中,函數(shù)聲明語句的行為類似于let江解,在塊級作用域之外不可引用设预。
// 瀏覽器的 ES6 環(huán)境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重復(fù)聲明一次函數(shù)f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
如果改變了塊級作用域內(nèi)聲明的函數(shù)的處理規(guī)則,顯然會對老代碼產(chǎn)生很大影響犁河。為了減輕因此產(chǎn)生的不兼容問題鳖枕,ES6 在附錄 B里面規(guī)定魄梯,瀏覽器的實現(xiàn)可以不遵守上面的規(guī)定,有自己的行為方式宾符。
- 允許在塊級作用域內(nèi)聲明函數(shù)酿秸。
- 函數(shù)聲明類似于
var
,即會提升到全局作用域或函數(shù)作用域的頭部魏烫。 - 同時辣苏,函數(shù)聲明還會提升到所在的塊級作用域的頭部。
上面的例子實際運行的代碼如下哄褒。
// 瀏覽器的 ES6 環(huán)境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
引用自: ECMAScript 6 入門
3 作用域鏈
JS中有兩種作用域鏈:
1稀蟋、靜態(tài)作用域鏈:全局、函數(shù)或塊級的作用域鏈(執(zhí)行聲明代碼時創(chuàng)建此作用域鏈)
2呐赡、動態(tài)作用域鏈:執(zhí)行上下文的作用域鏈(在執(zhí)行過程中生成退客,初始化為當(dāng)前作用域的作用域鏈,使用完畢后執(zhí)行上下文被銷毀)
4 執(zhí)行上下文(execution context)罚舱,又叫執(zhí)行環(huán)境:
執(zhí)行上下文是一個JS的內(nèi)部對象遏餐。
擁有作用域的代碼在每次執(zhí)行時都會創(chuàng)建一個獨一無二的執(zhí)行上下文诫隅,代碼執(zhí)行完畢后執(zhí)行上下文被銷毀。
執(zhí)行上下文的生命周期:
1谍咆、創(chuàng)建階段:
ExecutionContext = {
ThisBinding = <this value>, // 確定this
LexicalEnvironment = { ... }, // 詞法環(huán)境 存儲函數(shù)聲明和變量( let 和 const )綁定
VariableEnvironment = { ... }, // 變量環(huán)境 僅用于存儲變量( var )綁定
}
2窃肠、執(zhí)行階段包个,執(zhí)行代碼并賦值
3、執(zhí)行完畢冤留,銷毀執(zhí)行上下文
5 執(zhí)行棧:
執(zhí)行上下文依次放入執(zhí)行棧中碧囊,依次執(zhí)行;單線程的JS纤怒,同一時間只能執(zhí)行一個方法糯而,這些方法被放在一個棧內(nèi)等待執(zhí)行,這個棧就是執(zhí)行棧泊窘。
6 閉包:
閉包是執(zhí)行代碼可以訪問執(zhí)行代碼聲明位置的作用域
1熄驼、函數(shù)在定義時作用域以外的地方被調(diào)用,此函數(shù)在定義時的作用域依然存在且可以被函數(shù)訪問烘豹,這就是閉包瓜贾。(你不知道的JS)
2、閉包類似于JS的內(nèi)置對象携悯,閉包的作用域鏈包含了函數(shù)創(chuàng)建位置的執(zhí)行上下文相同的作用域鏈祭芦,因此在執(zhí)行上下文的作用域在代碼執(zhí)行完畢后無法被銷毀。(高性能JS P25)
在瀏覽器中驗證閉包的描述:
函數(shù)在定義作用域內(nèi)被調(diào)用憔鬼,依然產(chǎn)生了閉包龟劲,只不過這個閉包沒有被保存下來胃夏,執(zhí)行完畢后直接被銷毀。但這不能推翻描述1中的閉包概念咸灿,只能說1中描述的是一個更為嚴格的閉包概念构订,需要函數(shù)在特定位置被調(diào)用才能稱之為閉包(狹義閉包),有實用價值避矢。而閉包的產(chǎn)生更為常見悼瘾,只要函數(shù)調(diào)用自身作用域以外非全局作用域中的值就可以形成閉包(廣義閉包)。
瀏覽器中真實觀察到的閉包如下圖审胸。
二 JS代碼執(zhí)行過程:
1亥宿、引擎編譯JS代碼
2、主線程順序執(zhí)行編譯后的JS代碼
圖中stack是執(zhí)行棧砂沛,heap是存儲基本類型的堆烫扼,圖中少了一個存儲引用類型的棧。
3碍庵、棧內(nèi)執(zhí)行上下文遵循先進后出規(guī)則(FILO)映企。全局執(zhí)行上下文進棧后,直到程序關(guān)閉才會銷毀静浴。
JS主線程在執(zhí)行棧中從上到下執(zhí)行棧內(nèi)代碼堰氓,遇到存在作用域的代碼塊,會向執(zhí)行棧中添加對應(yīng)的執(zhí)行上下文苹享,進入這個執(zhí)行上下文繼續(xù)執(zhí)行代碼双絮,執(zhí)行完畢后會銷毀對應(yīng)的執(zhí)行上下文,直到同步代碼執(zhí)行完畢得问。(執(zhí)行上下文可能存放到了存儲引用類型的棧內(nèi))
4囤攀、當(dāng)遇到異步任務(wù)時,將異步任務(wù)的回調(diào)函數(shù)放入到事件隊列中宫纬,繼續(xù)執(zhí)行代碼焚挠;在此過程中回調(diào)函數(shù)如果引用了函數(shù)外作用域內(nèi)的變量,會將此函數(shù)漓骚,會形成閉包蝌衔。
5、當(dāng)執(zhí)行棧中的代碼執(zhí)行完畢后认境,去事件隊列中檢查是否有可執(zhí)行的回調(diào)函數(shù)胚委,存在,將回調(diào)函數(shù)放入執(zhí)行棧叉信。這個檢查會循環(huán)執(zhí)行亩冬,而這個循環(huán)檢查就是事件循環(huán)。
tips:異步任務(wù),例如:定時器硅急、ajax覆享、promise、DOM事件营袜。
三 異步任務(wù)的細節(jié):
以上的事件循環(huán)過程是一個宏觀的表述撒顿,不同異步任務(wù)的執(zhí)行優(yōu)先級有區(qū)別。異步任務(wù)被分為兩類:微任務(wù)(micro task)和宏任務(wù)(macro task)荚板。
以下事件屬于微任務(wù):new Promise()凤壁、Object.observe、MurtationObserver
以下事件屬于宏任務(wù):setInterval()跪另、setTimeout()拧抖、dom事件、I/O免绿、UI rendering唧席、requestAnimationFrame
單次事件循環(huán)處理過程中,先處理微任務(wù)嘲驾,所有的微任務(wù)都會被處理淌哟,然后處理宏任務(wù),只處理一個宏任務(wù)辽故。當(dāng)微任務(wù)隊列清空后徒仓,事件循環(huán)會檢查是否需要更新UI視圖。
參考:
這篇參考文章有有些邏輯無法理解
詳解JavaScript中的Event Loop(事件循環(huán))機制
需要參考這篇核對上篇文章
深入理解javascript原型和閉包系列
https://es6.ruanyifeng.com/#docs/let
前端進階系列
形象化解讀js事件循環(huán)以及node事件循環(huán)和瀏覽器事件循環(huán)的區(qū)別
JavaScript事件循環(huán)機制及微任務(wù)與宏任務(wù)