做用域、做用域鏈及閉包(一)

正文

做用域是什麼

做用域是一套規則,用於肯定在何處以及如何查找變量。前端

瞭解做用域以前還要簡單瞭解一下編譯原理: 書中(你不知道的js)說道JavaScript是一門編譯語言。在傳統編譯語言的流程中,程序中一段源代碼在執行以前會經歷三個步驟,統稱爲「編譯」。編程

  • 分詞/詞法分析將字符串分解成有意義的代碼塊,代碼塊又稱詞法單元。
  • 解析/語法分析將詞法單元流轉換成一個由元素逐級嵌套所組成的表明了程序語法接口的數,又稱「抽象語法樹」。
  • 代碼生成:將抽象語法樹轉換爲機器可以識別的指令。

理解做用域

做用域與編譯器、引擎進行配合完成代碼的解析bash

書中舉了個例子 對於 var a = 2 這條語句,首先編譯器會將其分爲兩部分,一部分是 var a,一部分是 a = 2。編譯器會在編譯期間執行 var a,而後到做用域中去查找 a 變量,若是 a 變量在做用域中尚未聲明,那麼就在做用域中聲明 a 變量,若是 a 變量已經存在,那就忽略 var a 語句。而後編譯器會爲 a = 2 這條語句生成執行代碼,以供引擎執行該賦值操做。因此咱們平時所提到的變量提高,其實就是利用這個先聲明後賦值的原理。架構

做用域的工做模式

做用域共有兩種主要的工做模型。第一種是最爲廣泛的,被大多數編程語言所採用的詞法做用域( JavaScript中的做用域就是詞法做用域)。另一種是動態做用域,仍有一些編程語言在使用(好比Bash腳本、Perl中的一些模式等)。編程語言

異常狀況

對於 var a = 10 這條賦值語句,其實是爲了查找變量 a, 而且將 10 這個數值賦予它,這就是 LHS 查詢。 對於 console.log(a) 這條語句,其實是爲了查找 a 的值並將其打印出來,這是 RHS 查詢。函數

爲何區分 LHS 和 RHS 是一件重要的事情?工具

在非嚴格模式下,LHS 調用查找不到變量時會建立一個全局變量,RHS 查找不到變量時會拋出 ReferenceError。 在嚴格模式下,LHS 和 RHS 查找不到變量時都會拋出 ReferenceError。學習

詞法做用域

詞法做用域是一套關於引擎如何尋找變量以及會在何處找到變量的規則。詞法做用域最重要的特徵是它的定義過程發生在代碼的書寫階段(假設沒有使用 eval() 或 with )ui

舉個🌰spa

function testA() {
  console.log(a);  // 2
}

function testB() {
  var a = 3;
  testA();
}

var a = 2;

testB()
詞法做用域讓testA()中的a經過RHS引用到了全局做用域中的a,所以會輸出2。
複製代碼

動態做用域

動態做用域只關心它們從何處調用。換句話說,做用域鏈是基於調用棧的,而不是代碼中的做用域嵌套。

// 所以,若是 JavaScript 具備動態做用域,理論上,下面代碼中的 testA() 在執行時將會輸出3。
function testA() {
  console.log(a);  // 3
}

function testB() {
  var a = 3;
  testA();
}

var a = 2;

testB()
複製代碼

函數做用域

具名與匿名

書中舉了個例子->回調函數

setTimeout( function() {
  console.log("我等了很久!")
}, 1000 )
複製代碼

其實這個叫函數匿名錶達式,函數表達式能夠匿名,而函數聲明則不能夠省略函數名。匿名函數表達式書寫起來簡單快捷,不少庫和工具也傾向鼓勵使用這種風格的代碼。但它也有幾個缺點須要考慮。

  • 匿名函數在棧追蹤中不會顯示出有意義的函數名,使得調試很困難。
  • 若是沒有函數名,當函數須要引用自身時只能使用已通過期的 arguments.callee 引用,好比在遞歸中。另外一個函數須要引用自身的例子,是在事件觸發後事件監聽器須要解綁自身。
  • 匿名函數省略了對於代碼可讀性/可理解性很重要的函數名。一個描述性的名稱可讓代碼不言自明。

針對於這種缺點,書中給出了建議:給函數表達式命名

setTimeout( function timeoutHandler() {
  console.log("我等了很久!")
}, 1000 )
複製代碼

提高

先提出個問題,現有賦值仍是先有聲明

a = 2;

var a;

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

等價於

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //undefined
複製代碼

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

這意味着不管做用域中的聲明出如今什麼地方,都將在代碼自己被執行前首先進行處理。能夠將這個過程形象地想象成全部的聲明(變量和函數)都會被「移動」到各自做用域的最頂端,這個過程稱爲提高。

因此能夠看出先有聲明後有賦值

再看個小🌰

foo();  // TypeError
bar();  // ReferenceError

var foo = function bar() {
  // ...
};
複製代碼

這個代碼片斷通過提高後,實際上會被理解爲如下形式:

var foo;
foo();  // TypeError
bar();  // ReferenceError

foo = function() {
  var bar = ...self...
  // ...
};
複製代碼

這段程序中的變量標識符 foo() 被提高並分配給全局做用域,所以 foo() 不會致使 ReferenceError。可是foo此時並無賦值(若是它是一個函數聲明而不是函數表達式就會賦值)。foo()因爲對 undefined 值進行函數調用而致使非法操做,所以拋出 TypeError 異常。另外即時是具名的函數表達式,名稱標識符(這裏是 bar )在賦值以前也沒法在所在做用域中使用。

結語

但願你們都能找到適合本身的學習方法重學前端,完善本身的知識體系架構~

相關文章
相關標籤/搜索