【JavaScript】做用域 - 筆記

什麼是做用域

做用域是爲了在咱們使用變量引用以後,更方便的尋找到這些變量而制定的一套規則。程序員

簡單來講,做用域就是變量的使用範圍,且同一個做用域內的變量是惟一的。bash

做用域嵌套

做用域在實際的使用中,會互相的嵌套,因此咱們一般須要顧及多個做用域。閉包

當一個塊或者函數嵌套在另外一個塊或者函數中,就發生了做用域的嵌套。在當前做用域沒法找到某個變量時,引擎會在外層的做用域中尋找,逐級遞增出去,直到找到該變量或者已經抵達全局做用域。函數

最外層的做用域是全局做用域。工具

LHS和RHS

在理解做用域的時候,咱們還須要對LHSRHS有所瞭解。由於在變量還未聲明的狀況下,LHSRHS的查詢方式是不同的。性能

在未使用**「嚴格模式」**的狀況下,LHS在未找到目標變量時,會建立一個對應名稱的變量而後使用。而RHS只要未查詢到目標變量,就會直接報錯。學習

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

簡單理解。LHS查找的是容器,RHS查找的是容器裏面的內容。ui

例如: var a = 1spa

a是容器,咱們要將=1這個賦值,賦值到容器a上面,這個操做並不須要a本來容器裏面是什麼,不管是什麼都覆蓋掉便可。

console.log(a) 這裏的操做,須要將a容器裏面的值取出來而後打印出來。

詞法做用域

做用域共有兩種主要的工做模型。分別是最廣泛的詞法做用域和比較少見的動態做用域。

這裏咱們只討論詞法做用域。

JavaScript的做用域,就是詞法做用域。

大部分標準語言編譯器的第一個工做階段叫詞法化。詞法做用域就是定義在詞法階段的做用域。 因此詞法做用域就是由你寫代碼時的變量和做用域決定的。

隨便舉個例子:

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

}
foo(2); //2,4,12
複製代碼

上述例子中,出現了三個做用域,分別是:

  1. 全局做用域
  2. foo()方法內部的做用域
  3. bar()方法內部的做用域

JavaScript的做用域是嚴格包含的,沒有任何函數能夠部分地同時出如今兩個父級函數中。

欺騙詞法

JavaScript中,有兩種機制可讓代碼在運行的時候來「修改」(或者說欺騙)詞法做用域。

須要注意的是,欺騙詞法做用域會致使性能降低。

eval

JavaScript中的eval()函數能夠接受一個字符串爲參數,並將其中的內容視爲書寫時就存在於某個位置中的代碼。

舉個例子:

function foo(str,a){
    eval(str);
    console.log(a,b);
}
var b = 2;
foo( 'var b = 3;', 1 ); // 1,3
console.log(b); //2
複製代碼

能夠看到,這個例子中,eval()的參數爲var b = 3。在全局做用域中,自己已經將b變量的值聲明爲2。可是經過eval()方法,將foo()方法中所調用到的b參數的值,改成了3

在調用完eval()以後,咱們在全局做用域中,再次打印b參數的值,發現依舊是2

那麼咱們再看一個例子:

function foo(str){
    eval(str);
    console.log(a);
}

foo('var a = 2'); // 2

console.log(a); // ReferenceError: a is not defined
複製代碼

結合兩個例子咱們能夠看到,eval()方法的參數傳入的聲明,只會在調用對應方法的時候有效。實際做用域中並不會永久性的生成或者改變對應的聲明。 能夠理解爲臨時聲明。

with

JavaScript中,還可使用with關鍵字來欺騙詞法做用域。

with一般被當作重複引用同一個對象中的多個屬性的快捷方式,能夠不須要重複引用對象自己。

例子:

function foo(obj) {
    with (obj) {
        a = 2;
    }
}

var obj = {
	a:1
}

foo(obj);

console.log(obj) // {a:2}
複製代碼

在上述例子中,咱們能夠看到,調用with聲明以後,修改的內容會泄露到全局做用域上。、

with聲明其實是根據你傳遞的對象,憑空建立了一個全新的詞法做用域。

小結

詞法做用域意味着做用域是由書寫代碼時函數聲明的位置來決定的。編譯的詞法分析階段 基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它 們進行查找。 JavaScript 中有兩個機制能夠「欺騙」詞法做用域: eval(..) 和 with 。前者能夠對一段包 含一個或多個聲明的「代碼」字符串進行演算,並藉此來修改已經存在的詞法做用域(在 運行時)。後者本質上是經過將一個對象的引用看成做用域來處理,將對象的屬性看成做 用域中的標識符來處理,從而建立了一個新的詞法做用域(一樣是在運行時)。 這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎地認 爲這樣的優化是無效的。使用這其中任何一個機制都將致使代碼運行變慢。不要使用它們。

函數做用域和塊做用域

JavaScript的做用域,主要由函數做用域塊做用域組成。

函數中的做用域

JavaScript 具備基於函數的做用域,每建立一個函數,就會建立一個對應的做用域。

函數做用域的含義是指,屬於這個函數的所有變量均可以在整個函數的範圍內使用以及複用。

隱藏內部實現

