理解 JavaScript 中的做用域

前言

學習 JavaScript 也有一段時間,今天抽空總結一下做用域,也方便本身之後翻閱。javascript

什麼是做用域

若是讓我用一句簡短的話來說述什麼是做用域,個人回答是:java

其實做用域的本質是一套規則,它定義了變量的可訪問範圍,控制變量的可見性和生命週期。瀏覽器

既然做用域是一套規則,那麼究竟如何設置這些規則呢?性能優化

先不急,在這以前,咱們先來理解幾個概念。閉包

編譯到執行的過程

下面咱們就拿這段代碼來說述 JavaScript 編譯到執行的過程。框架

var a = 2;
複製代碼

首先咱們來看一下在這個過程當中,幾個功臣所須要作的事。函數

  1. 引擎(總指揮):工具

    從頭至尾負責整個 JavaScript 程序的編譯及執行過程。性能

  2. 編譯器(勞工):學習

    1. 詞法分析(分詞)

      解析成詞法單元,vara=2

    2. 語法分析(解析)

      將單詞單元轉換成抽象語法樹(AST)。

    3. 代碼生成

      將抽象語法樹轉換成機器指令。

  3. 做用域(倉庫管理員):

    負責收集並維護全部生命的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限。

而後咱們再來看,執行這段代碼時,每一個功臣是怎麼協同工做的。

引擎:

其實這段代碼有兩個徹底不一樣的聲明,var aa = 2,一個由編譯器在編譯時處理,另外一個則由引擎在運行時處理。

編譯器:

  1. 一套編譯器常規操做下來,到代碼生成步驟。
  2. 遇到var a,會先詢問做用域中是否已經存在同名變量,若是是,則忽略該聲明,繼續進行編譯;不然它會要求做用域聲明一個新的變量a
  3. 爲引擎生成運行a = 2時所需的代碼。

引擎:

會先詢問做用域是否存在變量a,若是是,就會使用這個變量進行賦值操做;不然一直往外層嵌套做用域找(詳見做用域嵌套),直至到全局做用域都沒有時,拋出一個異常。

**總結:**變量的賦值操做會執行兩個動做, 首先編譯器會在當前做用域中聲明一個變量( 若是以前沒有聲明過),而後在運行時引擎會在做用域中查找該變量, 若是可以找到就會對它賦值。

LHS & RHS 查詢

從上面可知,引擎在得到編譯器給出的代碼後,還會對做用域進行詢問變量。

聰明的你確定一眼就看出,LR的含義,它們分別表明左側和右側。

如今咱們把代碼改爲這樣:

var a = b;
複製代碼

這時引擎對a進行 LHS 查詢,對b進行 RHS 查詢,可是LR並不必定指操做符的左右邊,而應該這樣理解:

LHS 是爲了找到賦值的目標。 RHS 是賦值操做的源頭。也就是 LHS 是爲了找到變量這個容器自己,給它賦值,而 RHS 是爲了取出這個變量的值。

做用域嵌套

當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套,進而造成了一條做用域鏈。所以,在當前做用域中沒法找到某個變量時,引擎就會在外層嵌套的做用域中繼續查找,直到找到該變量, 或抵達最外層的做用域(也就是全局做用域)爲止。

詞法做用域

做用域分爲兩種:

  1. 詞法做用域(較爲廣泛,JavaScript所使用的也是這種)
  2. 動態做用域(使用較少,好比 Bash 腳本、Perl 中的一些模式等)

詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的。

看如下代碼,這個例子中有三個逐級嵌套的做用域。

var a = 2; // 做用域1 全局
function foo(){ 
    var b = a * 2; // 做用域2 局部
    function bar(){
		var c = a * b; // 做用域3 局部
    }
}
複製代碼
  1. 做用域是由你書寫代碼所在位置決定的。
  2. 子級做用域能夠訪問父級做用域,而父級做用域則不能訪問子級做用域。

引擎對做用域的查找

做用域查找會在找到第一個匹配的標識符時中止,在多層的嵌套做用域中能夠定義同名的標識符,這叫作「遮蔽效應」(內部的標識符「遮蔽」了外部的標識符)。也就是說查找時會從運行所在的做用域開始,逐級往上查找,直到碰見第一個標識符爲止。

全局變量(全局做用域下定義的變量)會自動變成全局對象(好比瀏覽器中的 window對象)。

var a = 1;
function foo(){
    var a = 2;
    console.log(a); // 2
    function bar(){
        var a = 3;
        console.log(a); // 3
        console.log(window.a); // 1
    }
}
複製代碼

非全局的變量若是被遮蔽了,就不管如何都沒法被訪問到,因此在上述代碼中,bar內的做用域沒法訪問到foo下定義的變量a

詞法做用域查找只會查找一級標識符,好比ab,若是是foo.bar,詞法做用域查找只會試圖查找foo標識符,找到這個變量後,由對象屬性訪問規則接管屬性的訪問。

欺騙語法

雖然詞法做用域是在代碼編寫時肯定的,但仍是有方法能夠在引擎運行時動態修改詞法做用域,有兩種機制:

  1. eval
  2. with

eval

JavaScript 的 eval函數能夠接受一個字符串參數並做爲代碼語句來執行, 就好像代碼是本來就在那個位置同樣,考慮如下代碼:

