我不知道的javaScript--你居然是這樣的做用域

javaScript這門語言真的頗有意思,就算你對他不怎麼了解,只是簡單的知道一點點,在平常使用中也徹底沒問題,畢竟我就是這樣的工做了一段時間,雖然我不是很懂他,可是完成業務也沒什麼壓力,但是爲了能多賺一點錢,我決定把javaScript搞懂,就從做用域開始吧。html

一、做用域是什麼

聲明一個變量是再常作不過的事情了,可是這些變量是儲存在哪裏呢?須要的時候是怎麼找到它們的呢?java

其實javaScript不會像其餘語言編譯器有那麼多的時間進行優化,大部分狀況下編譯發生在代碼執行前的幾微妙,所以聲明一個變量var name = ’shuting’;,javaScript編譯器首先會對var name = ’shuting’;這段程序進行編譯,而後作好執行它的準備,而且一般立刻就會執行它。閉包

當你聲明var name = ’shuting’時,實際上是發生瞭如下步驟1.png函數

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

實際上,當變量出如今賦值操做的左側時進行 LHS 查詢, 出如今非左側時進行 RHS 查詢(若是查找的目的是對變量進行賦值, 那麼就會使用 LHS 查詢; 若是目的是獲取變量的值, 就會使用 RHS 查詢。賦值操做符會致使 LHS 查詢。 = 操做符或調用函數時傳入參數的操做都會致使關聯做用域的賦值操做),因此在上面的例子裏引擎會爲變量name進行 LHS 查詢。this

一個簡單的例子,這段代碼的處理過程是怎樣的呢?spa

function f(name) {
    console.log(name);  // shuting
}
f(’shuting’);

編譯器聲明f函數,name爲f的形式參數 => 引擎運行,詢問做用域要對f進行RHS引用 => 對name進行LHS引用 => 爲console(內置對象)進行RHS引用 => 還有個log(…)是個函數,再對name進行RHS引用,拿到name的值也就是shuting,傳進log(…)。code

當多個做用域發生嵌套時,若是在當前做用域中沒法找到某個變量時, 引擎就會在外層嵌套的做用域中繼續查找, 直到找到該變量,或抵達最外層的做用域(也就是全局做用域) 爲止。htm

遍歷嵌套做用域鏈的規則很簡單:好比下面這個例子對象

function fullName(firstName) {
    console.log(firstName + lastName);
}
var lastName = ‘yang';
fullName(’shuting’);

引擎先在fullName做用域要對lastName進行RHS引用,可是沒找到 => 就去fullName做用域的上一級也就是全局做用域查找,找到了

若是一個變量或者其餘表達式不在」當前做用域」,那麼javaScript機制會繼續沿着做用域鏈上查找直到全局做用域,若是找不到將不可被使用。做用域也能夠根據代碼層次分層,以便子做用域能夠訪問父做用域,一般是指沿着鏈式的做用域鏈查找,而不能從父做用域引用子做用域中的變量和引用。

爲何要區分LHS查詢和RHS查詢呢?
LHS 和 RHS 查詢都會在當前執行做用域中開始, 若是有須要(也就是說它們沒有找到所需的標識符), 就會向上級做用域繼續查找目標標識符, 這樣每次上升一級做用域(一層樓), 最後抵達全局做用域(頂層), 不管找到或沒找到都將中止。
不成功的 RHS 引用會致使拋出 ReferenceError 異常。 不成功的 LHS 引用會致使自動隱式地建立一個全局變量(非嚴格模式下), 該變量使用 LHS 引用的目標做爲標識符, 或者拋出 ReferenceError 異常(嚴格模式下)。

二、常見做用域

全局做用域:

變量在函數或者代碼塊{}外定義,即爲全局做用域。不過,在函數或者代碼塊{}內未定義的變量也是擁有全局做用域的(不推薦)。

var carName = "Volvo";
// 此處可調用 carName 變量
function myFunction() {
    // 函數內可調用 carName 變量
}

