JavaScript原生系列-預解析、解析器、變量提升、函數提升、作用域、作用域鏈

轉載請註明預見才能遇見的博客:http://my.csdn.net/

原文地址:https://blog.csdn.net/pcaxb/article/details/102396423

JavaScript原生系列-預解析、解析器、變量提升、函數提升、作用域、作用域鏈

目錄

1.預解析概念

2.預解析過程

3.ES5和ES6提升區別

4.什麼是提升、變量提升、函數提升、爲什麼會有提升

1.什麼是提升(Hosting)?

2.變量提升

3.函數提升

 4.爲什麼會有提升

5.函數提升和變量提升的具體情況

1)函數表達式沒有函數提升(提升的是變量),函數聲明的函數纔有函數提升

2)函數和變量同名、變量同名、函數同名

3)函數內部同樣適用於js預解析

6.深入解析過程

1)首先是找到script標籤按照script塊依次解析

2)解析執行環境

3)對標識符( var function)進行解析

7.案例

1.一個經典的案例 

2.代碼塊中提升的案例(ES5沒有塊級作用域,所以此處是在全局作用域中)

8.作用域和作用域鏈

1)作用域:全局作用域和局部作用域

2)作用域鏈

3)函數的執行

4)在ES5中沒有塊級作用域,ES6出現後,增加了塊級作用域

9.全局作用域下帶var和不帶var的區別

10. 預解析中的一些變態機制

1)不管條件是否成立,都要把帶var的進行提前的聲明

2)只預解析“=”左邊的,右邊的是指針,不參與預解析

3)自執行函數:定義和執行一起完成

4)return下的代碼依然會進行預解析

5)名字已經聲明過了,不需要重新的聲明,但是需要重新的賦值


JavaScript是解釋型語言是毋庸置疑的,但它不是自上往下一句一句地解析的。JS的解析過程分爲兩個階段:預編譯期(預處理、預解析、預編譯)與執行期。

1.預解析概念

在當前作用域中,JS代碼執行之前,瀏覽器預先會把一些東西(帶function和var定義的變量)解析到內存中。

2.預解析過程

1)創建一個當前執行環境下的活動對象(Window)

2)將var聲明的變量設置爲活動對象的屬性(也就是將其添加到活動對象當中),並將其賦值爲undefined

3)將function定義的函數也添加到活動對象當中

在瀏覽器內部,有一塊是專門解析JS數據的,我們可以稱之爲JS解析器。一旦瀏覽器識別到了SCRIPT標籤,JS解析器就開始工作。 JS解析器,分兩步執行:

第一步預解析:找一些東西,找程序中var關鍵字,如果找到了提前給var定義的變量賦值undefined,找程序中的普通函數,如果找到了,函數提升,將整個函數賦值給函數名。

第二步解讀代碼: 逐行解析代碼。按照上下順序。說明:如果碰到函數定義或者變量聲明會忽略。

程序最開始的時候,只對window下的變量和函數進行預解析,只有函數執行的時候纔會對函數中的變量和函數進行預解析。

3.ES5和ES6提升區別

 這個是var關鍵字,注意只有var纔有變量提升,ES6新增了let和const關鍵字,使得js也有了“塊”級作用域,而且使用let和const聲明的變量和函數是不存在提升現象的。let和const也被提升了,只是沒有初始化,所以報錯,看起來就像是沒有被提升一樣。

JS中無論哪種形式聲明(var, let, const, function, function*, class)都會存在提升現象,不同的是,  var,function,function*的聲明會在提升時進行初始化賦值爲 undefined,因此訪問這些變量的時候,不會報 ReferenceError 異常,而使用 let,const,class 聲明的變量,被提升後不會被初始化,這些變量所處的狀態被稱爲“temporal dead zone”,此時如果訪問這些變量會拋出ReferenceError 異常,看上去就像沒被提升一樣。

4.什麼是提升、變量提升、函數提升、爲什麼會有提升

1.什麼是提升(Hosting)?

引擎會在解釋JavaScript代碼之前首先對其進行編譯,編譯過程中的一部分工作就是找到所有的聲明,並用合適的作用域將他們關聯起來,這也正是詞法作用域的核心內容。

簡單說就是在js代碼執行前引擎會先進行預編譯,預編譯期間會將變量聲明與函數聲明提升至其對應作用域的最頂端。

2.變量提升

