學無止境,不會的爲啥老是那麼多?chrome
今天在一個技術羣裏面,有朋友丟了一道題目,問這個輸出是啥?瀏覽器
粗略一看,輸出都是 21 啊,你覺得加個 if(true),大夥就不知道了?函數
而後,就沒有而後了…………ui
「光榮」的答錯了!spa
正確答案:內部是 21,外部是 1;3d
這個玄妙之處確實就在這個塊級做用域 if 裏面。指針
假如咱們去掉 if 看題。code
var a = 0;
// if(true){
a = 1;
function a(){}
a = 21;
console.log("裏面",a);
// }
console.log("外部",a);
複製代碼
這道題估計沒人好意思去問了,毫無疑問,輸出的 a 都是 21 啊。cdn
然而在 JS 裏面都說沒有塊級做用域,那爲啥 if 這個塊會影響最終結果呢?blog
如下爲chrome瀏覽器下的實現
本文主要點:
主要分爲變量提高和函數提高。
變量的提高是以變量做用域來決定的,即全局做用域中聲明的變量會提高至全局最頂層,函數內聲明的變量只會提高至該函數做用域最頂層。
看題:
console.log(a);
var a = 0;
複製代碼
這個並不會報: Uncaught ReferenceError: a is not defined。
而是會輸出 undefined。
由於變量提高以後的結果是:
var a;
console.log(a);
a = 0;
複製代碼
例子1:
var x = 0;
function a(){
console.log(x);
let x = 1;
}
a();
複製代碼
若是 let x 不會變量提高的話,那麼應該 x 輸出 0,其實是:
VM296:3 Uncaught ReferenceError: Cannot access 'x' before initialization at a (:3:14) at :1:1
它也不是報錯 x not defined,而是 Cannot access。
那這個報錯是啥緣由呢?
例子2:
let a = a;
let a = a;
複製代碼
你以爲會報什麼錯誤呢?
「 a not defined 」 或者 「Cannot access 'a' before initialization」 ?
實際上都不是,而是報錯: a has already been declared。
這裏看起來是:let 也會 「變量提高」,若是不會提高的話,例子1 的 x 應該輸出 0 ,例子2 應該報錯 a not defined。
可是若是會變量提高,那也說不過去呀, 那上面的例子1 應該輸出 undefined 啊。
因而咱們管這叫 「暫時性死區」。
實際上這個既不是咱們理解的變量提高,也不是沒有變量提高。那什麼是暫時性死區呢?
let 定義變量是有一個「特殊聲明」的過程,JS 預解析的時候,先將定義的 let ,const 「特殊聲明」提早,相似「舉手」,JS 引擎規定了同一個做用域,同一個變量只能被一次「舉手」。
這裏不一樣於 var 的定義和賦值,var 的聲明是若是已經聲明瞭,後者直接忽略聲明。
咱們繼續回到本題目來看。
let a = a; // 定義變量 a,我暫標識爲 a1
let a = a; // 定義變量 a,我暫標識爲 a2
複製代碼
預解析,將 a1 聲明,而後準備將 a2 聲明,這個時候,JS 引擎發現,聲明 a2 的時候 ,已經有 a1 聲明瞭。
因而違反了 「同一個做用域,同一個變量只能被聲明一次」 的規定,直接報錯。實際上代碼中賦值的 a 變量還沒讀取(在讀取變量的時候纔可能拋變量未定義的錯誤)。
因此,報錯了,錯誤內容:a2 已經被聲明瞭(被 a1 聲明瞭 a)。
因此回到上述例 1,代碼在讀取 x 的時候,發現已有 let 聲明的 x ,可是並未初始化,才直接報錯 x 沒法訪問。
那麼 let 變量「特殊聲明」是一個什麼神奇的東西呢?
其實是 JS 引擎爲了解決這個 let 變量提高時引入的 declareData, 在預解析的時候,裏面存儲了做用域裏面全部的 let 和 const 聲明數據。
事實上,做用域內全部的函數和變量的建立都須要校驗是否與 declareData 的值衝突。
例子 3:
var a = 1;//定義變量 a,我暫標識爲 a1
let a = 2;//定義變量 a,我暫標識爲 a2
複製代碼
declareData 聲明變量 a2,而後準備定義變量 a1,發現 declareData 已經有聲明 a2 了,直接報錯: a1 已經被聲明瞭,由於已經由 a2 聲明瞭變量 a 。
函數提高,相似變量提高,可是確有些許不一樣。
console.log(a);// undfined
var a = function (){}
console.log(a); // function a
function a(){}
複製代碼
函數表達式不會聲明提高,第一個例子輸出的是 undefined 而不是 not defined,是由於中了變量 var a 的變量提高。
console.log(a);// undefined
if(true){
console.log(a); // function a
function a(){}
}
複製代碼
若是是變量提高,是不存在塊級做用域的,可是函數提高是存在的,這個預解析以下:
var a; // 函數 a 的聲明
console.log(a);// undefined
if(true){
function a(){} // 函數 a 的定義
console.log(a); // function a
}
複製代碼
其實函數 function a(){} 在通過預解析以後,將函數聲明提到函數級做用域最前面,而後將函數定義提高到塊級做用域最前面。
注意:這裏的函數定義是提高到塊級做用域最前面。
再看一題:
try{
console.log(a);// undefined
aa.c;
}catch(e){
var a = 1;
}
console.log(a);// 1
console.log(e);// Uncaught ReferenceError: e is not defined
複製代碼
在 catch 裏面定義的 a,被聲明提早了。可是 catch 裏面的 e 在外部沒法訪問。 若是你認爲 catch 是一個「函數做用域」,那麼裏面的 a 不該該被提高到最外層。實際上 catch 裏面遵循的是塊做用域。
在 JS 領域內,自己是存在塊級做用域的(let const 以外)。
爲了方便閱讀,我再貼一下題目:
var a = 0;
if(true){
a = 1;
function a(){}
a = 21;
console.log("裏面",a);
}
console.log("外部",a);
複製代碼
結合上面咱們瞭解到的知識。首先,if 裏面的 function a(){} 會聲明提高,將聲明" var a" 移到函數級做用域最前面,將函數定義移到塊級做用域最前面,預解析以下:
var a;// 函數 a 的聲明提早
var a = 0; // 已經聲明瞭 a,這裏會忽略聲明 ,直接賦值爲 0
if(true){
function a(){} // 函數定義 a 聲明提高到塊級最前面
a = 1; // 這裏將 塊級做用域最前面的函數 a 重置爲 1了。
// function a(){}; how do ?
a = 21;
console.log("裏面",a);
}
console.log("外部",a);
複製代碼
函數自己是【 定義函數名變量 指針指向 函數內存塊】。
函數提高是在塊級做用域,可是函數名變量是函數級別的做用域。因此在塊級的函數定義(原始代碼函數的聲明位置)的時候,會將函數名變量同步到函數級做用域,實際上,只有這個時候,在塊級做用域外才能訪問到函數定義。
預解析以下:
var a = 0;
console.log(a,window.a); // 輸出 0 和 0
if(true){
console.log(a,window.a);// 函數提高,是塊級做用域,輸出 function a 和 0
a = 1; // 取做用域最近的塊級做用域的 function a ,且被重置爲 1了,本質又是一個 變量的賦值。
console.log(a,window.a);// a 是指向塊級做用域的 a, 輸出 1 和 0
function a(){} // 函數的聲明,將執行函數的變量的定義同步到函數級的做用域。
console.log(a,window.a);// 輸出 1 和 1
a = 21; // 仍然是函數定義塊級做用域的 a ,重置爲 21
console.log(a,window.a); // 輸出爲函數提高的塊級做用域的 a, 輸出 21,1
console.log("裏面",a);
}
console.log("外部",a);
複製代碼
到此爲止,應該都理解了吧,不再會答錯了吧!若是不理解,再多看幾遍試試。