JavaScript必需要掌握的知識-做用域

JavaScript編譯過程

在學習做用域以前先簡單瞭解一下JavaScript的編譯、執行過程。編程

JavaScript被稱之爲解釋性語言,與Java等這類編譯語言區別在於:JavaScript代碼寫好了就能夠直接當即執行,Java則須要相對較長時間的編譯過程纔可生成可執行的機器碼。瀏覽器

但其實JavaScript也是有編譯過程的,JavaScript使用的是一種即時編譯的方式(JIT)。 JIT會把JavaScript代碼中會屢次運行的代碼給編譯器編譯,生成編譯後的代碼並保存起來,在下次使用時使用編譯好的代碼。這實際上是JavaScript運行環境採用的一種優化解決方案。 若是不這麼作,大量重複的代碼都會在運行前重複編譯,這將極大的影響性能與運行效率。性能優化

JavaScript引擎也會對JavaScript代碼在運行前進去預編譯,在預編譯的過程當中會定義一套規則用來存儲變量,對象,函數等,方便在以後的運行調用。這套規則就是做用域編程語言

JavaScript引擎在編譯過種中要對代碼進行詞法分析、語法分析、代碼生成、性能優化等等一系列工做。JIT就是這一過程當中用來優化的一部分。模塊化

var a = 1;

var a = 1; 這行代碼在運行前編譯器都會作哪些事情?函數

編譯器會把這行代碼分紅 var aa = 1 ,兩個部分。性能

  1. 首先編譯器會在相同做用域內查詢是否已經存在一個叫 a 的變量,如是存在,編譯器會忽略聲明a,繼續下一步編譯;若是不存在,則在當前做用域聲明一個變量,命名爲a
  2. 而後編譯器會爲引擎生成運行時的代碼,這些代碼中包含處理a = 1的部分,引擎在處理a = 1的時候,一樣也會查詢做用域中是否存在a變量(會逐級向上一個做用域查找), 存在則賦值爲2,不存在則拋出異常(嚴格模式下,如非嚴格模式則會隱式建立一個全局變量aLHS)。

LHS查詢 & RHS查詢

LHS 和 RHS 的含義是「賦值操做的左側與右側」,不過要注意並不單指「=」和左側與右側。 賦值操做還有其它的形式,所以能夠理解爲:LHS-賦值操做的目標是誰? RHS-誰是賦值操做的源頭。學習

a = 1; 是對a LHS查詢,a是賦值操做的目標,爲a賦值爲1. 如LHS查詢失敗,非嚴格模式下會隱式建立一個全局變量,嚴格模式下會拋出ReferenceError: a is not defined;優化

console.log(a) 是對a RHS查詢,a是賦值的源頭;若是在做用域鏈中沒有查詢到a,一樣也會拋出ReferenceError: a is not defined;code

做用域鏈

做用域是存儲變量的一套規則,當代碼運行時可能並不僅是在一個做用域查詢變量。 當一個做用域中包含另外一個做用域的時候,就會存在做用域嵌套的狀況。因此當內部的做用域沒法找到某個變量的時候,引擎會在當前做用域的外層嵌套中繼續查詢;直到查到變量或者達到最外層的做用域爲止。這就是做用域連接

var name = "rewa"; 

function sayHi(){
    console.log("hello,"+name);
}

sayHi(); // hello,rewa

如上述代碼,sayHi函數做用域中並無變量name;卻能正常引用。就是由於引擎在上一層做用域找到並使用了變量name;

var name = "rewa"; 

function sayHi(){
    var name = "fang"; // 添加的代碼
    console.log("hello,"+name);
}

sayHi(); // hello,fang

sayHi做用域中已經找到變量name時,引擎會中止向上層做用域查找,這叫做「遮蔽效應」,內部變量遮蔽外部做用域變量。

詞法做用域

做用域有兩種主要的工做模型。一種是最爲最爲廣泛的,被大多數編程語言採用的詞法做用域; 還有一種叫動態做用域

詞法做用域就是在寫代碼時將變量和塊做用域寫在哪裏做用域就在哪裏,定義在詞法階段的做用域。JavaScript就是採用的詞法做用域。

