【跟着犀牛書複習JS基礎】做用域、做用域鏈、閉包和執行上下文(this)

引言javascript

能夠說是取名廢了,把這幾個關鍵詞放在一塊兒也是由於看完犀牛書相關章節和不少技術文章以後以爲這幾個概念是相互滲透的,須要放在一塊兒理解。而在這以前,我對這幾個概念都是久聞其名,真正遇到了似懂非懂還很心虛,往往看完一篇零零散散的技術文章總以爲明白了,可是下一次遇到了仍是不明白。這種其實充其量只能說記住了一些知識點碎片,而學習不能只是碎片,必須是找到碎片與碎片之間的關聯與規律,才能將知識碎片連成完整的知識體系吃下去。因此,某位大佬說得對,有產出的學習纔是真的學習,畢竟若是不是真的搞清楚了,你什麼也寫不出來,況且寫一遍還能加深印象。若是你也有相似的狀況,請重視起來,一塊兒加油叭!java

本章對標犀牛書第6版3.9變量聲明、3.10變量做用域和第8大章8.6以前的章節,以後兩小節值得在後面的文章裏更詳細的單獨研究!es6

變量聲明和函數定義

你可能以爲這過於基礎了,但事實上若是忽略其中的一些細節會影響後面的理解。面試

變量聲明

咱們知道在ES6以前,咱們一般用var關鍵字來聲明一個或多個變量var i,sum;,還能夠將變量的初始賦值和變量聲明寫在一塊兒var i = 0, j = 1, k = 2;,若是沒有在var聲明語句中給變量指定初始值,那麼雖然聲明瞭變量,但在給他賦值以前,它的值就是undefined。segmentfault

若是試圖讀取一個沒有聲明的變量,會觸發一個報錯。在嚴格模式下,給一個未聲明的變量賦值也會觸發一個報錯,在非嚴格模式下,給一個未聲明的變量賦值,js實際上會給全局對象建立一個同名屬性,使其工做起來一個正常聲明的全局變量。瀏覽器

var聲明的屬性是不可配置的,也就是不能夠用delete操做符刪除,而給未聲明的變量賦值建立的全局對象同名屬性是正常的可配置屬性,是能夠被刪除的babel

函數定義

函數定義能夠經過兩種方式來進行:閉包

  • 函數定義表達式 var func = function(){...};
  • 函數聲明function func(){...}

雖然二者都包含相同的函數名,都建立了一個新的函數對象,但仍是有所區別,區別就體如今聲明提高的時候。ide

做用域和聲明提高

全局做用域和函數做用域

ES6以前,JS是沒有經常使用的不受約束的塊級做用域的,只有全局做用域和函數做用域。全局做用域是頂級做用域,顧名思義,在頂級聲明的全部變量全局中均可訪問;函數做用域是指在函數內聲明的全部變量在函數體內始終可見,包括嵌套函數內。以及,函數體內的局部變量優先級高於同名的全局變量,若是函數內聲明的全局變量或函數參數帶有的變量和全局變量同名,那麼全局變量就被局部變量覆蓋。例如:函數

var scope = 'global';
function f() {
  var scope = 'local';
  console.log(scope); //local 同名的全局變量被局部變量覆蓋
  function f2() {
    console.log(scope); //local 函數體內聲明的變量在嵌套函數內也可見
  }
  f2();
}
f(); 
複製代碼

聲明提高

正常狀況下咱們認爲js語句是由上到下一句一句執行的,這徹底沒錯,可是有一種狀況會使咱們很迷惑。

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

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

