天天看點

第一部分:作用域和閉包

1. 關于作用域

1.1 作用域是什麼

js 中每個函數都有自己的作用域,作用域内部可以通路外部(向上查找),外部無法通路内部,作用域可以嵌套多層

function foo(){
		var a = 2;
		console.log(a); // 2
	}
	foo();
	console.log(a); // // ReferenceError: a is not defined
           

1.2 詞法作用域

這裡需要先了解一下編譯器編譯代碼的原理:

在傳統編譯語言的流程中,程式中的一段源代碼在執行之前會經曆三個步驟,統稱為“編譯”。

  1. 分詞 / 詞法分析

    解釋:

    這個過程會将由字元組成的字元串分解成(對程式設計語言來說)有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。

    例如,考慮程式 var a = 2;。這段程式通常會被分解成為下面這些詞法單元:var、a、=、2 、;。空格是否會被當作詞法單元,取決于空格在這門語言中是否具有意義。

    這裡要注意:

    分詞(tokenizing)和詞法分析(Lexing)之間的差別是非常微妙、晦澀的,主要差異在于詞法單元的識别是通過有狀态還是無狀态的方式進行的。簡單來說,如果詞法單元生成器在判斷 a 是一個獨立的詞法單元還是其他詞法單元的一部分時,調用的是有狀态的解析規則,那麼這個過程就被稱為詞法分析。

  2. 解析 / 文法解析

    解釋:

    這個過程是将詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的代表了程式文法結構的樹。這個樹被稱為“抽象文法樹”(Abstract Syntax Tree,AST)。

    var a = 2; 的抽象文法樹中可能會有一個叫作 VariableDeclaration 的頂級節點,接下來是一個叫作 Identifier(它的值是 a)的子節點,以及一個叫作 AssignmentExpression的子節點。AssignmentExpression 節點有一個叫作 NumericLiteral(它的值是 2)的子節點。

  3. 代碼生成

    解釋:

    AST 轉換為可執行代碼的過程稱被稱為代碼生成。這個過程與語言、目标平台等息息相關。

    抛開具體細節,簡單來說就是有某種方法可以将 var a = 2; 的 AST 轉化為一組機器指令,用來建立一個叫作 a 的變量(包括配置設定記憶體等),并将一個值儲存在 a 中。

詞法作用域:

簡單地說,詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫代碼時變量和塊作用域寫在哪裡來決定的,是以當詞法分析器處理代碼時會保持作用域不變(大部分情況下是這樣的)。

2. 閉包

之前我對閉包的了解就是,函數外部可以通路函數内部的變量,但是這并不完全正确

例:

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

通過這種方式可以從函數外部拿到函數内部的變量,但是這并不能稱之為閉包。

什麼才是閉包:

當函數可以記住并通路所在的詞法作用域時,就産生了閉包,即使函數是在目前詞法作用域之外執行。

引自:《你不知道的JavaScript》

閉包:

function foo() {
		var a = 2;
		function bar() {
			console.log( a );
		}
		return bar;
	}
	var baz = foo();
	baz(); // 2 —— 朋友,這就是閉包
           

解釋:

函數 bar() 的詞法作用域能夠通路 foo() 的内部作用域。然後我們将 bar() 函數本身當作一個值類型進行傳遞。在這個例子中,我們将 bar 所引用的函數對象本身當作傳回值。
在 foo() 執行後,其傳回值(也就是内部的 bar() 函數)指派給變量 baz 并調用 baz(),實際上隻是通過不同的辨別符引用調用了内部的函數 bar()。
bar() 顯然可以被正常執行。但是在這個例子中,它在自己定義的詞法作用域以外的地方執行。
在 foo() 執行後,通常會期待 foo() 的整個内部作用域都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的記憶體空間。由于看上去 foo() 的内容不會再被使用,是以很自然地會考慮對其進行回收。
而閉包的“神奇”之處正是可以阻止這件事情的發生。事實上内部作用域依然存在,是以沒有被回收。誰在使用這個内部作用域?原來是 bar() 本身在使用。

拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 内部作用域的閉包,使得該作用域能夠一直存活,以供 bar() 在之後任何時間進行引用。

bar() 依然持有對該作用域的引用,而這個引用就叫作閉包。

新的了解:

函數的作用域在函數執行完畢後未被垃圾回收機制回收,作用域所占用的記憶體空間仍然存在,并且可以被通路時,就産生了閉包,也可以解釋為什麼不能濫用閉包,因為他會占用過多的記憶體。

注:

  1. 文中若有錯誤,請及時向作者反應
  2. 文中有很多部分引用了《你不知道的JavaScript》該書的内容,推薦大家閱讀原書

繼續閱讀