JavaScript的做用域詳解

做用域

做用域(scope),程序設計概念,一般來講,一段程序代碼中所用到的變量並不老是有效/可用的,而限定這個變量的可用性的代碼範圍就是這個變量的做用域。通俗一點就是我要把個人變量分紅一坨一坨保管起來,有些地方只能用這幾個變量,有些地方只能用另外幾個變量,而這個分開的一坨一坨的區域就是做用域~es6

那這個做用域何時用到的呢?編程

沒錯就是編譯的時候~
讓咱們來看看編譯的大概流程數組

  • 詞法分析(這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊)
  • 語法分析(這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的樹。這個樹被稱爲「抽象語法樹」)
  • 代碼生成(將這棵「樹」 轉換爲可執行代碼,將咱們寫的代碼變成機器指令並執行)

比起上面這些編譯過程只有三個步驟的語言的編譯器,JavaScript 引擎要複雜得多。例如,在語法分析和代碼生成階段有特定的步驟來對運行性能進行優化,包括對冗餘元素進行優化等,可是大致上也是差很少的流程~閉包

那咱們要編譯var a = 2的話,是‘誰’來執行編譯的過程呢?編程語言

噹噹噹當~函數

  • 引擎:負責整個編譯運行的所有過程。
  • 編譯器:負責詞法分析以及代碼生成。
  • 做用域:負責收集維護全部聲明的標識符,肯定當前執行代碼對標識符的訪問權限。

當咱們看到var a = 2的時候,咱們認爲是一條聲明,可是對於引擎來講,這是兩個徹底不同的聲明,分爲下面兩部分性能

  • 1.遇到 var a,編譯器會詢問做用域是否已經有一個該名稱的變量存在於同一個做用域的集合中。若是是,編譯器會忽略該聲明,繼續進行編譯;不然它會要求做用域在當前做用域的集合中聲明一個新的變量,並命名爲a(嚴格模式下報錯)。
  • 2.接下來編譯器會爲引擎生成運行時所需的代碼,這些代碼被用來處理 a = 2這個賦值操做。引擎運行時會首先詢問做用域,在當前的做用域集合中是否存在一個叫做 a的變量。若是是,引擎就會使用這個變量;若是否,引擎會繼續查找該變量。

能夠看到,編譯的時候,編譯器和引擎須要詢問做用域,所求變量是否存在,而後根據查詢結果來進行不一樣的操做學習

做用域嵌套

上面咱們展現了只有一個做用域,變量的聲明和賦值過程。
實際狀況中,咱們一般須要同時顧及幾個做用域。當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量,或抵達最外層的做用域(也就是全局做用域)爲止;可是反過來,外層的做用域沒法訪問內層做用域的變量,若是能夠的話那不就全都是全局變量了嗎嘿嘿嘿優化

function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4

當引擎須要變量b的時候,首先在foo的做用域中查找,發現沒有b的蹤跡,因而就跑出來,往上面一層做用域走一走,發現了這個b原來在全局做用域裏待着,那可不得一頓引用!若是全局做用域也沒有b的話,那就得報錯了,告訴寫代碼的傻子「你豬呢?一天到晚淨會寫bug!」。spa

clipboard.png

第一層樓表明當前的執行做用域,也就是你所處的位置。建築的頂層表明全局做用域。
變量引用都會在當前樓層進行查找,若是沒有找到,就會坐電梯前往上一層樓,若是仍是沒有找到就繼續向上,以此類推。一旦抵達頂層(全局做用域),可能找到了你所需的變量,也可能沒找到,但不管如何查找過程都將中止

函數做用域

能夠看到咱們在上面生成兩層做用域(一層foo一層全局)的時候用了函數。由於JavaScript的函數能夠產生一層函數做用域。
上代碼!

function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log( a, b, c );
    }
    bar( b * 3 );
}
foo( 2 ); // 2, 4, 12

咱們來分析一下上面幾行代碼。這個例子裏面包含了三層逐級嵌套的做用域,其中兩個函數生成了兩層嵌套做用域。

1.包含着整個全局做用域,其中只有一個標識符: foo 。
2.包含着 foo 所建立的做用域,其中有三個標識符: a 、 bar 和 b 。
3.包含着 bar 所建立的做用域,其中只有一個標識符: c 。

因爲bar是最內層的做用域,若是在它做用域內的查詢不到它須要的值,它會逐級往外查詢外層做用域的同名變量。若是查詢到了則取用~

塊級做用域

儘管函數做用域是最多見的做用域單元,固然也是現行大多數 JavaScript 中最廣泛的設計方法,但其餘類型的做用域單元也是存在的,而且經過使用其餘類型的做用域單元甚至能夠實現維護起來更加優秀、簡潔的代碼。(若是你會其餘一些語言你就會發現一個花括號不就一個塊級做用域了嗎)
咱們來看看JavaScript中的花括號~

for(var i=0;i<5;i++){console.log(window.i)} //0 1 2 3 4

你驚奇的發現,媽耶,我這個var不等於白var嘛,反正都是全局變量(若是你沒在函數內使用的話)。
是的JavaScript就是這麼的高端兼靈性~(滑稽)
if的花括號和for是同樣的,不作贅述。

那咱們怎樣整一個獨立做用域?而後我又不想一直聲明函數
JavaScript有四種方式能夠產生塊級做用域。

  • with
  • try/catch
  • let
  • const

讓咱們來介紹一下這四種東西吧~