詞法:就是組成代碼塊的字符串。好比:

var a = 1;

這行代碼中,vara=2; 還有這中間的空格 都是詞法單元。

編譯器的第一個工做就是詞法化,會把代碼分解成一個一個詞法單元;具體編譯器在詞法化階段都作了哪些工做遵照哪些規則,根據不一樣編程語言而不一樣。JavaScript是怎麼樣的規則我特麼也不清楚,等我研究清楚了;再來作一個筆記。

簡單的說,詞法做用域就是你寫代碼的時候,把變量a寫在函數b中,那麼編譯器編譯時b的做用域中就會包含有a變量,編譯器會保持詞法做用域不變。(也會有特殊狀況)

以下代碼:

var a = 1;
function foo(){
    var b = a + 2;
    function bar(){
        var c = b + 3;
        console.log(a,b,c)
    }
    bar();
}

foo(); // 1,3,6

這段代碼編譯後的做用域與你編寫時的詞法做用域是一致的。

全局做用域: 變量a, 函數 foo

函數foo()建立的做用域:變量b,函數bar

函數bar()建立的做用域:變量c

代碼寫在哪做用域就在哪。

瞭解詞法做用域須要注意如下幾點:

  • 不管函數在哪裏被調用,如何被調用,函數的詞法做用域都只由函數被聲明時所處的位置決定。
  • 詞法做用域查詢只會查找一級標識符,好比上述代碼中的變量a,b,c。若是訪問foo.bar.baz,詞法做用域只會查詢foo。找到這個變量後,再訪問屬性bar,再到baz
  • 存在使詞法做用域編譯後不一致的方法,但會致使性能降低。

修改詞法做用域的方法 eval & with (千萬不要這麼作)

eval

代碼以下:

var a = 1;
function foo(str){
    eval(str);
    console.log(a);
}
foo('var a = 2;'); // 2

var a = 1; 會在函數foo中運行,變量a將包含做用域。 eval(...)函數接受一個字符串,並將字符串看成代碼運行;就至關於把代碼寫在這個位置。

eval在嚴格模式下會拋出異常:

var a = 1;
function foo(str){
    "use strict"
    eval(str);
    console.log(a); // ReferenceError: a is not defined
}
foo('var a = 2;');

默認狀況下,若是eval()中有包含聲明,就會對所處的詞法做用域進行修改;在嚴格模式下,eval()在運行時有其本身的詞法做用域,那麼將沒法修改所在的做用域,如上述代碼。

with
var obj = {
    a:1,
    b:2,
    c:3
}

obj.a = 11;
obj.b = 22;
obj.c = 33;

// with 也能夠達到一樣的效果
with(obj){
    a=111;
    b=222;
    c=333;
}
//這樣 obj 被修改成:
{   
    a:111,
    b:222,
    c:333
}

with()接受一個參數,在這裏是obj;此時with中做用域是obj, 能夠訪問obj中的屬性。 這種方式賦值就變得簡潔不少。

with能夠爲一個對象建立一個做用域,對象的屬性會定義爲這個做用域中的變量;不過with中的經過var聲明的變量並不會成爲這個做用域的成員,而是被聲明到with所在的做用域中。這不正常了,代碼使用with會變得很不容易控制。好比:

with(obj){
    a=111;
    b=222;
    c=333;
    d=444;
}

console.log(obj.d); // undefined
console.log(d); // 444

原來覺得會添加在obj中的屬性d,卻被添加到了全局做用域中;這就可能與開發編寫時的預期結果不符;也不符合詞法做用域的規則。

因此evalwith都已經被禁止了,也不推薦使用。

這種不可預估詞法做用域的特性,也帶了一個嚴重的性能問題。 JavaScript引擎在編譯階段會進行性能優化。其中有一些優化依賴代碼的詞法,對詞法進行靜態分析,並預先肯定全部變量與函數的定義位置,才能在執行過程當中快速找到變量。

若是引擎在代碼中發現了evalwith,它沒法在詞法分析階段明確知道eval(...)接生什麼代碼;也沒法知道傳遞給with用來建立新詞法做用域的對象內容是什麼。 那麼優化未知的代碼和詞法做用域是沒有意義的,引擎將放棄優化這一部分。