對函數的傳統認知就是先聲明一個函數,而後再向裏面添加代碼。但反過來想也能夠帶來 一些啓示:從所寫的代碼中挑選出一個任意的片斷,而後用函數聲明對它進行包裝,實際 上就是把這些代碼「隱藏」起來了。

實際上,「隱藏」這個操做,遠比咱們想象的做用更大。

隱藏部分變量或者函數,符合最小受權或暴露原則。避免過多的變量向外泄露。

咱們須要遵照的一個原則是,儘可能讓變量或者函數,只讓其在須要使用的範圍內出現。

規避衝突

「隱藏」所帶來的另外一個好處,是能夠避免同名標識符之間的衝突,避免變量的值被意外覆蓋。

畢竟程序員煩惱的事情之一,是如何給衆多類似且重複的變量命名。

  1. 全局命名空間 變量衝突的一個典型例子存在於全局做用域中。當程序中加載了多個第三方庫時,若是它們沒有妥善地將內部私有的函數或變量隱藏起來,就會很容易引起衝突。 這些庫一般會在全局做用域中聲明一個名字足夠獨特的變量,一般是一個對象。這個對象被用做庫的命名空間,全部須要暴露給外界的功能都會成爲這個對象(命名空間)的屬性,而不是將本身的標識符暴漏在頂級的詞法做用域中。 例如:
var MyReallyCoolLibrary = {
    awesome: "stuff",
    doSomething: function() {
        // ...
    },
    doAnotherThing: function() {
        // ...
    }
}
複製代碼
  1. 模塊管理 另一種避免衝突的辦法和現代的模塊機制很接近,就是從衆多模塊管理器中挑選一個來使用。使用這些工具,任何庫都無需將標識符加入到全局做用域中,而是經過依賴管理器的機制將庫的標識符顯式地導入到另一個特定的做用域中。 顯而易見,這些工具並無可以違反詞法做用域規則的「神奇」功能。它們只是利用做用域的規則強制全部標識符都不能注入到共享做用域中,而是保持在私有、無衝突的做用域中,這樣能夠有效規避掉全部的意外衝突。 所以,只要你願意,即便不使用任何依賴管理工具也能夠實現相同的功效。

函數做用域

「隱藏」變量或函數,這個技術雖然能夠解決一些問題,可是並不理想。

首先必須聲明一個具名函數,其次咱們必須顯式的經過函數名去調用這個函數,才能夠運行其中的代碼。

爲此,JavaScript提供了能夠同時解決這兩個問題的方案。

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

使用這個寫法,函數會被當作函數表達式,而不是一個標準的函數聲明來處理。該寫法也被稱爲自動執行函數表達式。

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

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

匿名和具名

沒有名稱標識符的函數表達式,稱爲匿名函數表達式。反之,有名稱標識符的函數表達式,稱爲具名函數表達式

匿名函數表達式書寫起來簡單快捷,不少庫和工具也傾向鼓勵使用這種風格的代碼。可是 它也有幾個缺點須要考慮。

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

給函數表達式命名是一個最佳實踐。

塊級做用域

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

對於for循環,想必你們都不陌生。

咱們在 for 循環的頭部直接定義了變量 i ,一般是由於只想在 for 循環內部的上下文中使用 i ,而忽略了 i 會被綁定在外部做用域(函數或全局)中的事實。這就是塊做用域的所帶來的好處。而且變量的聲明應該距離使用的地方越近越好,並最大限度地本地化。

塊做用域是一個用來對以前的最小受權原則進行擴展的工具,將代碼從在函數中隱藏信息擴展爲在塊中隱藏信息。

閉包

閉包是基於詞法做用域書寫代碼時所產生的天然結果,並不須要爲了利用它們而有意識的建立閉包。

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,這就是閉包的效果。
複製代碼

在上述例子中,bar()函數被正常的執行,可是它是在本身定義的詞法做用域外執行的。 在foo()方法執行以後,引擎的垃圾回收器正常狀況下會將該方法銷燬以釋放內存。可是由於閉包的存在,bar()方法調用到了foo()的詞法做用域,因此垃圾回收器並無將foo()的內部銷燬。

閉包就是在定義的詞法做用域之外的地方被調用。

小結

當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這時就產生了閉包。 若是沒能認出閉包,也不瞭解它的工做原理,在使用它的過程當中就很容易犯錯,好比在循環中。但同時閉包也是一個很是強大的工具,能夠用多種形式來實現模塊等模式。

結語

做用域的使用在咱們的平常開發中隨處可見,靈活的應用和明確的瞭解本身所寫的代碼的做用域,能夠提到開發的效率。

同時,正確的使用相關知識,也能夠提到本身的代碼質量。

本篇內容關於閉包的內容較少,主要是由於幾個方面:

  • 閉包的概念若是想描述清楚,實屬困難。
  • 要驗證閉包的種種,須要代入大量的例子和分析,和做用域同一篇章的話,篇幅會過長。

因此本文僅僅只是簡單地提到了閉包的一些內容。

但願個人文章能被大家所喜歡,也但願如有不足之處,大佬們能一一點出,謝謝。

參考

本文內容,爲學習《你不知道的JavaScript》上卷的第一部分【做用域與閉包】後所產出的筆記文章。有興趣的小夥伴能夠直接查看原書籍。

相關文章
相關標籤/搜索