1. 程式人生 > 實用技巧 >變數提升

變數提升

變數提升

var a=b=15//這種寫法表示 var a=15;b=15 b不會用var進行宣告
let a=b=15//同理,b也不會用let宣告

es5

當棧記憶體或作用域形成,js程式碼開始執行之前,js解析器會把所有帶var關鍵字的變數進行提前宣告,帶function關鍵字的函式進行提前宣告和定義,並且已經宣告,定義過的變數不會進行二次宣告。

var fn=function (){console.log("2")}
function fn(){
    console.log("1")
}
/*最後fn的值為console.log("2") 因為在變數提升階段,首先宣告
這個fn變數(var),此時fn的值為undefined,然後進行fn的函式
宣告以及定義,此時fn的值為console.log("1"),之後開始執行程式碼
,在第一行,fn被賦值為console.log("2"),然後第二行的fn宣告,
由於已經在變數提升階段做過了,所以不會再宣告。所以,fn最終的
值為console.log("2")*/

在函式執行時,js解析器會先開闢一個新的棧記憶體,然後對形參進行賦值(如果有的話),下來進行變數提升,最後執行程式碼。

如果我們不加var進行變數宣告,那麼所有的變數宣告都是對window下的該屬性進行賦值,即使在函式作用域下也是如此。(個人理解:不加var進行的變數宣告實際上是一種省略window物件的語法糖,因為無論從哪個角度來說,他兩個都是等效的。比如通過a=18定義的變數,用a in window這個方法進行檢查,會發現window中存在a這個屬性,並且,修改a或者window的a,另一方也會同時修改)如果我們使用typeof去檢查一個未定義的變數,他會返回undefined。(個人理解:他檢查的其實是window下的該屬性,由於未定義過,所以會輸出undefined)

如果通過var進行變數宣告,在全域性作用域下,他也是對window下該屬性的賦值,和不帶var沒有任何區別。但是,如果在函式作用域下,他表示的則是在當前作用域中新建一個私有變數,與外界無關。同理,用function宣告的函式也是私有函式。通過var宣告的變數,只會對等號左邊進行變數提升*,換句話說,如果用var宣告一個函式,這個函式是不會被賦值給等號左邊的變數的,但是等號左邊的變數還是會進行宣告。

var fn=function (){
	console.log("hello")
}
/*執行過程 js解析器對fn進行變數提升,此時fn=undefined 然後
開始執行程式碼,在第一行fn被賦值為一個函式*/

如果在if條件下宣告變數,則無論該if語句是否成立,都會對其中的用var進行宣告的變數進行變數提升。如果宣告的是函式(特指用function xx(){}這種方式進行宣告),在較新的瀏覽器中,會進行變數宣告,但不會進行賦值

function f(){return true}
function g(){return true}
(function(){
  if(g()){
    function g(){return false}
  }
})()/*這種寫法叫立即執行函式,就是將函式宣告轉化為函式表示式,
然後進行在後面加括號直接呼叫
console.log(f(),g());//這裡是沒有執行結果的,會直接報錯
TypeError: g is not a function*/
/* 原因
   首先js解析器會進行變數提升,將f g進行定義,賦值。然後開始執
行程式碼,遇到立即執行函式,開闢一個新的棧空間,首先進行變數提升,
將立即執行函式內部的g進行變數宣告=>var g 然後開始執行程式碼,
遇到if(g()),由於此時g的值為undefined,所以報錯,g不是一個函式
*/
/*
  常用的將函式宣告轉化為函式表示式的寫法有 
  !function(){}()
  +function(){}()
  -function(){}()
  ~function(){}()
  (function(){})()
  (function(){}())
*/

es6

現在處於混亂時期,es5和es6並行。所以,瀏覽器會根據變數的宣告是否是let和const決定當前變數該使用哪種規則。

當我們使用let或const建立變數的時候,js解析器不會進行變數提升,但是在進入作用域時(包括函式作用域 全域性作用域 塊級作用域(通過單純的花括號定義的作用域,不帶function,比如 if () { },如果在花括號內用let const function宣告變數,則花括號內部會被瀏覽器視作塊級作用域)),會進行語法檢查,檢查是否有重名的變數(不管是let還是var宣告的變數),如果有,則直接報錯,同時檢查是否有let變數在宣告前呼叫,如果有,則報錯。

let a=10,
    b=10
let fn=function (){
    let a=b=20
    console.log(a,b)//==> 20 20
}
fn()
console.log(a,b)//==> 10 20

// 由於全是es6的語法,所以js解析器會進行es6的解析
/* 
  首先會進行語法檢查 發現a b fn並沒有重名的。然後會開始執行程式碼,宣告a,b,fn三個變數
  然後執行fn這個函式,建立一個新的棧空間,然後進行語法檢查,發現只有一個變數a。開始執行
  程式碼,宣告一個a變數,存在這個棧空間內,然後給b(全域性作用域下 存在全域性作用域的棧空間內)
  賦值為20 然後輸出a(自己的棧空間下)和b(全域性棧空間下)的值 然後執行結束,回收這個函式的棧空間
  然後執行最後一行輸出 變數a和b都是全域性作用域下的變數
*/
var a=18
if(true){
	console.log(a) // Cannot access 'a' before initialization
    let a=20
}
/*
  在進入if語句時,首先進行語法檢查 發現在第四行用let聲明瞭a變數,然後開始從第三行執行,發現此
  時呼叫了a變數,可是檢查時a變數在第四行宣告,於是就報錯了
*/

如果在塊級作用域內用function宣告函式(不使用var),並且函式名與外部變數重名,則在執行到該函式宣告位置前所有對這個函式名的操作都會對外部的變數進行操作。

var a = 0;
if (true) {
  console.log(a);
    a = 1;
    console.log(a);
    function a() {};
    a = 21;
    console.log(a)//21
}
console.log(a);//1
/*
  解析
  由於塊級作用域內的函式a和外面的變數a重名,所以,在執行到這個函
數宣告之前,對a的操作都屬於對全域性變數a的操作。
*/

練習題

var a=12,
    b=13,
    c=14
function fn(a){
    console.log(a,b,c)
    var b=c=a=20
	console.log(a,b,c)
}
fn(10)
console.log(a,b,c)
//解析

/*
  輸出分別為
  10 undefined 14
  20 20 20
  12 13 20
  首先進入全域性作用域,開始變數提升,宣告a,b,c三個變數,此時他們的值
為undefined,然後宣告fn這個函式變數,併為他賦值為console.log.....
  然後開始執行程式碼,首先給a b c進行賦值,為12 13 14,然後呼叫函式fn,
開闢一個新的棧記憶體,先給fn的引數a賦值為10. 然後對b進行變數提升,此時b
的值為undefined。然後開始執行程式碼。首先輸出 a(函式的形參) b(下一行
宣告的變數) c(全域性作用域的c),那麼結果當然是10 undefined 14了,然後
對b,c(全域性作用域),a分別賦值為20,再次輸出a b c,值均為20,函式呼叫
結束,回收函式棧記憶體。輸出 a b c,由於在函式裡已經對c賦值為20了,所以輸
出的就是12 13 20
*/
var foo = 1;
function bar() {
   if(!foo) {
       var foo = 10;
   }
   console.log(foo); 
}
bar();
//解析
/*
  輸出10
  首先在全域性作用域下,變數foo進行宣告,bar進行宣告,賦值。然後開始執行程式碼,
給foo進行賦值。執行bar函式,建立一個私有棧空間,開始變數提升(將內部的foo
進行宣告),此時foo的值為undefined,然後開始執行if判斷,undefined取反為true
,對foo進行賦值10,然後輸出10
*/