實際上這是由於包括變量和函數在內的全部聲明(但不涉及賦值)都會在如何代碼被執行前也就是js的預編譯階段被首先處理,會被」提高「到相應做用域的頂部,這個過程就是聲明提高,關於聲明提高,咱們只須要注意如下幾點:

  1. 函數聲明也會被提高,但函數表達式不會。也就是說上述兩種函數定義的方式,使用函數聲明語句,函數變量名和函數體均會提高,但使用var的表達式,只是變量聲明被提高,函數體會留在原來的位置等待執行;

    f();
    
    function f(){
      console.log(a);  //會被執行且輸出undefined
      var a = 2;
    }
    
    f(); //TypeError f is not a function
    var f = function bar() {
      console.log(a);  
      var a = 2;
    }
    複製代碼
  2. var定義的具名的函數表達式,函數名變量也不會被提高;

    f(); //TypeError f is not a function
    bar(); //ReferenceError: bar is not defined
    var f = function bar() {
      console.log(a);  
      var a = 2;
    }
    //注意這裏f()拋出的是TypeError而不是ReferenceError 是由於變量名已經被提高但沒有被賦值,因此此時f是undefined,對undefined作調用執行,所以拋TypeError異常。
    //這段代碼實際上被編譯成如下形式:
    var f;
    f(); //TypeError
    bar(); //ReferenceError
    f = function() {
      var bar = self;
      var a
      a = 2;
    }
    複製代碼
  3. 函數聲明優先,已知變量聲明和函數聲明都會被提高,若是同一做用域裏有相同命名的函數聲明和變量聲明提高,函數聲明優先。

    foo();  // foo2
    var foo = function() {
        console.log('foo1');
    }
    
    foo();  // foo1,foo從新賦值
    
    function foo() {
        console.log('foo2');
    }
    
    foo(); // foo1
    
    //這段代碼實際上被形容成
    function foo() {
      console.log('foo2');
    }
    foo();
    foo = function() {
      console.log('foo1');
    }
    foo();
    foo();
    //儘管var foo出如今function foo(){..}以前,可是函數聲明會被提高到普通變量聲明以前,所以重複的聲明會被忽略,但會被從新賦值。此外,後面的函數聲明仍是能夠覆蓋前面的,好比:
    foo(); // foo2
    function foo() {
      console.log('foo1');
    }
    function foo() {
      console.log('foo2');
    }
    複製代碼
  4. 條件語句中的函數聲明:

    這個東西迷惑了我很久!!!浪費個人時間!!!

    MDN中的相關解釋:developer.mozilla.org/zh-CN/docs/…

    Segmentfault中回覆樓的相關解釋:segmentfault.com/q/101000000…

    以及下面的(【阮一峯】ES6入門)中也有相關解釋:【阮一峯】ES6入門

    foo();
    var a = true;
    if(a) {
    	function foo(){console.log('foo1')}
    } else {
    	function foo(){console.log('foo2')}
    }
    //理論上,這段代碼會輸出foo2
    //經實驗,實際上在大多數瀏覽器控制檯裏會拋出TypeError foo is not a function,在Safari裏輸出了foo2
    
    (function () {
      if (false) {
        // 重複聲明一次函數f
        function f() { console.log('I am inside!'); }
      }
      f(); //TypeError foo is not a function
    }());
    
    //總之,在塊級做用域和條件語句中聲明函數,在不一樣瀏覽器中解釋的標準都不同,考慮到環境致使的行爲差別太大,不要在塊級做用域或條件語句中聲明函數!!!
    // 等我有時間了再來糾結這個吧。。。。。。不過感受遙遙無期。。。。。
    
    複製代碼

塊級做用域

偷個懶~ 【阮一峯】ES6入門

註釋一點:

絕大部分的文章裏都提到在ES6以前,js中是沒有塊級做用域的,只有函數做用域和全局做用域,實際上,ES3中的try/catch語句中的catch語句就會建立一個塊級做用域

try {
	throw 2
} catch (a) {
	console.log(a); //2
}
console.log(a);//Reference Error
複製代碼

我試着在babel在線編譯器裏轉換

{
	let a = 2;
	console.log(a);
}
console.log(a);
複製代碼

可是獲得的是

"use strict";

{
var _a = 2;
console.log(_a);
}
console.log(a);
複製代碼

做用域鏈和閉包

做用域鏈

說做用域鏈先理解一下自由變量:當前做用域沒有定義的變量即自由變量。如何訪問到自由變量:向父級做用域尋找。若是父級做用域沒有,則一層一層向上尋找,直到找到全局做用域,若是仍是沒找到,則宣佈放棄。

這個一層一層向上的層級關係,就是做用域鏈,它是一個對象列表。

只用記住一點:自由變量將從做用域鏈中去尋找, 依據的是函數定義時的做用域鏈,而不是函數執行時

function F1() {
    var a = 100
    return function () {
        console.log(a)
    }
}
var f1 = F1()
var a = 200
f1() //100
複製代碼

閉包

當函數能夠記住並訪問函數體內的變量時,就造成了閉包,所以嚴格來講每一個函數都是閉包。

注意三點:

  1. 同一個做用域鏈中的多個閉包共享私有變量或變量

    function countFunc() {
    	var funcs = [];
    	for(var i = 0; i < 10; i++){
        funcs[i] = function (){ return i }
      }
      return funcs
    }
    countFunc()[0]() //10
    //這段代碼建立了10個閉包,可是都是在一個函數調用中定義的,所以它們共享變量i
    複製代碼
  2. 每次調用js函數的時候,都會建立一個新的做用域鏈

    function counter() {
      var n = 0;
      return {
        counter: function() { return n++; },
        reset: function() { n = 0; }
      }
    }
    var c = counter();
    var d = counter();
    c.counter(); //0
    d.counter(); //0 互不干擾
    c.reset();  //重置c
    c.counter(); //c的reset和counter共享n變量
    複製代碼
  3. this是JS關鍵字而不是變量,每個函數調用都包含一個this值,若是閉包在外部函數中,是沒法訪問外部函數的this值的,arguments同理,雖然它不是關鍵字,可是調用函數時會自動聲明它。

執行上下文this

記住大佬作的一張圖就夠了(侵刪):

img

小結


有點曲折的一章,被條件語句和塊級做用域中的函數聲明搞得頭大。

後續須要補充:js編譯執行機制,this詳解,面試題詳解

相關文章
相關標籤/搜索