變量提升即將變量聲明提升到它所在作用域的最開始的部分。

L(a,person);//undefined undefined
var a = 1,person={name:'pca',age:22};
L(a,person);//1 {name:'pca',age:22}

3.函數提升

函數提升即將函數聲明提升到它所在作用域的最開始的部分。

L(f1); // ƒ f1() {L("222")}
L(f2); // undefined  
function f1() {L("111")}   //函數聲明
var f2 = function() {}    //函數表達式
function f1() {L("222")} // 函數提升,整個代碼塊提升到文件的最開始   
L(f1);//ƒ f1() {L("222")}   
L(f2);//ƒ () {}
//分析
function f1() {L("222")} 
var f2;
L(f1);
L(f2); 
f2 = function() {}    
L(f1);  
L(f2);

 4.爲什麼會有提升

 下面是Dmitry Soshnikov早些年的twitter,他也對這個問題十分感興趣, Jeremy Ashkenas想讓Brendan Eich聊聊這個話題:

Brendan Eich給出了答案:

大致的意思就是:由於第一代JS虛擬機中的抽象紕漏導致的,編譯器將變量放到了棧槽內並編入索引,然後在(當前作用域的)入口處將變量名綁定到了棧槽內的變量。(注:這裏提到的抽象是計算機術語,是對內部發生的更加複雜的事情的一種簡化。)

然後,Dmitry Soshnikov又提到了函數提升,他提到了相互遞歸(就是A函數內會調用到B函數,而B函數也會調用到A函數):

Brendan Eich又給出答案:

Brendan Eich很確定的說,函數提升就是爲了解決相互遞歸的問題,大體上可以解決像ML語言這樣自下而上的順序問題。

最後,Brendan Eich還對變量提升和函數提升做了總結:大概是說,變量提升是人爲實現的問題,而函數提升在當初設計時是有目的的。

5.函數提升和變量提升的具體情況

1)函數表達式沒有函數提升(提升的是變量),函數聲明的函數纔有函數提升

// a2(); //a2 is not a function
a2_();
var a2 = function(){L("a2")};
function a2_(){L("a2_")};

2)函數和變量同名、變量同名、函數同名

當變量和函數同名時,函數提升比變量提升優先級高。但是和函數名相同變量的賦值還是會覆蓋函數的。

2-1)函數和變量同名

L(a);//f a(){L(4)}
var a = 1;
L(a);//1
function a(){L(2)};
L(a);//1
var a = 3;
L(a);//3
function a(){L(4)};
L(a);//3
//分析
function a(){L(4)};
var a;//忽略
L(a);
a = 1;
L(a);
L(a);
a = 3;
L(a);
L(a);

2-2)函數同名

同名函數,後邊覆蓋前面

function a(){L(1)}
L(a)
function a(){L(2)}
L(a)
a()
//解析
function a(){L(1)}
function a(){L(2)}
L(a)
L(a)
a()

2-3)變量同名

同名變量,聲明會被提升,後邊會忽略

L(a)
var a = 1
L(a)
var a = 2
L(a)
//分析
var a;
var a; //忽略
L(a) // undfined
a = 1
L(a) //1
a = 2
L(a)//2

3)函數內部同樣適用於js預解析

function t1(age) {
    L(age);
    var age = 27;
    L(age);
    function age() {}
    L(age);
}
t1(3);//ƒ age() {}  27  27
//分析
function t1(age) {
    function age() {}
    var age;//忽略

    L(age);
    age = 27;
    L(age);

    L(age);
}

6.深入解析過程

1)首先是找到script標籤按照script塊依次解析

首先是找到<script>標籤按照<script>塊依次解析,JS預解析不會跨<script>塊去進行預解析。

<script>
    // alert(msg);//msg is not defined
    // test();//test is not defined
</script>
<script>
    var msg ="test";
    function test(){
        alert("this is function");
    }
</script>

如果把兩個script塊調換一下位置,結果就不一樣了。

<script>
    var msg ="test";
    function test(){
        alert("this is function");
    }
</script>
<script>
    alert(msg);//test
    test();//this is function
</script>

按照script塊順序進行預解析,當第一個script塊預解析完,會解析到var msg 和function test,當再第二個script塊中調用alert(msg);和test();時上面的js程序已經執行完畢了,自然會彈出 test 和this is function。

2)解析執行環境

