js的預解析


在ES6之前,變量使用var聲明,會存在變量的預解析(函數也有預解析)。ES6引了letconst,但是現階段ES6並沒有完全普及,而且很多比較老的代碼都還是按照ES5的標准甚至是ES3的標准來書寫的,要想得心應手的維護之前的代碼個人感覺還是很有必要了解下js的預解析機制。

一、變量和函數在內存中的存在形式


JavaScript中的變量類型和其他語言一樣,有基本數據類型和引用數據類型。基本數據類型包括:undefinednullbooleanStringNumber;引用數據類型主要是對象(包括{}、[]、/^$/、Date、Function等)。

var num = 24; var obj = {name:'iceman' , age:24}; function func() { console.log('hello world'); }

以上的代碼在內存中的模型為:


內存模型.png
  • 基本數據類型按照值來操作,引用數據類型按照地址來操作。
  • 基本類型在棧區:
  • 引用數據類型:

     

基本類型是直接存儲在棧內存中,而對象是存儲在堆內存中,變量只是持有該對象的地址。所以obj持有一個對象的地址oxff44,函數func持有一個地址oxff66。

在以上的代碼的基礎上再執行:

console.log(func); console.log(func());

第一行輸出的是整個函數的定義部分(函數本身):


 

上面已經說明了,func存儲的是一個地址,該地址指向一塊堆內存,該堆內存就保留了函數的定義。

第二行輸出的是func函數的返回結果:


由於func函數沒有返回值,所以輸出undefined。
注意:函數的返回結果,return后面寫的是什么,返回值就是什么,如果沒有return,默認返回值是undefined。

二、預解析


有了以上的內存模型的理解之后,就能更好的了解預解析的機制了。所謂的預解析就是:在當前作用域中,JavaScript代碼執行之前,瀏覽器首先會默認的把所有帶var和function聲明的變量進行提前的聲明或者定義。

2.1. 聲明和定義

var num = 24;

這行簡單的代碼其實是兩個步驟:聲明和定義。

  • 聲明:var num; 告訴瀏覽器在全局作用域中有一個num變量了,如果一個變量只是聲明了,但是沒有賦值,默認值是undefined。
  • 定義:num = 12; 定義就是給變量進行賦值。

2.2. var聲明的變量和function聲明的函數在預解析的區別

var聲明的變量和function聲明的函數在預解析的時候有區別,var聲明的變量在預解析的時候只是提前的聲明,function聲明的函數在預解析的時候會提前聲明並且會同時定義。也就是說var聲明的變量和function聲明的函數的區別是在聲明的同時有沒同時進行定義。

2.3. 預解析只發生在當前的作用域下

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

console.log(num); var num = 24; console.log(num); func(100 , 200); function func(num1 , num2) { var total = num1 + num2; console.log(total); }

打印結果:

 

第一次輸出num的時候,由於預解析的原因,只聲明了還沒有定義,所以會輸出undefined;第二次輸出num的時候,已經定義了,所以輸出24。

由於函數的聲明和定義是同時進行的,所以func()雖然是在func函數定義聲明處之前調用的,但是依然可以正常的調用,會正常輸出300。

三、 作用域鏈


先理解以下三個概念:

  • 函數里面的作用域成為私有作用域,window所在的作用域稱為全局作用域;
  • 在全局作用域下聲明的變量是全局變量;
  • 在“私有作用域中聲明的變量”和“函數的形參”都是私有變量;

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

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

  1. 如果有形參,先給形參賦值;
  2. 進行私有作用域中的預解析;
  3. 私有作用域中的代碼從上到下執行

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

console.log(total); var total = 0; function func(num1, num2) { console.log(total); var total = num1 + num2; console.log(total); } func(100 , 200); console.log(total);

以上代碼執行的時候,第一次輸出total的時候會輸出undefined(因為預解析),當執行func(100,200)的時候,會執行函數體里的內容,此時func函數會形成一個新的私有作用域,按照之前描述的步驟:

  • 先給形參num1、num2賦值,分別為100、200;
  • func中的代碼進行預解析;
  • 執行func中的代碼

因為在func函數內進行了預解析,所以func函數里面的total變量會被預解析,在函數內第一次輸出total的時候,會輸出undefined,接着為total賦值了,第二次輸出total的時候就輸出300。 因為函數體內有var聲明的變量total,函數體內的輸出total並不是全局作用域中的total。
最后一次輸出total的時候,輸出0,這里輸出的是全局作用域中的total。

console.log(total); var total = 0; function func(num1, num2) { console.log(total); total = num1 + num2; console.log(total); } func(100 , 200); console.log(total);

將代碼作小小的變形之后,func函數體內的total並沒有使用var聲明,所以total不是私有的,會到全局作用域中尋找total,也就說說這里出現的所有total其實都是全局作用域下的。

四、 全局作用域下帶var和不帶var的區別


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

console.log(num1); var num1 = 12; console.log(num2); num2 = 12;
輸出結果:

  • num2 = 12; 相當於給window增加了一個num2的屬性名,屬性值是12;
  • var num1 = 12; 相當於給全局作用域增加了一個全局變量num1,但是不僅如此,它也相當於給window增加了一個屬性名num,屬性值是12;

問題:在私有作用域中出現一個變量,不是私有的,則往上級作用域進行查找,上級沒有則繼續向上查找,一直找到window為止,如果window也沒有呢?

  • 獲取值:console.log(total); --> 報錯 Uncaught ReferenceError: total is not defined
  • 設置值:total= 100; --> 相當於給window增加了一個屬性名total,屬性值是100
function fn() { // console.log(total); // Uncaught ReferenceError: total is not defined total = 100; } fn(); console.log(total);

注意:JS中,如果在不進行任何特殊處理的情況下,上面的代碼報錯,下面的代碼都不再執行了

五、 預解析中的一些變態機制


5.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。

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

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

建議:聲明變量的時候盡量使用var fn = ...的方式。

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

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

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

補充:其他定義自執行函數的方式

~ function (num) {}(100) + function (num) {}(100) - function (num) {}(100) ! function (num) {}(100)

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

function fn() { console.log(num); // -> undefined return function () { }; var num = 100; } fn();

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

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

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

經典題目

fn(); // -> 2                                            
function fn() {console.log(1);}  (預解析1)                           
fn(); // -> 2                                            
var fn = 10; // -> fn = 10          (預解析2)                      
fn(); // -> 10()  Uncaught TypeError: fn is not a function                          
function fn() {console.log(2);}        (預解析2)                   
fn();//不執行
//分析以上代碼:
1、首先 預解析的時候(預解析1)
fn作為函數聲明定義->(預解析2)fn前面已經聲明所以此次不再聲明->
(預解析3)fn作為函數重新定義賦值地址(因為前面已經聲明所以直接定義)。

2、然后 執行的時候前面兩個fn()直接調用重新賦值的
function fn() {console.log(2);}
當遇到var fn= 10;時再次對fn進行賦值此時fn為Number類型,所以執行下一條語句的時候就會報錯fn不是一個函數,此時代碼停止運行,后面代碼不再運行。

本文通過鏈接:http://www.jianshu.com/p/c3276ff58c93,按照個人理解進行了修改編輯,補充了部分沒有解釋清楚的地方,如果不對之處,望不吝賜教


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM