JS中的變量提高機制

在當前執行上下文中(不管是全局,仍是函數執行私有的),JS代碼自上而下執行以前,首先會默認把全部帶VARFUNCTION關鍵字的進行聲明或者定義。javascript

  • VAR的只是提早聲明
  • FUNCTION的會提早聲明+定義

思惟導圖

1、先簡單瞭解兩個語法

var n = 100爲例,咱們以前講過的操做步驟應該是:java

    1. 建立值100存儲到棧內存中(引用數據類型首先建立堆,存完數據後,把堆的地址存放到棧中)
    1. 建立一個變量 var a;
    1. 讓變量和值關聯在一塊兒

這是咱們以前說過的建立變量並賦值的過程;面試

  • 然而在這個過程當中,咱們所謂的建立變量的專業描述叫作=>變量聲明(declare)
  • 讓變量和值關聯在一塊兒的專業描述叫作 =>變量定義(defined)

上面咱們所說的是咱們常規的建立變量並賦值的操做。瀏覽器

那當咱們只聲明變量時,也就是var a;可是沒有給變量賦值(也就是未定義),因此默認值是undefined未定義 (這也是undefined的由來)bash

2、理解變量提高的含義

爲了更好的理解變量提高,咱們仍是得先從瀏覽器的機制提及:函數

一、瀏覽器運行機制

  • 一、爲了可以讓代碼執行,瀏覽器首先會造成一個執行環境棧ECStack(Execution Context Stack)
    • 棧內存的做用:
      • 1.代碼執行
      • 2.基本數據值存儲在棧內存中
    • 堆內存做用:(本次咱們用不到,只是提到棧內存時,天然提一下堆內存)
      • 1.存儲引用數據類型的值

    這裏咱們不在過多講解堆棧內存,前面已經專門講過。ui

有了棧內存代碼就能夠自上而下的執行了,只不過剛開始,是要把全局下的代碼先執行;spa

  • 二、開始執行全局下的代碼,就會造成一個全局執行上下文EC(GLOBAL 簡寫 爲G)(上下文就是上文下文加一塊兒就是環境的意思);code

    • 至關於在棧內存中造成了一個小空間,用來執行全局下的代碼;
    • 之後咱們的函數執行也會造成一個這樣的小空間,用來執行函數中的代碼,這個咱們一會說;

    一、2 兩步總體原理:咱們的目的是爲了執行代碼,因此圍繞代碼執行引起的下列操做:cdn

    • ==> 瀏覽器加載頁面,想讓代碼執行,首先會造成一個棧內存(執行環境棧 ECStack);而後開始讓代碼準備執行;
    • ==> 最開始要執行的必定是全局下的代碼,
    • ==> 此時造成一個全局代碼的執行環境(全局執行上下文 EC(G))
    • ==> 造成以後,把EC(G)壓縮到棧內存中去執行(進棧);每個函數的執行也是這樣操做的...
    • ==> 有些上下文在代碼執行完成後,會從棧內存中移除去(出棧),可是有些狀況是不能移出去的(例如:全局上下文就不能移除...);
    • ==> 在下一次有新的執行上下文進棧的時候,會把以前沒有移除的都放到棧內存的底部,讓最新要執行的在頂部執行
  • 三、對應代碼會在本身所屬的執行上下文中執行,而這個環境中有一個存放變量的地方:變量對象(VO/AO)

    • 全局下存儲變量的地方叫作:全局變量對象VO(G) ->全稱Variable Object (Global)
      • 所謂的變量對象,就是專門爲了存儲當前環境下要建立的變量的地方

在咱們以前的理解,建立完一系列的環境後,就能夠執行代碼了,然而並無,在代碼執行以前還有一系列的操做要作;例如:詞法解析 => 形參賦值 => 變量提高...等好多好多事情;詞法解析和形參賦值咱們後面會說,如今咱們先來理解:變量提高

二、變量提高

變量提高:就是在當前上下文中,JS代碼執行以前要作的事情;首先會默認把全部帶VARFUNCTION關鍵字的進行聲明或者定義。

  • VAR的只是提早聲明
  • FUNCTION的會提早聲明+定義

咱們如下題爲例

console.log(a);
console.log(func);
var a = 10;
function func(){
    console.log(b);
    var b=20;
    console.log(b);
}
func();
console.log(a);
console.log(func); 
複製代碼
  • 第一步:找到帶VAR和先聲明,找到帶FUNCTION的聲明+定義
    • 一、var a ;
    • 二、function func(){...};(按照函數建立的步驟,即聲明又定義)
      • 1).建立堆內存,把函數體內的代碼看成字符串存儲進堆內存中;
      • 2).把堆內存的十六進制地址存進棧內存;
      • 3).建立變量func與十六進制地址關聯;
  • 第二步:代碼開始自上而下執行
    • 一、conlose.log(a):=> 此時在當前執行上下文中,已經聲明過a可是並無賦值,因此打印結果爲undefined
    • 二、console.log(func):=> 此時當前上下文中已經聲明瞭func變量,而且已經賦值,因此打印func的結果爲f func(){...}
    • 三、var a = 10:=> 以前咱們在變量提高階段,已經完成了var a操做(瀏覽器是又懶惰性的,作完了這件事,不會在作第二遍了),因此此時只建立了一個值10,沒有在建立變量a;把10與以前在變量提高階段聲明的變量a關聯;
    • 四、function func(){...}:=> 上面咱們說了,同一件事情瀏覽器不會作兩便;很明顯以前在變量提高階段已經作過這件事了,因此此步就會跳過;直接進行下一步;
    • 五、func():=> 讓函數執行;
      • 目的:也是執行以前存儲到堆內存中的代碼,既然是要執行代碼;就要造成一個執行上下文;這個上下文就叫作私有上下文EC(func隨便起的名字)
      • 1).瀏覽器處理過程:
        • 造成後也一樣要壓縮進入執行環境棧中執行,
        • 此時,執行環境棧中已有的全局執行上下文就會向下壓縮(給函數的執行上下文騰出執行空間)
        • 私有上下文中存儲變量的地方叫作:私有變量對象AO(func1隨便起的名字) ->全稱Active Object;
      • 2).函數執行過程:
        • 第一步:變量提高:找到帶VAR和先聲明,找到帶FUNCTION的聲明+定義
          • var b
          • 沒有帶function
        • 第二步:函數執行
          • console.log(b):=> 此時b以聲明爲定義,因此是undefined;
          • var b=20:=> 同全局同樣,跳過var b,直接建立值,並與之聲明的b關聯;
          • console.log(b):=> 此時,b已經被賦值,因此結果爲20;
          • 函數執行完成
      • 3).瀏覽器處理:
        • 函數執行完成,出棧,
        • 執行環境棧中空間被釋放(之後再講,這裏先這樣寫),
        • 以前被壓縮到下面的全局執行上下文又回到原來的位置;
        • 繼續執行全局下的代碼;
    • 六、console.log(a):=> 此時a的值爲10;
    • 七、console.log(func):=> 此時func依然指向堆內存所對應的十六進制地址,因此打印結果爲f func(){...};

3、避免變量提高在函數執行時的不嚴謹性(不是重點簡單介紹)

上面咱們簡單的理解了變量提高的含義;

  • 那麼咱們思考一下這個題,是否會報錯呢?
func();
function func(){
    console.log('OK');
}
+ 答案是不會報錯
複製代碼
func();
var func = function (){
    console.log('OK');
}

+ 答案是會報錯:
    +  變量提高階段:var func; 只定義不賦值 (默認值是undefined)
    + //=> undefined() =>Uncaught TypeError: func is not a function 
複製代碼

對比上面兩題的區別,咱們得出結論:

  • 函數表達式方式建立函數,在變量提高階段,並不會給函數賦值,這樣只有代碼執行的過程當中,函數賦值後才能夠執行(不能在賦值以前執行) =>符合嚴謹的執行邏輯 =>真實項目中推薦的方式
  • 爲了保證語法的規範性,JS中本來建立的匿名函數,咱們最好設置一個名字,可是這個名字在函數外面仍是沒法使用的,只能在本函數體中使用(通常也不用) 例如:
var func = function anonymous() {
	// console.log(anonymous); //=>當前這個函數
};
// console.log(anonymous); //=>Uncaught ReferenceError: anonymous is not defined
func();

//================================================

var func = function func() {
	console.log(func); //=>函數名func
};
console.log(func); //=>變量func,存儲的值是函數
func();
複製代碼

4、變量提高在條件判斷下的處理

截止目前(注意是目前),咱們的上下文只有兩種:

  • => 全局上下文
  • => 函數執行產生的私有上下文

一、全局上下文中帶VAR

/* * 全局上下文中的變量提高: * [VO(G) 全局變量對象] * var n; 不論IF條件是否成立都進行(只要不在函數裏面,帶VAR的都要變量提高) * 不管是IF仍是FOR中的VAR都進行提高; */
console.log(n); //=>undefined
if (1 > 1) { //=>條件不成立
	var n = 100;
}
console.log(n); //=>undefined
複製代碼