function test(){
    var msg ='This is test';
}
alert(msg); // 報錯msg未定義 (作用域不同,解析執行環境不同)

3)對標識符( var function)進行解析

 

如果一個文檔流中包含多個script代碼段(用script標籤分隔的js代碼或引入的js文件),它們的運行順序是:

步驟1. 讀入第一個代碼段(js執行引擎並非一行一行地執行程序,而是一段一段地分析執行的)

步驟2. 做語法分析,有錯則報語法錯誤(比如括號不匹配等),並跳轉到步驟5

步驟3. 對var變量和function定義做“預解析”(永遠不會報錯的,因爲只解析正確的聲明)

步驟4. 執行代碼段,有錯則報錯(比如變量未定義)

步驟5. 如果還有下一個代碼段,則讀入下一個代碼段,重複步驟2

步驟6. 結束

 

7.案例

1.一個經典的案例 

我們知道函數一執行完是會被垃圾回收機制銷燬的。
但瞭解閉包的朋友會相信,內存暫用這一說法的
其實 函數 return 直接返回的那個,其實是一個結果或者是值,是不需要預解釋的。
說了這麼多看代碼:
var n = 99;

function outer(){
    var n = 0;
    return function inner(){
    return n++; // 注意不是++n
    }
}
var c = outer();  // c=function inner(){ return n++; }
var num1 = c();  // 0,然後再執行n++ 此時n=1;
var num2 = c();  // 1, n++ 2;
var d = outer(); //重新開闢新
var num3 = d();  //0

當我們的一個函數返回一個新的function,我們在外面定義一個變量來接收,這樣這個函數
的內存就不能在執行完成後自動銷燬,也就是我們所謂的函數內存被佔用了。
變量的值要看它在哪定義,this,要看它在哪調用的。

2.代碼塊中提升的案例(ES5沒有塊級作用域,所以此處是在全局作用域中)

var a = 1;
if(true){
    L(a);//1
    var a = 2;
    L(a);//2
}
L(a);//2
if(false) {//
    // var mark1 = 1;//Identifier 'mark1' has already been declared	標識符“mark1”已經聲明
     function mark1(){
        L("exec mark1");//爲true時會輸出exec mark1
     }
    // var mark1;//Identifier 'mark1' has already been declared
    L(mark1);//爲true時直接輸出整個函數
 }
 L(mark1);//爲false時輸出  undefined ;爲true時直接輸出整個函數
 mark1();//if爲true時,就會輸出exec mark1,爲false是 mark1 is not a function

 

8.作用域和作用域鏈

在ES5的時候,只存在兩種作用域:函數作用域和全局作用域;ES6出現後,增加了塊級作用域

1)作用域:全局作用域和局部作用域

函數裏面的作用域成爲局部作用域,window所在的作用域稱爲全局作用域;在全局作用域下聲明的變量是全局變量;在“局部作用域中聲明的變量”和“函數的形參”都是局部變量;

var a = 0;//全局作用域
func();
function func(){
  var b = 1;//局部作用域(函數作用域)
  L(a); //0 函數作用域中訪問全局變量
  L(b); //1
}
L(b); //報錯 全局作用域中訪問func函數作用域中的局部變量

L(a);先到func中找a,找不到然後到上一層作用域中去找a,形成了作用域鏈。

2)作用域鏈

在局部作用域中,代碼執行的時候,遇到了一個變量,首先需要確定它是否爲局部變量,如果是局部變量,那麼和外面的任何東西都沒有關係,如果不是局部的,則往當前作用域的上級作用域進行查找,如果上級作用域也沒有則繼續查找,一直查找到window爲止,這就是作用域鏈

var i = 1;
function fn1(){
    var i = 5;
    var j = 20
    function fn2()
    {
      var i = 10;
      function fn3()
      {
        var j = 15;
        console.log(i); //10
      }
      fn3();
      console.log(i); //10
      console.log(j); //20
    }
    fn2();
}
fn1();
console.log(i); //1

3)函數的執行

當函數執行的時候,首先會形成一個新的局部作用域,然後按照如下的步驟執行:

第一步:如果有形參,先給形參賦值;

第二部:進行局部作用域中的預解析;

第三部:局部作用域中的代碼從上到下執行

函數形成一個新的私有的作用域,保護了裏面的私有變量不受外界的干擾(外面修改不了私有的,私有的也修改不了外面的),這也就是閉包的概念。