上述代碼中變量carName就是在函數外定義的,它擁有全局做用域,這個變量能夠在任意地方被讀取或者修改。若是變量在函數內沒有聲明,該變量依然爲全局變量

// 此處可調用 carName 變量
function myFunction() {
     carName = "Volvo";
    // 此處可調用 carName 變量
}

實際上carName在函數內,擁有全局做用域,它將做爲global或者window的屬性存在。
在函數內部或代碼塊中沒有定義的變量其實是做爲window/global的屬性存在,而不是全局變量。沒有使用var定義的變量雖然擁有全局做用域,可是它是能夠被delete的,而全局變量不能夠。

函數做用域:

在函數內部定義的變量,就是局部做用域。函數做用域內,對外是封閉的,從外層的做用域沒法直接訪問函數內部的做用域。

function out() {
    var a = 1;
    function inner() {
        var b = 2;
        console.log(’this is inner');
 }
 inner();    // this is inner
 var c = 3;
}
inner();   // ReferenceError 錯誤
console.log(a,b,c)    // ReferenceError 錯誤

因爲標識符 a、 b、 c 和 inner 都附屬於 out(..) 的做用域氣泡, 所以沒法從 out(..) 的外部對它們進行訪問。可是在out內部是能夠被訪問的。out函數的所有變量均可以在整個函數的範圍內使用及複用。

function doSomething(a) {
    b = a + doSomethingElse( a \* 2 );
    console.log( b \* 3 );
}
function doSomethingElse(a) {
    return a - 1;
}
var b;
doSomething( 2 ); // 15

上面這個例子有個很大的問題是:變量b和函數doSomethingElse應該是函數doSomething私有的,像如今這樣給予外部做用域對b和doSomethingElse的訪問權限是沒有必要且危險的,下面的例子使doSomethingElse和b都沒法在外部訪問,更合理。

function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    }
    var b;
    b = a + doSomethingElse( a \* 2 );
    console.log( b \* 3 );
}
doSomething( 2 ); // 15

經過上面的例子能夠發現,在任意代碼片斷外部添加包裝函數, 能夠將內部的變量和函數定義「隱藏」 起來, 外部做用域沒法訪問包裝函數內部的任何內容。

塊狀做用域

關於什麼是塊,認識{}就好

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

在這個代碼中, if 後 {} 就是「塊」,這個裏面的變量就是擁有這個塊狀做用域,按照規則,{} 以外是沒法訪問這個變量的。
ES6引入了let/const關鍵字,提供除了var之外的另外一種變量聲明方式,let/const關鍵字能夠將變量綁定到所在的任意做用域中(一般是 { .. } 內部)。 換句話說, let爲其聲明的變量隱式地了所在的塊做用域。
對比下面兩段代碼

for (var i=0; i<10; i++) {
    console.log( i );
}

上面例子:
咱們在 for 循環的頭部直接定義了變量 i, 一般是由於只想在 for 循環內部的上下文中使用 i, 而忽略了 i 會被綁定在外部做用域(函數或全局) 中的事實。

for (let i=0; i<10; i++) {
    console.log( i );
}

上面例子:
for 循環頭部的 let 不只將 i 綁定到了 for 循環的塊中, 事實上它將其從新綁定到了循環的每個迭代中, 確保使用上一個循環迭代結束時的值從新進行賦值。
用 let 將變量附加在一個已經存在的塊做用域上的行爲是隱式的。使用 let 進行的聲明不會在塊做用域中進行提高。 聲明的代碼被運行以前, 聲明並不「存在」。

下面是實際行爲的例子

{
    let j;
    for (j=0; j<10; j++) {
        let i = j; // 每一個迭代從新綁定!
        console.log( i );
    }
}
動態做用域

只在執行階段才能決定變量的做用域,那就是動態做用域

實際上我準備在下一篇文章的時候好好說一說this,等寫好了,再把連接貼過來,嘿嘿。

