關於javascript中做用域的碎碎念

做用域是什麼

幾乎全部編程語言最基本的功能之一,就是可以儲存變量當中的值,而且能在以後對這個 值進行訪問或修改。事實上,正是這種儲存和訪問變量的值的能力將狀態帶給了程序。
接下來咱們介紹兩個概念: LHS 查詢和 RHS查詢
我打賭你必定能猜到「L」和「R」的含義,它們分別表明左側和右側。
什麼東西的左側和右側?是一個賦值操做的左側和右側。
換句話說,當變量出如今賦值操做的左側時進行 LHS 查詢,出如今右側時進行 RHS 查詢。編程

講得更準確一點,RHS 查詢與簡單地查找某個變量的值別無二致,而 LHS 查詢則是試圖找到變量的容器自己,從而能夠對其賦值。從這個角度說,RHS 並非真正意義上的「賦 值操做的右側」,更準確地說是「非左側」。編程語言

你能夠將 RHS 理解成 retrieve his source value(取到它的源值),這意味着「獲得某某的 值」。
讓咱們繼續深刻研究。
請看如下代碼:
console.log( a );函數

其中對 a 的引用是一個 RHS 引用,由於這裏 a 並無賦予任何值。相應地,須要查找並取得 a 的值,這樣才能將值傳遞給 console.log(..)。性能

相比之下,例如:優化

a = 2;code

這裏對 a 的引用則是 LHS 引用,由於實際上咱們並不關心當前的值是什麼,只是想要爲 =2 這個賦值操做找到一個目標。對象

LHS 和 RHS 的含義是「賦值操做的左側或右側」並不必定意味着就是「= 賦值操做符的左側或右側」。賦值操做還有其餘幾種形式,所以在概念上最 好將其理解爲「賦值操做的目標是誰(LHS)」以及「誰是賦值操做的源頭(RHS)」。ip

做用域是一套規則,用於肯定在何處以及如何查找變量(標識符)。若是查找的目的是對變量進行賦值,那麼就會使用 LHS 查詢;若是目的是獲取變量的值,就會使用 RHS 查詢。作用域

賦值操做符會致使 LHS 查詢。=操做符或調用函數時傳入參數的操做都會致使關聯做用域 的賦值操做。字符串

JavaScript引擎首先會在代碼執行前對其進行編譯,在這個過程當中,像var a = 2這樣的聲 明會被分解成兩個獨立的步驟:

1. 首先,var a 在其做用域中聲明新變量。這會在最開始的階段,也就是代碼執行前進行。
2. 接下來,a = 2 會查詢(LHS 查詢)變量 a 並對其進行賦值。

不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處 的位置決定。

若是詞法做用域徹底由寫代碼期間函數所聲明的位置來定義,怎樣才能在運行時來「修改」(也能夠說欺騙)詞法做用域呢?

JavaScript 中有兩種機制來實現這個目的。

eval和with

JavaScript 中的 eval(..) 函數能夠接受一個字符串爲參數,並將其中的內容視爲好像在書 寫時就存在於程序中這個位置的代碼。換句話說,能夠在你寫的代碼中用程序生成代碼並 運行,就好像代碼是寫在那個位置的同樣。

在執行 eval(..) 以後的代碼時,引擎並不「知道」或「在乎」前面的代碼是以動態形式插 入進來,並對詞法做用域的環境進行修改的。引擎只會如往常地進行詞法做用域查找。

eval(..) 調用中的 "var b = 3;" 這段代碼會被看成原本就在那裏同樣來處理。因爲那段代 碼聲明瞭一個新的變量 b,所以它對已經存在的 foo(..) 的詞法做用域進行了修改。事實 上,和前面提到的原理同樣,這段代碼實際上在 foo(..) 內部建立了一個變量 b,並遮蔽 了外部(全局)做用域中的同名變量。

當 console.log(..) 被執行時,會在 foo(..) 的內部同時找到 a 和 b,可是永遠也沒法找到 外部的 b。所以會輸出「1, 3」而不是正常狀況下會輸出的「1, 2」。

JavaScript中 還 有 其 他 一 些 功 能 效 果 和eval(..)很 相 似。setTimeout(..)和 setInterval(..) 的第一個參數能夠是字符串,字符串的內容能夠被解釋爲一段動態生成的 函數代碼。這些功能已通過時且並不被提倡。不要使用它們!

在程序中動態生成代碼的使用場景很是罕見,由於它所帶來的好處沒法抵消性能上的損 失。


小結

詞法做用域意味着做用域是由書寫代碼時函數聲明的位置來決定的。編譯的詞法分析階段 基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它 們進行查找。

JavaScript 中有兩個機制能夠「欺騙」詞法做用域:eval(..) 和 with。前者能夠對一段包 含一個或多個聲明的「代碼」字符串進行演算,並藉此來修改已經存在的詞法做用域(在 運行時)。後者本質上是經過將一個對象的引用看成做用域來處理,將對象的屬性看成做 用域中的標識符來處理,從而建立了一個新的詞法做用域(一樣是在運行時)。

這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎地認 爲這樣的優化是無效的。使用這其中任何一個機制都將致使代碼運行變慢。不要使用它們。

函數做用域

var a = 2;
function foo() { // <-- 添加這一行
var a = 3; console.log( a ); // 3
} // <-- 以及這一行 foo(); // <-- 以及這一行
     console.log( a ); // 2

雖然這種技術能夠解決一些問題,可是它並不理想,由於會致使一些額外的問題。首先, 必須聲明一個具名函數 foo(),意味着 foo 這個名稱自己「污染」了所在做用域(在這個 例子中是全局做用域)。其次,必須顯式地經過函數名(foo())調用這個函數才能運行其 中的代碼。

var a = 2;
(function foo(){ // <-- 添加這一行 var a = 3;

console.log( a ); })(); // <-- 以及這一行  3
console.log( a ); // 2

區分函數聲明和表達式最簡單的方法是看 function 關鍵字出如今聲明中的位 置(不只僅是一行代碼,而是整個聲明中的位置)。若是 function 是聲明中 的第一個詞,那麼就是一個函數聲明,不然就是一個函數表達式。

函數聲明和函數表達式之間最重要的區別是它們的名稱標識符將會綁定在何處。

(function foo(){ .. })做爲函數表達式意味着foo只能在..所表明的位置中 被訪問,外部做用域則不行。foo 變量名被隱藏在自身中意味着不會非必要地污染外部做 用域。

var a = 2;

(function foo() { var a = 3;

 console.log( a ); // 3
      })(); 

 console.log( a ); // 2

因爲函數被包含在一對 ( ) 括號內部,所以成爲了一個表達式,經過在末尾加上另一個 ( ) 能夠當即執行這個函數,好比 (function foo(){ .. })()。第一個 ( ) 將函數變成表 達式,第二個 ( ) 執行了這個函數。

社區給它規定了一個術語:IIFE,表明當即執行函數表達式

相較於傳統的 IIFE 形式,不少人都更喜歡另外一個改進的形式:(function(){ .. }())。仔 細觀察其中的區別。第一種形式中函數表達式被包含在 ( ) 中,而後在後面用另外一個 () 括 號來調用。第二種形式中用來調用的 () 括號被移進了用來包裝的 ( ) 括號中。

這兩種形式在功能上是一致的。選擇哪一個全憑我的喜愛。

好啦,今天就碎碎唸到這裏啦,端午安康啊everybody~
相關文章
相關標籤/搜索