最近在學習過程當中,總感受書本上 對於javascript 的一些知識點講的比較亂,想要看一些視頻教程來深刻理解一下,又發現大多數只是流於表面,閱讀一些博客,又感受在關鍵部分彷佛總被簡單的幾句話帶過,彷佛在搪塞讀者, 因而決定趁週末靜下心來好好捋一下這部份內容。javascript
javascript運行過程、預編譯、執行期上下文、做用域、做用域鏈、當即執行函數、閉包 以上的知識點就像一個有方向的圓環,在掌握每個知識點以前彷佛都得先了解其它某個或者某些知識點,讓人不知從哪提及。在捋了半天關係以後,決定按照本文的順序總結。java
javascript 運行分3個步驟:數組
首先,什麼叫預編譯? 講解預編譯以前,首先須要知道 變量提高 和函數提高bash
所謂提高就是指,變量或者函數在聲明以前就使用它,舉個例子:數據結構
<script>
console.log(a)
var a=234;
test();
function test(){
console.log('good');
}
</script>
複製代碼
上面的 變量a 和函數 test 在聲明以前就使用了,可是沒報錯,爲何,由於有預編譯過程,在預編譯過程,將變量和函的聲明進行了提高。 爲何沒有輸出 234 可是輸出了 good ? 注意,提高的時變量的聲明,而不是定義 變量:聲明提高 函數:函數聲明總體提高
應該有人會發現問題,以下代碼閉包
<script>
console.log(a)
var a=234;
function a(){
return a;
}
</script>
複製代碼
輸出的是啥?其實就是涉及到變量和函數哪一個先提高的問題是吧 函數
注意,不要根據結果來判斷 函數後提高,上面的結果不是由於有什麼前後提高的說法,而是由於預編譯過程,要想搞明白上面的原理,繼續往下看: 提高只是預編譯過程當中的兩個現象,要理解預編譯,還須要理解一些東西學習
一、imply global (暗示全局變量):任何變量,若是該變量未經聲明就賦值,則此變量爲全局對象全部,注意,是任何,以下面語句中的 a 和 cui
a = 1;
var b = c = 3;
複製代碼
二、一切聲明的全局變量,均爲window的屬性spa
<script>
function test(){
var a = b = 123;
}
test();
var c = 100;
</script>
複製代碼
b未經聲明就賦值,注意上面說的是任何變量,未經聲明就賦值,即爲全局對象全部,也就是不須要考慮它是在哪裏賦值的,反之 a 是在函數內聲明賦值的,所以它的做用域就是函數內部,在外面訪問不了,因而輸出undefined,而 c自己就是在全局下聲明 的,天然爲全局對象全部。好,前面的理解了,接下來看到預編譯
預編譯發生在函數執行前一刻
一個例子搞懂預編譯四部曲,注意搞懂AO對象內部屬性值的變化:
<script>
function func(a){
console.log(a);
var a=123;
console.log(a);
function a(){};
console.log(a);
var b = function (){};
console.log(b);
function d (){};
}
func(1);
</script>
複製代碼
咱們一步一步進行四部曲:
//第一步:建立AO對象(執行期上下文,先抽象理解成一個對象便可,
存放屬性和屬性值)
AO{
}
複製代碼
//第二步:找形參和變量聲明,並將其做爲AO對象的屬性,
將屬性值設置爲undefined
AO{
a:undefined //形參 ,注意函數內部也有一個 var a 可是注意,
由於a 先是形參,一個對象是不能有兩個同名的屬性的,
因此第一步以後AO對象裏的 a是找的形參a
b:undefined //變量聲明
}
複製代碼
//第三步:將形參值與實參相統一
AO{
a:1 // 實參是1 ,自此以後不用關注a怎麼來的,關注的是 a 值的變化
b:undefined //變量聲明
}
複製代碼
//第四步:在函數體裏找函數聲明(注意是函數聲明,
區分函數聲明和函數表達式的區別),也做爲AO對象的屬性,屬性值即爲函數體
上面的函數體中就只有 a 和 d是函數聲明, b是函數表達式,不是聲明,
AO對象裏已經有屬性 a了,所以只需將其屬性值改爲函數體,
再添加屬性 d,將值賦爲d的函數體,最終AO對象就是下面的狀況
AO{
a:function a(){}
b:undefined
d:function d (){}
}
複製代碼
接下來執行函數 func(1),函數體以下,注意觀察執行過程當中AO對象中屬性值的變化
function func(a){
console.log(a); //此時AO裏的a爲 function a(){},輸出
var a=123; //將AO裏的 a 值改爲 123
console.log(a); //此時 AO裏的 a 值爲123,因此輸出123
function a(){}; //這句不看,由於在預編譯過程已經將它提高了
console.log(a); //此時AO裏的a值仍是 123,因此輸出123
var b = function (){}; //將AO裏的b值改爲function (){}
console.log(b); //此時AO裏的b值爲函數體,所以輸出一個函數體
function d (){}; //這句也不看,由於預編譯過程已經提高了
}
複製代碼
結果以下:
每一個函數在執行時都會產生一個執行期上下文,也就是上面的AO,它是一個對象上面說函數在執行的時候會產生一個獨一無二的執行期上下文,那麼,在整個全局下,每每不僅有函數,還有其它的東西,好比全局變量之類的,因此,在全局下也有一個執行期上下文 ,也是個對象 (global object) 簡稱GO ,本文如下部分全部通常的執行期上下文都簡稱爲AO,全局下的執行期上下文都簡稱爲GO,咱們回到一開始講的例子
<script>
console.log(a)
var a=234;
function a(){
return a;
}
</script>
複製代碼
GO的建立過程跟通常的AO是同樣的,可是有一點區別,那就是既然是在全局下 的執行期上下文,也就不存在形參了,因此只有預編譯四部曲中的第1,2,4步驟,也就是隻有3個步驟
//第一步:建立GO對象
GO{
}
複製代碼
//第二步:尋找變量聲明
GO{
a:undefined
}
複製代碼
//第三步:尋找函數聲明,並將其值賦爲函數體
GO{
a:function a(){ return a; }
}
複製代碼
預編譯完成,而後執行console.log(a) ,此時的a是函數,因此控制檯輸出結果是:
首先在javascript中,咱們說一切都是對象,函數也是對象,並且函數是第一類對象,被稱做一等公民。對象,有屬性,函數也有屬性,有些屬性是咱們能夠訪問的,有些屬性是確實存在可是確不能夠被咱們拿來訪問的,舉個例子
function test(){
}
複製代碼
好比屬性 test.name test.prototype 分別表示函數名和函數原型,這兩個屬性是咱們能夠訪問的,test.[[ scope ]] 屬性就屬於其中一個不能夠被訪問的屬性,scope意爲範圍,[[ scope ]] 這個屬性存放的就是函數的做用域。準確的說存放的是函數的做用域鏈。 那麼什麼是做用域,什麼是做用域鏈呢? 做用域:可訪問變量,對象,函數的集合 做用域鏈:[[ scope ]] 中存放着執行期上下文的集合,這個集合呈鏈式連接,咱們把這種鏈式連接稱爲做用域鏈。 但看概念彷佛仍是有點抽象,首先,從語法上來解讀一下做用域鏈的概念:首先,做用域鏈由不少執行期上下文組成,這些執行期上下文的又不是散亂的擺放,有必定的位置關係:鏈式連接。
這裏又提到了執行期上下文,也就是上面提到的AO,那麼具體什麼是執行期上下文呢? 執行期上下文 :**某個函數或全局代碼的執行環境,該環境中包含執行代碼須要的全部信息。**能夠簡單的認爲它就是一個對象 當函數執行時,會建立一個稱爲 執行期上下文 的對象,一個執行期上下文定義了一個函數執行時的環境,函數每次執行時對應的執行期上下文都是獨一無二的,因此屢次調用一個函數會致使建立多個執行期上下文,當函數執行完畢,它建立的執行期上下文會被銷燬。 下面經過一個例子來理解做用域,做用域鏈,執行期上下文的關係。
能夠看出,真正的執行期上下文比上面講到的 AO其實內部存放的東西是更復雜的,上面的AO只是執行期上下文的一種抽象提取。
<script>
function a(){
function b(){
var b=234;
}
var a=123;
b();
}
var glob=100;
a();
</script>
複製代碼
從全局的角度來解讀上面的代碼:定義了一個函數 a ,聲明瞭並賦值變量glob 。函數a已經定義了,那它就有上面提到的一個函數該有的屬性,此時,a 的 [[ scope ]]屬性是什麼?此時的 a.[[ scope ]] 是一個全局的執行期上下文 GO
而後 a() 執行,產生一個 執行期上下文 aAO, 並放在做用域鏈的頂端,那麼 a 的 做用域鏈爲
a() 要完成執行,必然 b() 要執行完成,b函數建立時
b在執行時產生一個執行期上下文bAO,並將其放在做用域鏈的頂端,那麼b的做用域鏈爲
從上面的流程看,若是用一種數據結構去表示做用域鏈的話,用棧表示比較合適。每次新產生一個執行期上下文,就會放到本身做用域鏈的頂端,釋放的時候也是從做用域鏈自頂向下釋放執行期上下文,即後入先出。
定義:此類函數沒有聲明,在一次執行事後即釋放。適合作初始化工做。 只執行一次,執行以後就被釋放(銷燬)
1.(function (){}());W3C建議
2.(function (){})()
複製代碼
只有表達式才能被執行符號執行,被執行符號執行的函數會自動忽略函數名 由於執行後被釋放了,函數名也訪問不了了,因此直接去掉函數名也沒什麼區別
var test = function (){
...
}()
//函數聲明:
function test(){
console.log('a'');
}
//函數表達式舉例
var x = function test(){
console.log('a'');
}
複製代碼
函數聲明前加個符號->變成表達式,如+ - ! !! && ||等 可是注意若是是邏輯運算符,操做數數量還得符合該運算符要求,如&& ||
! function test(){
console.log('a'');
}
複製代碼
例:
function(a,b,d){
console.log(a+b+c);
}(1,2,3);
複製代碼
系統不會執行,可是也不會報錯 系統如何理解:
function(a,b,d){
console.log(a+b+c);
}
(1,2,3);
//系統將其識別爲正常的分開寫法
複製代碼
閉包是指能夠訪問另外一個函數做用域中變量的函數,建立閉包的最多見的方式就是在一個函數內建立另外一個函數,經過另外一個函數訪問這個函數的局部變量,利用閉包能夠突破做用鏈域,將函數內部的變量和方法傳遞到外部。
1.函數內再嵌套函數 2.內部函數能夠引用外層的參數和變量 3.參數和變量不會被垃圾回收機制回收
function test(){
var arr=[];
for(var i=0;i<10;i++){
arr[i] = function(){
document.write(i+ " ");
}
}
return arr;
}
var maArr=test()
for(var j=0;j<10;j++){
myArr[j]();
}
複製代碼
執行結果是? 10 10 10...10 //總共10個10 代碼解讀: test() 的結果是返回一個數組,數組存放的是10個函數體, 每一個函數體的做用是打印一個值 注意,只是返回函數體,不是返回函數的執行結果 myArrj纔是執行
test執行結束的判斷條件:i==10終止循環,函數執行完畢 返回10個函數體
arr[i] = function(){
document.write(i+ " ");
}
複製代碼
上面的是一條賦值語句 系統在識別的時候只能讀到函數的引用,而無論函數體內部的是什麼,
arr[i] = function(){
}
複製代碼
只有在執行的時候纔會去取i值, 10個函數體在外部執行的時候纔去訪問i,10個函數在執行的時候分別產生一個 執行期上下文 iAO, 也就是本身做用域鏈的頂端,而後訪問i值準備將其打印, 可是本身的AO裏面沒有i變量,因此沿着做用域鏈去找test函數執行時產生的 執行期上下文 AO,也就是你們訪問的都是同一個執行期上下文, 訪問的都是同一個i,也就是10
那如何想要在本身想打印的時候打印出0-9呢? 運用當即執行函數:
function test(){
var arr=[];
for(var i=0;i<10;i++){
(function (j){
arr[j] = function(){
document.write(j+" ")
}
}(i));
}
return arr;
}
var myArr=test()
for(var j=0;j<10;j++){
myArr[j]();
}
複製代碼
怎麼理解: 返回的arr裏面存的是 10個當即執行函數,也就是下面的函數
(function (j){
arr[j] = function(){
document.write(j+" ")
}
}(i));
複製代碼
將i做爲實參傳給形參j ,在i從0-9的循環過程裏,依次發生的是
arr[0] = function(){
}
arr[1] = function(){
}
...
arr[9] = function(){
}
複製代碼
注意,當即執行,執行的是 function (j){} 它裏面的只是一條賦值語句,對arr數組的每個元素賦值,也就是一個函數的引用 這個函數的功能是打印一個值,也就是上面提到的,系統怎麼識別語句的問題,此時打印語句沒有被執行 當函數在外面執行的時候,就是myArrj時,也就是打印函數的執行,此時它們去訪問 j值, 可是它們本身的AO裏沒有 j變量,因而沿着做用域鏈去訪問 j值,也就是當即執行函數執行時產生的AO, 而10個當即執行函數產生的AO是不同的,裏面的j值分別是0-9,因此打印出0-9
閉包會致使多個執行函數共用一個公有變量,若是不是特殊須要,應儘可能防止這種狀況發生。