function foo(str){
    eval(str) // 欺騙
    console.log(a);
}
var a = 1;
foo("var a = 2;"); // 2
複製代碼

彷彿eval中傳入的參數語句本來就在那同樣,會建立一個變量a,並遮蔽了外部做用域的同名變量。

注意

  • eval一般被用來執行動態建立的代碼,能夠根據程序邏輯動態地將變量和函數以字符串形式拼接在一塊兒以後傳遞進去。
  • 在嚴格模式下,eval沒法修改所在的做用域。
  • eval類似的還有,setTimeoutsetIntervalnew Function

with

with一般被看成重複引用同一個對象中的多個屬性的快捷方式, 能夠不須要重複引用對象自己。

使用方法以下:

var obj1 = { a:1,b:2 };
function foo(obj){
    with(obj){
        a = 2;
        b = 3;
    }
}
foo(obj1);
console.log(obj1); // {a: 2, b: 3}
複製代碼

然而考慮如下代碼:

var obj2 = { a:1,b:2 };
function foo(obj){
    with(obj){
        a = 2;
        b = 3;
        c = 4;
    }
}
foo(obj2);
console.log(obj2); // {a: 2, b: 3}
console.log(c); // 4 很差,c被泄露到全局做用域下
複製代碼

儘管with能夠將對象處理爲詞法做用域,可是這樣塊內部正常的var操做並不會限制在這個塊的做用域下,而是被添加到with所在的函數做用域下,而不經過var聲明變量將視爲聲明全局變量。

性能

evalwith會在運行時修改或建立新的做用域,以此來欺騙其餘書寫時定義的詞法做用域,然而 JavaScript 引擎會在編譯階段進行性能優化,有些優化依賴於可以根據代碼的詞法進行靜態分析,並預先肯定全部的變量和函數的定義位置,才能在執行過程當中快速找到標識符。可是經過evalwith來欺騙詞法做用域會致使引擎沒法知道他們對詞法做用域作了什麼樣的改動,只能對部分不進行優化,所以若是在代碼中大量使用evalwith就會致使代碼運行起來變得很是慢。

函數做用域和塊做用域

函數做用域

在 JavaScript 中每聲明一個函數就會建立一個函數做用域,同時屬於這個函數的全部變量在整個函數的範圍內均可以使用。

塊做用域

從 ES3 發佈以來,JavaScript 就有了塊做用域,建立塊做用域的幾種方式有:

  • with

    上面已經講了,這裏再也不復述。

  • try/catch

    try/catchcatch 分句會建立一個塊做用域,其中聲明的變量僅在 catch 內部有效。

    try{
    	throw 2;
    }catch(a){
    	console.log(a);
    }
    複製代碼
  • letconst

    ES6 引入的新關鍵詞,提供了除 var 之外的變量聲明方式,它們能夠將變量綁定到所在的任意做用域中(一般是{}內部)。

    {
        let a = 2;
    }
    console.log(a); // ReferenceError: a is not defined
    複製代碼

    注意:使用 letconst 進行的聲明不會在塊做用域中進行提高。

提高

考慮這段代碼:

console.log( a ); 
var a = 2;
複製代碼

輸入結果是undefined,而不是ReferenceError

爲何呢?

前面說過,編譯階段時,會把聲明分紅兩個動做,也就是隻把var a部分進行提高。

因此第二段代碼真正的執行順序是:

var a; // 這時 a 是 undefined
console.log(a);
a = 2;
複製代碼
  • 編譯階段時會把全部的聲明操做提高,而賦值操做原地執行。
  • 函數聲明會把整個函數提高,而不只僅是函數名。

函數優先

雖然函數和變量都會被提高,但函數聲明的優先級高於變量聲明,因此:

foo(); // 1
var foo;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
}
複製代碼

由於這個代碼片斷會被引擎理解爲以下形式:

function foo(){
    console.log(1);
}
foo(); // 1
foo = function() { 
  console.log( 2 );
};
複製代碼

這個值得一提的是,儘管var foo出如今function foo()...以前,但因爲函數聲明會被優先提高,因此它會被忽略(由於重複聲明瞭)。 注意:

JavaScript 會忽略前面已經聲明過的聲明,無論它是變量仍是函數,只要其名稱相同。

後記

由於篇幅緣由,有一部份內容只是大概提到,並無太過於詳細的講解,若是你感興趣,那麼我推薦你看看**《你不知道的 JavaScript(上)》**這本書,書上對此內容有很詳細的說明。

本文也是做者一邊查看此書一邊結合本身的理解來進行編寫的。

其實做用域還有一個很是重要的概念,那就是閉包。但閉包也是 JavaScript 中的一個很是重要卻又難以掌握的,因此須要另開一篇文章來介紹。

最後,我想說的就是,在這個框架工具流行的時代,咱們每每會被這些新東西所吸引,卻忽略了最本質的東西,諸諸不知,偏偏是這些咱們所忽略的東西纔是最重要的,全部的 JavaScript 框架工具都是基於這些內容。因此,不妨回過頭來看看這些原生的東西,相信你會更上一層樓。

謝謝觀看!

注:此文爲原創文章,如需轉載,請註明出處。

相關文章
相關標籤/搜索