若是在代碼中頻繁使用evalwith,程序運行起來將會很是慢。

函數做用域

函數內部的變量和函數定義均可以封裝起來,外部沒法訪問封裝在函數內部的變量標識符。

以下代碼:

function foo(){
    var a = 1;
    function sayHi(){
        console.log('Hello!')
    }
}

console.log(a); // ReferenceError:a is not defined
sayHi(); //ReferenceError: sayHi is not defined

在函數外部訪問其內部的變量與函數會拋出異常。

這樣函數就能夠行成一個相對獨立的做用域,能夠用函數來封裝一個相對獨立的功能。 把業務代碼隱藏在函數內部實現,對外暴露接口;只要傳入不一樣的參數就能夠輸入對應的結果。 因此不少狀況下函數能夠用來模擬Java語言中類的實現。

例如:

function shoot(who,score){
    //這裏面能夠包含更多邏輯
    function one(){
        console.log(who + '罰籃命中!到得' +score+ '分!');
    }
    function dunk(){
        console.log(who + '扣籃,得到' +score+ '分!');
    }
    function three(){
        console.log(who + '命中了一個' +score+ '分球!');
    }
    switch(score){
        case 1:
            one();
            break;
        case 2:
            dunk();
            break;
        case 3:
            three();
            break;
    }
}

shoot('Kobe',3); // Kobe投中了一個3分球!'
shoot('Lebron',2); // Lebron扣籃,得到2分!' 
shoot('Shaq',1); // Shaq罰籃命中!到得1分!'

函數內部隱藏變量與函數的定義能夠避免污染全局命名空間;好比當全局做用域中也有one dunk three 這些函數,而且內部實現不一樣;代碼邏輯就會混亂。 而在上面的代碼中,函數中定義的函數會遮蔽外部做用域的函數定義,只會調用到當前函數做用域中的同名函數。

可是即便如此,大量的函數聲明一樣也會污染全局全名空間。 當下流行的模塊化就是解決這一問題的方案之一。不過在模塊化出來以前,大多數狀況可使用當即執行函數(IIFE)來解決。 代碼以下:

(function(){
    var name = 'kobe';
    console.log(name);
})();

當函數執行結束後,name變量會被垃圾回收; 且不會與外部的任何做用域產生衝突,由於整個函數都執行在一個當即執行函數中。它是一個塊做用域,且自己也沒有在做用域下建立任何標識符。

當即執行函數也能夠接受參數,用來函數內部引用:

(function(name){
    console.log(name);
})('kobe');

JavaScript中除了函數做用域,還有其它塊做用域。好比with也是塊做用域;上面有過介紹with。 還有一個容易被忽略的塊做用域 try/catch

try{
    undefined(); //拋出異常
}
catch(err){
    console.log(err); // 正常執行
}

console.log(err); //ReferenceError: err is not defined

err只能在catch中訪問,在外部的引用會拋出異常。

對於塊做用域,ES6中咱們能夠用let聲明實現這種需求。

if(true){
    let a = 1;
    console.log(a); //1
}

console.log(a); //ReferenceError: a is not defined

if(){} 並非塊做用域,但上述代碼中let可讓a變量成爲僅if(){...}中的變量,外部不可訪問。

這是否是像極了try/catch , 可letES6的標準;在ES6以前實現相似塊做用域效果的方法可沒這麼輕鬆。 如今通常咱們在編寫ES6代碼,想要運行在全部瀏覽器上須要經過轉譯。而轉譯器也會把相似let的聲明,轉爲 try/catch的形式。

{
    let a = 1;
    console.log(a); // 1
}
console.log(a); //ReferenceError: a is not defined

轉爲:

try{
    throw 1;
}catch(a){
    console.log(a); //1
}
console.log(a); //ReferenceError: a is not defined

還有可能轉譯爲:

{
    let _a = 1;  // 把{}中的 a 轉爲_a 
    console.log(_a); 
}
console.log(a);
相關文章
相關標籤/搜索