一、JS代碼執(zhí)行流程
JS的執(zhí)行機(jī)制:先編譯益缎,再執(zhí)行谜慌。js代碼在編譯階段,會創(chuàng)建執(zhí)行上下文链峭,變量和函數(shù)會被放到變量環(huán)境中畦娄,變量初始化為undefiend;在執(zhí)行階段弊仪,js引擎會從變量環(huán)境中查找自定義的變量和函數(shù)熙卡。
變量提升:js代碼執(zhí)行過程中,js引擎會把變量的聲明部分和函數(shù)的聲明部分提升到代碼開頭的“行為”励饵。變量被提升后會給變量設(shè)置默認(rèn)值undefined驳癌。
實(shí)際上變量和函數(shù)聲明在代碼里的位置是不會改變的,而是在編譯階段被js引擎放入內(nèi)存中役听。(js代碼先被js引擎編譯颓鲜,編譯完成后進(jìn)入執(zhí)行階段)。具體分析:
一典予、編譯階段
1甜滨、執(zhí)行上下文:js執(zhí)行一段代碼時的運(yùn)行環(huán)境,在執(zhí)行上下文中存在一個變量環(huán)境的對象(Viriable Environment)瘤袖,該對象中保存了變量提升的內(nèi)容衣摩。
示例:
showName()
console.log(myname)
var myname = '極客時間'
function showName() {
console.log('函數(shù)showName被執(zhí)行');
}
- 第 1 行和第 2 行,這兩行代碼不是聲明操作捂敌,所以 JavaScript 引擎不會做任何處理艾扮;
- 第 3 行,由于這行是經(jīng)過 var 聲明的占婉,因此 JavaScript 引擎將在環(huán)境對象中創(chuàng)建一個名為 myname 的屬性泡嘴,并使用 undefined 對其初始化;
- 第 4 行逆济,JavaScript 引擎發(fā)現(xiàn)了一個通過 function 定義的函數(shù)酌予,所以它將函數(shù)定義存儲到堆 (HEAP)中,并在環(huán)境對象中創(chuàng)建一個 showName 的屬性奖慌,然后將該屬性值指向堆中函數(shù)的位置霎终。
這樣就生成了變量環(huán)境對象。接下來 JavaScript 引擎會把聲明以外的代碼編譯為字節(jié)碼升薯。
二、執(zhí)行階段
JavaScript 引擎開始執(zhí)行“可執(zhí)行代碼”击困,按照順序一行一行地執(zhí)行涎劈。
- 注:如果存在同名的函數(shù)或者同名的變量广凸,在編譯階段,前者會被后者覆蓋蛛枚。即谅海,最終存儲在變量環(huán)境中的是最后定義的那個。如果變量和函數(shù)同名蹦浦,編譯階段扭吁,變量的聲明會被忽略,變量環(huán)境中存儲的是函數(shù)聲明盲镶,而不論順序(函數(shù)提升比變量提升優(yōu)先級高)
代碼編譯時有三種情況會創(chuàng)建執(zhí)行上下文
- 當(dāng) JavaScript 執(zhí)行全局代碼的時候侥袜,會編譯全局代碼并創(chuàng)建全局執(zhí)行上下文,而且在整個頁面的生存周期內(nèi)溉贿,全局執(zhí)行上下文只有一份枫吧。
- 當(dāng)調(diào)用一個函數(shù)的時候,函數(shù)體內(nèi)的代碼會被編譯宇色,并創(chuàng)建函數(shù)執(zhí)行上下文九杂,一般情況下,函數(shù)執(zhí)行結(jié)束之后宣蠕,創(chuàng)建的函數(shù)執(zhí)行上下文會被銷毀例隆。
- 當(dāng)使用 eval 函數(shù)的時候,eval 的代碼也會被編譯抢蚀,并創(chuàng)建執(zhí)行上下文镀层。
調(diào)用棧:在執(zhí)行上下文創(chuàng)建好后,js引擎會將執(zhí)行上下文壓入棧中思币。這種用來管理執(zhí)行上下文的棧稱為執(zhí)行上下文棧鹿响,又稱調(diào)用棧。
示例:
var a = 2
function add(b,c) {
return b+c
}
function addAll(b,c) {
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
-
第一步谷饿,創(chuàng)建全局上下文惶我,并將其壓入棧低
全局執(zhí)行上下文壓入到調(diào)用棧后,JavaScript 引擎便開始執(zhí)行全局代碼了博投。首先會執(zhí)行 a=2 的賦值操作绸贡,執(zhí)行該語句會將全局上下文變量環(huán)境中 a 的值設(shè)置為 2。
-
第二步毅哗,調(diào)用addAll函數(shù)听怕。當(dāng)調(diào)用該函數(shù)時,js引擎會編譯該函數(shù)虑绵,并為其創(chuàng)建一個執(zhí)行上下文尿瞭,最后將該函數(shù)的執(zhí)行上下文壓入棧中
addAll 函數(shù)的執(zhí)行上下文創(chuàng)建好之后,便進(jìn)入了函數(shù)代碼的執(zhí)行階段了翅睛,這里先執(zhí)行的是 d=10 的賦值操作声搁,執(zhí)行語句會將 addAll 函數(shù)執(zhí)行上下文中的 d 由 undefined 變成了 10黑竞。
-
第三步,當(dāng)執(zhí)行到 add 函數(shù)調(diào)用語句時疏旨,同樣會為其創(chuàng)建執(zhí)行上下文很魂,并將其壓入調(diào)用棧
-
第四步,當(dāng)add函數(shù)返回檐涝,該函數(shù)的執(zhí)行上下文會從棧頂彈出遏匆,并將result的值設(shè)置為add的返回值
-
addAll執(zhí)行完,其執(zhí)行上下文也會從棧頂彈出谁榜,此時只剩下全局上下文
整個js流程執(zhí)行結(jié)束
二幅聘、js如何支持塊級作用域
作用域:全局作用域、函數(shù)作用域惰爬、塊級作用域
ES6通過let/const關(guān)鍵字可以實(shí)現(xiàn)塊級作用域喊暖,在編譯階段,js引擎不會把塊級作用域中的變量存放到變量環(huán)境中撕瞧,而是存放到詞法環(huán)境中陵叽,塊級作用域是通過詞法環(huán)境的棧結(jié)構(gòu)來實(shí)現(xiàn)的。
示例:
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
- 第一步:編譯并創(chuàng)建執(zhí)行上下文
var聲明的變量在變量環(huán)境
let/const聲明的變量在詞法環(huán)境
在作用域塊內(nèi)部丛版,let/const聲明的變量不在詞法環(huán)境中 - 繼續(xù)執(zhí)行代碼巩掺,并賦值,當(dāng)進(jìn)入作用域塊中页畦,let/const聲明的變量會被存放在詞法環(huán)境的一個單獨(dú)區(qū)域中胖替。
在詞法環(huán)境內(nèi)部,維護(hù)了一個小型棧結(jié)構(gòu)豫缨,棧底是函數(shù)最外層的變量独令,進(jìn)入一個作用域塊后,就會把該作用域塊內(nèi)部的變量壓到棧頂好芭;當(dāng)作用域執(zhí)行完成之后燃箭,該作用域的信息就會從棧頂彈出。
塊級作用域就是通過詞法環(huán)境的棧結(jié)構(gòu)來實(shí)現(xiàn)的舍败,而變量提升是通過變量環(huán)境來實(shí)現(xiàn)招狸,通過這兩者的結(jié)合,JavaScript 引擎也就同時支持了變量提升和塊級作用域了邻薯。
注: - var的創(chuàng)建和初始化被提升裙戏,賦值不會被提升
- let/const的創(chuàng)建被提升,初始化和賦值不會被提升
- function的創(chuàng)建、初始化和賦值均被提升
三厕诡、作用域鏈和閉包
每個執(zhí)行上下文的變量環(huán)境中都包含了一個外部引用outer累榜,用來指向外部的執(zhí)行上下文。
當(dāng)一段代碼使用一個變量時灵嫌,js引擎會先在“當(dāng)前的執(zhí)行上下文”中查找信柿,如果在當(dāng)前的變量環(huán)境中沒有找到冀偶,js引擎會繼續(xù)在outer所指向的執(zhí)行上下文中查找,這個查找的鏈條就稱為作用域鏈渔嚷。作用域是由代碼中函數(shù)聲明的位置來決定的
閉包:在 JavaScript 中,內(nèi)部函數(shù)總是可以訪問其外部函數(shù)中聲明的變量稠曼,當(dāng)通過調(diào)用一個外部函數(shù)返回一個內(nèi)部函數(shù)后形病,即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了,但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中霞幅,我們就把這些變量的集合稱為閉包漠吻。比如外部函數(shù)是 foo,那么這些變量的集合就稱為 foo 函數(shù)的閉包司恳。
閉包還可以這樣理解:當(dāng)函數(shù)嵌套時途乃,內(nèi)層函數(shù)引用了外層函數(shù)作用域下的變量,并且內(nèi)層函數(shù)在全局作用域下可訪問時扔傅,就形成了閉包耍共。
四、this
this和作用域鏈屬于兩套不同的系統(tǒng)猎塞。
執(zhí)行上下文中包含了變量環(huán)境试读、詞法環(huán)境、外部環(huán)境outer荠耽,還有一個this
this是和上下文綁定的钩骇,每個執(zhí)行上下文都有一個this。
執(zhí)行上下文分文三種:全局執(zhí)行上下文铝量、函數(shù)執(zhí)行上下文和eval執(zhí)行上下文倘屹,所以this對應(yīng)的也有三種。
- 全局執(zhí)行上下文中的this:指向window對象
-
函數(shù)執(zhí)行上下文中的this:要取決于函數(shù)是如何調(diào)用的
- 函數(shù)作為普通函數(shù)調(diào)用慢叨,在嚴(yán)格模式下纽匙,this 值是 undefined,非嚴(yán)格模式下 this 指向的是全局對象 window插爹;
- 作為對象的方法調(diào)用哄辣,this指向該對象
- 函數(shù)使用call、apply或bind方法調(diào)用赠尾,this由調(diào)用時傳入的參數(shù)決定力穗,指向傳入的參數(shù)(call、apply和bind都是用于指定函數(shù)中的this值的方法)
- 在構(gòu)造函數(shù)中气嫁,this指向即將創(chuàng)建的新對象
- 在事件處理函數(shù)中当窗,this指向觸發(fā)事件的元素。
- 嵌套函數(shù)不會繼承外層的this寸宵,也是根據(jù)函數(shù)時如何調(diào)用來決定的崖面,但是箭頭函數(shù)不會創(chuàng)建其自身的執(zhí)行上下文元咙,箭頭函數(shù)中的this,指向外層非箭頭函數(shù)的this