1.首先是with,算了,垃圾,不講。好處很少,壞處卻是挺多,有興趣百度用法~不建議使用
2.而後是try/catch, ES3 規範中規定 try / catch 的 catch 分句會建立一個塊做用域,其中聲明的變量僅在 catch 內部有效
try{throw 2}catch(a){console.log(a)};
console.log(a);//Uncaught ReferenceError
3.let,這個是es6引入的新關鍵字,很是香~看下面能夠和上面的var i的循環作對比
for(let j=0;j<5;j++)(console.log(window.j));//undefined *5
4.這個跟let差很少,可是是用來定義常量的。const a = 5;a = 6;//報錯

ok~這個很敢單~讓咱們來學習下一部分

提高

在最開始以前,咱們先來學習一下兩種報錯。

  • ReferenceError 異常
  • TypeError

第一種的出現是由於遍歷了全部的做用域都查找不到變量,第二種是找到了這個變量,可是對這個變量的值進行了錯誤的操做,好比試圖對一個非函數類型的值進行函數調用

咱們先來看看下面的代碼會輸出什麼

a = 2;
var a;
console.log( a );

你可能會覺得,我先給a賦值了2,而後var a又給a賦值了undefined,因此會輸出undefined。可是這個輸出了2。
咱們再來看一題

console.log( a );
var a = 2;

這個時候你可能認爲會報ReferenceError異常,由於使用在前,使用的時候a尚未定義,做用域確定也找不到a,可是這個卻輸出了undefined。

Why?

爲了搞明白這個問題,咱們須要回顧一下前面關於編譯器的內容。回憶一下,引擎會在解釋 JavaScript 代碼以前首先對其進行編譯。編譯階段中的一部分工做就是找到全部的聲明,並用合適的做用域將它們關聯起來。
所以,正確的思考思路是,包括變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理。當你看到 var a = 2; 時,可能會認爲這是一個聲明。但 JavaScript 實際上會將其當作兩個聲明: var a; 和 a = 2; 。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執行階段。
上面的第一段代碼就能夠看作

var a;
a = 2;
console.log(a)

第二段代碼則能夠當作

var a;
console.log(a);//此時a還沒賦值,因此是undefined
a = 2;

打個比方,這個過程就好像變量從它們在代碼中出現的位置被「移動」到了最上面(變量所在做用域)。這個過程就叫做提高。

咱們從上面能夠看到變量聲明的提高,那麼對於函數聲明呢?固然是no趴笨啦~

foo();
function foo() {
    console.log( a ); // undefined
    var a = 2;
}

可是,須要注意的是,函數聲明會被提高,可是函數表達式卻不會。

foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};

這個就至關於

var foo;
foo(); // 此時foo確定是undefined啦,undefined()? 對undefined值進行函數調用顯然是錯誤操做!TypeError!
foo = function bar() {
// ...
};

既然函數聲明和變量聲明都會被提高,那它們兩個哪一個提高到更前面呢?

是函數!!函數做爲JavaScript的一名大將,確實是有一些牌面。

foo(); // 1
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
    console.log( 2 );
};

咱們能夠將上面當作

function foo() {
    console.log( 1 );
}
var foo;//重複聲明,能夠去掉
foo(); // 1
foo = function() {
    console.log( 2 );
};

注意:後面的聲明會覆蓋前面的聲明。

foo(); // 3
function foo() {
    console.log( 1 );
}
var foo = function() {
    console.log( 2 );
};
foo();//2
function foo() {
    console.log( 3 );
}

至關於

function foo() {
    console.log( 1 );
}
function foo() {
    console.log( 3 );
}
var foo;
foo(); // 3
foo = function() {
    console.log( 2 );
};
foo();//2

閉包

咱們剛剛講那麼多,相信你們都已經知道而且深信,做用域只能一層一層往外查詢,不能往裏走,那我若是要找一個函數裏的變量值呢?那可咋整啊?
很簡單,咱們不能往裏走,可是咱們能夠再給這個函數裏面整一層做用域,這樣函數裏面的子做用域不就能夠訪問它的變量了嗎?
perfect~

function foo() {
    var a = 2;
    function bar() {
        console.log( a ); // 2
    }
    return bar;
}
var baz = foo();執行了foo()就返回了一個bar;如今至關於baz=bar;
baz();//2

這裏咱們須要獲取a的值,咱們就在裏面寫一個函數bar,顯然這個bar是有權利訪問a的,那咱們返回這個有權利訪問a的函數不就頂呱呱了嗎?

在 foo() 執行後,一般會期待 foo() 的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間。因爲看上去 foo() 的內容不會再被使用,因此很天然地會考慮對其進行回收。
而閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收(頻繁使用閉包可能致使內存泄漏)。誰在使用這個內部做用域?原來是 bar() 自己在使用。拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內部做用域的閉包,使得該做用域可以一直存活,以供 bar() 在以後任什麼時候間進行引用。

來點練習題

第一題
var tt = 'aa'; 
function test(){ 
    alert(tt); 
    var tt = 'dd'; 
    alert(tt); 
} 
test();
第二題
var a = 100;
function test(){
    console.log(a);
    a = 10;
    console.log(a);
}
test();
console.log(a);
第三題
var a=10; 
function aaa(){ 
    alert(a);
};            
function bbb(){
    var a=20;
    aaa();
}
bbb();

答案:

  1. undefined dd
  2. 100 10 10
  3. 10

參考文獻

《你不知道的JavaScript》

最後

有什麼錯誤或者建議能夠在評論區告訴我~謝謝

相關文章
相關標籤/搜索