4)在ES5中沒有塊級作用域,ES6出現後,增加了塊級作用域

var a = 0;
if(a < 10)
{
    a++;
    var b = a;
}
console.log(b); //1  b是全局變量。處於全局作用域,會成爲全局對象window對象的屬性

理解:以上代碼,雖然b是在if代碼塊中定義的,但由於ES5只有全局作用域和函數作用域,沒有塊級作用域,而b變量不是在函數中定義的,所以b只能是全局變量。

let a = 0; //注意:使用'let聲明的全局變量不會成爲window對象的屬性
if(a < 10)
{
    a++;
    let b = a;
}
console.log(b); //報錯 b是if代碼塊中的變量,只在'if'代碼塊{}中生效。處於塊級作用域。

ES6中{ }會形成一個塊級作用域,所以上面代碼的b處於if這個塊作用域中,不屬於全局作用域。

 

9.全局作用域下帶var和不帶var的區別

在全局作用域中聲明變量帶var可以進行預解析,所以在賦值的前面執行不會報錯;聲明變量的時候不帶var的時候,不能進行預解析,所以在賦值的前面執行會報錯。

//正確,有變量提升
L(a);
var a = 1;

//錯誤,沒有變量提升
L(b);//b is not defined
b = 2;

b = 2; 相當於給window增加了一個b的屬性名,屬性值是2;var a = 1; 相當於給全局作用域增加了一個全局變量a,但是不僅如此,它也相當於給window增加了一個屬性名a,屬性值是1;

function fn() {
    // L(total); // Uncaught ReferenceError: total is not defined
    total = 100;
}
fn();
L(total);//100

10. 預解析中的一些變態機制

1)不管條件是否成立,都要把帶var的進行提前的聲明

if (!('num' in window)) { 
    var num = 12;
}
console.log(num); // undefined

JavaScript進行預解析的時候,會忽略所有if條件,因爲在ES6之前並沒有塊級作用域的概念。本例中會先將num預解析,而預解析會將該變量添加到window中,作爲window的一個屬性。那麼 'num' in window 就返回true,取反之後爲false,這時代碼執行不會進入if塊裏面,num也就沒有被賦值,最後console.log(num)輸出爲undefined。

2)只預解析“=”左邊的,右邊的是指針,不參與預解析

//1
fn(); // -> undefined();  // Uncaught TypeError: fn is not a function
var fn = function () {
    console.log('ok');
}

//2
fn(); -> 'ok'
function fn() {
    console.log('ok');
}
fn(); -> 'ok'

3)自執行函數:定義和執行一起完成

(function (num) {
    console.log(num);
})(100);

自執行函數定義的那個function在全局作用域下不進行預解析,當代碼執行到這個位置的時候,定義和執行一起完成了。

4)return下的代碼依然會進行預解析

//return 後面的函數也是會變量提升的
var a = 1;
function foo() {
    a = 10;
    L(a);//10
    return;
    function a() {};
}
foo();
L(a);//1
//解析
var a;
a = 1;
function foo() {
    function a() {};
    a = 10;
    L(a);
    return;
}
foo();
L(a);

函數體中return後面的代碼,雖然不再執行了,但是需要進行預解析,return中的代碼,都是我們的返回值,所以不進行預解析。

5)名字已經聲明過了,不需要重新的聲明,但是需要重新的賦值

var fn = 13;                                       
function fn() {                                    
    console.log('ok');                               
}                                                  
fn(); // Uncaught TypeError: fn is not a function
//解析
function fn() {                                    
    console.log('ok');                               
} 
var fn;//忽略
fn = 13;
fn();
fn(); //2                                            
function fn() {console.log(1);}                         
fn(); //2                                            
var fn = 10;                   
fn(); // Uncaught TypeError: fn is not a function                          
function fn() {console.log(2);}                  
fn();//不執行
//分析
function fn() {console.log(1);} 
function fn() {console.log(2);} 
var fn;//忽略
fn();                                                                    
fn();                                         
fn = 10;                  
fn();                                   
fn();

參考資料:https://blog.csdn.net/bingo_wangbingxin/article/details/79159015

參考資料:https://www.cnblogs.com/shaohua007/p/7587657.html

 

JavaScript原生系列-預解析、解析器、變量提升、函數提升、作用域、作用域鏈

博客地址:https://blog.csdn.net/pcaxb/article/details/102396423

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章