二、全局上下文中帶FUNCTION

-1)[IE 10及之前以及谷歌等瀏覽器低版本狀態下]:function func(){ ... } 聲明+定義都處理了

-2)[最新版本的瀏覽器中,機制變了]:function func; 用判斷或者循環等包裹起來的函數,在變量提高階段,不論條件是否成立,只會先聲明

/* * 全局上下文中的變量提高: * [VO(G) 全局變量對象] * [IE 10及之前以及谷歌等瀏覽器低版本狀態下] * function func(){ ... } 聲明+定義都處理了 * * [最新版本的瀏覽器中,機制變了] * function func; 用判斷或者循環等包裹起來的函數,在變量提高階段,不論條件是否成立,此處只會先聲明 */
console.log(func); //=>undefined
if (1 === 1) {
	// 此時條件成立,進來的第一件事情仍是先把函數定義了(迎合ES6中的塊做用域) => func=function(){ .... }
	console.log(func); //=>函數
	function func() {
		console.log('OK');
	}
	console.log(func); //=>函數
}
console.log(func); //=>函數
複製代碼

一、在當前執行上下文中,無論條件是否成立,變量提高是有效的

二、[IE 10及之前以及谷歌等瀏覽器低版本狀態下]:function func(){ ... } 聲明+定義都處理了

三、[最新版本的瀏覽器中,機制變了]:function func; 用判斷或者循環等包裹起來的函數,在變量提高階段,不論條件是否成立,只會先聲明 重點在寫一遍強調:

5、變量提高的兩道經典面試題

一、寫出結果

// 瀏覽器有一個特徵:作過的事情不會從新再作第二遍(例如:不會重複聲明)
/* * 全局上下文中的變量提高 * fn = function(){ 1 } 聲明+定義 * = function(){ 2 } * var fn; 聲明這一步不處理了(已經聲明過了) * = function(){ 4 } * = function(){ 5 } * 結果:聲明一個全局變量fn,賦的值是 function(){ 5 } */
fn(); //=>5
function fn(){ console.log(1); }  //=>跳過(變量提高的時候搞過了)
fn(); //=>5
function fn(){ console.log(2); }  //=>跳過
fn(); //=>5
var fn = function(){ console.log(3); } //=>var fn; 這一步跳過,可是賦值這個操做在變量提高階段沒有搞過,須要執行一次 => fn = function(){ 3 }
fn(); //=>3
function fn(){ console.log(4); } //=>跳過
fn(); //=>3
function fn(){ console.log(5); } //=>跳過
fn(); //=>3
複製代碼

二、分別寫出在低版本瀏覽器和高版本瀏覽器下的輸出結果

低版本

/* * 低版本瀏覽器(包含IE10及之內) * 全局上下文中變量提高 * 沒有 */
f=function (){return true;};//給GO中設置一個屬性 f = function () {return true;}
g=function (){return false;};//給GO中設置一個屬性 g = function () {return false;}
(function () {
	/* * 自執行函數執行,造成一個私有的執行上下文 * [變量提高] * function g(){return true;} */
	// 條件解析:
	// g() => 私有的G執行 TRUE
	// []==![] => []==false => 0==0 => TRUE
    if (g() && [] == ![]) { //=>條件成立
        f = function () {return false;} //f不是本身私有的,則向上查找,屬於全局對象中的f,此處是把全局對象中的 f = function () {return false;}
        function g() {return true;} //跳過(變量提高處理過了)
    }
})();
console.log(f()); //=>FALSE
console.log(g()); //=>FALSE 這個G找全局的(函數裏面的G是本身私有的)
複製代碼

高版本

/* * 高版本瀏覽器 * 全局上下文中變量提高:沒有 */
f=function (){return true;};//給GO中設置一個屬性 f = function () {return true;}
g=function (){return false;};//給GO中設置一個屬性 g = function () {return false;}
(function () {
	/* * 自執行函數執行,造成一個私有的執行上下文 * [變量提高] * function g; 高版本瀏覽器中,在判斷和循環中的函數,變量提高階段只聲明不定義 */
	// 條件解析:
	// g() => undefined() => Uncaught TypeError: g is not a function 下面操做都不在執行了
    if (g() && [] == ![]) {
        f = function () {return false;} 
        function g() {return true;}
    }
})();
console.log(f());
console.log(g());
複製代碼
相關文章
相關標籤/搜索