結合做用域會對 this 有一個清晰的理解。看下這段代碼:

window.a = 3
function test () {
  console.log(this.a)
}

test.bind({ a: 2 })() // 2
test() // 3

在這裏 bind 已經把做用域的範圍進行了修改指向了 { a: 2 },而 this 指向的是當前做用域對象。

function foo() {
    console.log(a); // 2  (不是 3!)
}

function bar() {
    var a = 3;
    foo();
}
var a = 2;
bar();

若是按照動態做用域分析:當 foo() 不能爲 a 解析出一個變量引用時,它不會沿着嵌套的做用域鏈向上走一層,而是沿着調用棧向上走,以找到 foo() 是 從何處 被調用的。由於 foo() 是從 bar() 中被調用的,它就會在 bar() 的做用域中檢查變量,而且在這裏找到持有值 3 的 a。

若是按照靜態做用域分析:foo執行的時候沒有找到 a 這個變量,它會按照代碼書寫的順序往上找,也就是 foo 定義的外層,就找到了 var a=2 ,而不是 foo 調用的 bar 內找。因此結果就是 2。

從這個示例能夠看出 JavaScript 默認採用詞法(靜態)做用域,若是要開啓動態做用域請藉助 bind、with、eval 等。

三、提高

javaScript代碼在執行時是由上到下一行一行執行的。但實際上並不徹底正確,好比下面的例子

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

爲何會得出上面的結果呢,由於引擎會在解釋 JavaScript 代碼以前首先對其進行編譯。 編譯階段中的一部分工做就是找到全部的聲明, 並用合適的做用域將它們關聯起來。其實是這樣的執行順序

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

這個過程就是提高,只有聲明自己會被提高, 而賦值或其餘運行邏輯會留在原地。

咱們習慣將 var a = 2; 看做一個聲明, 而實際上 JavaScript 引擎並不這麼認爲。 它將 var a和 a = 2 看成兩個單獨的聲明, 第一個是編譯階段的任務, 而第二個則是執行階段的任務。

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

其實是如下代碼

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

函數聲明是會被提高的,可是函數表達式並不行哦

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

函數聲明和變量聲明都會被提高。 可是函數會首先被提高, 而後纔是變量。

四、做用域閉包

當函數能夠記住並訪問所在的詞法做用域時, 就產生了閉包, 即便函數是在當前詞法做用域以外執行。有兩種表現:

  • 函數做爲參數被傳遞
  • 函數做爲返回值被返回

下面這個例子就是函數做爲返回值被傳遞的閉包效果

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); // 2

但正常來說引擎有垃圾回收器用來釋放再也不使用的內存空間,而閉包的「神奇」 之處正是能夠阻止這件事情的發生,由於bar() 自己在使用 foo()的做用域,內部做用域依然存在, 所以沒有被回收。bar() 依然持有對該做用域的引用, 而這個引用就叫做閉包。

函數做爲參數被傳遞的閉包效果

function print(fn) {
    const a = 200
    fn()
}
const a = 100
function fn() {
    console.log(a)
}
print(fn)       // 100

for循環也是個很常見的閉包的例子

for (var i=1; i<=5; i++) {
 setTimeout( function timer() {
 console.log( i );
    }, i*1000 );
}

// 以每秒一次的頻率輸出五次 6

根據做用域的工做原理, 實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中, 所以實際上只有一個 i。

想要解決這個問題,咱們須要更多的閉包做用域, 特別是在循環的過程當中每一個迭代都須要一個閉包做用域。這讓我想到了每次迭代咱們都須要一個塊級做用域,咱們的好朋友let就派上了用場。

for (let i=1; i<=5; i++) {
 setTimeout( function timer() {
    console.log( i );
 }, i*1000 );
}
// 獲得了咱們想要的結果,每秒間隔輸出1,2,3,4,5

以上就是我關於做用域的理解啦~下篇文章再見哦~

參考:
什麼是做用域
JavaScript 做用域

相關文章
相關標籤/搜索