【跟著犀牛書(shū)復(fù)習(xí)JS基礎(chǔ)】作用域、作用域鏈、閉包和執(zhí)行上下文(this)

引言

可以說(shuō)是取名廢了,把這幾個(gè)關(guān)鍵詞放在一起也是因?yàn)榭赐晗?shū)相關(guān)章節(jié)和很多技術(shù)文章之后覺(jué)得這幾個(gè)概念是相互滲透的,需要放在一起理解。而在這之前,我對(duì)這幾個(gè)概念都是久聞其名,真正遇到了似懂非懂還很心虛,每每看完一篇零零散散的技術(shù)文章總覺(jué)得明白了,但是下一次遇到了還是不明白。這種其實(shí)充其量只能說(shuō)記住了一些知識(shí)點(diǎn)碎片,而學(xué)習(xí)不能只是碎片,必須是找到碎片與碎片之間的關(guān)聯(lián)與規(guī)律,才能將知識(shí)碎片連成完整的知識(shí)體系吃下去。所以,某位大佬說(shuō)得對(duì),有產(chǎn)出的學(xué)習(xí)才是真的學(xué)習(xí),畢竟如果不是真的搞清楚了,你什么也寫(xiě)不出來(lái),何況寫(xiě)一遍還能加深印象。如果你也有類(lèi)似的情況,請(qǐng)重視起來(lái),一起加油叭!

本章對(duì)標(biāo)犀牛書(shū)第6版3.9變量聲明、3.10變量作用域和第8大章8.6之前的章節(jié),之后兩小節(jié)值得在后面的文章里更詳細(xì)的單獨(dú)研究!

變量聲明和函數(shù)定義

你可能覺(jué)得這過(guò)于基礎(chǔ)了,但事實(shí)上如果忽略其中的一些細(xì)節(jié)會(huì)影響后面的理解。

變量聲明

我們知道在ES6之前,我們通常用var關(guān)鍵字來(lái)聲明一個(gè)或多個(gè)變量var i,sum;,還可以將變量的初始賦值和變量聲明寫(xiě)在一起var i = 0, j = 1, k = 2;,如果沒(méi)有在var聲明語(yǔ)句中給變量指定初始值,那么雖然聲明了變量,但在給他賦值之前,它的值就是undefined。

如果試圖讀取一個(gè)沒(méi)有聲明的變量,會(huì)觸發(fā)一個(gè)報(bào)錯(cuò)。在嚴(yán)格模式下,給一個(gè)未聲明的變量賦值也會(huì)觸發(fā)一個(gè)報(bào)錯(cuò),在非嚴(yán)格模式下,給一個(gè)未聲明的變量賦值,js實(shí)際上會(huì)給全局對(duì)象創(chuàng)建一個(gè)同名屬性,使其工作起來(lái)一個(gè)正常聲明的全局變量。

var聲明的屬性是不可配置的,也就是不可以用delete操作符刪除,而給未聲明的變量賦值創(chuàng)建的全局對(duì)象同名屬性是正常的可配置屬性,是可以被刪除的

函數(shù)定義

函數(shù)定義可以通過(guò)兩種方式來(lái)進(jìn)行:

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

雖然兩者都包含相同的函數(shù)名,都創(chuàng)建了一個(gè)新的函數(shù)對(duì)象,但還是有所區(qū)別,區(qū)別就體現(xiàn)在聲明提升的時(shí)候。

作用域和聲明提升

全局作用域和函數(shù)作用域

ES6之前,JS是沒(méi)有常用的不受約束的塊級(jí)作用域的,只有全局作用域和函數(shù)作用域。全局作用域是頂級(jí)作用域,顧名思義,在頂級(jí)聲明的所有變量全局中都可訪問(wèn);函數(shù)作用域是指在函數(shù)內(nèi)聲明的所有變量在函數(shù)體內(nèi)始終可見(jiàn),包括嵌套函數(shù)內(nèi)。以及,函數(shù)體內(nèi)的局部變量?jī)?yōu)先級(jí)高于同名的全局變量,如果函數(shù)內(nèi)聲明的全局變量或函數(shù)參數(shù)帶有的變量和全局變量同名,那么全局變量就被局部變量覆蓋。例如:

var scope = 'global';
function f() {
  var scope = 'local';
  console.log(scope); //local 同名的全局變量被局部變量覆蓋
  function f2() {
    console.log(scope); //local 函數(shù)體內(nèi)聲明的變量在嵌套函數(shù)內(nèi)也可見(jiàn)
  }
  f2();
}
f(); 

聲明提升

正常情況下我們認(rèn)為js語(yǔ)句是由上到下一句一句執(zhí)行的,這完全沒(méi)錯(cuò),但是有一種情況會(huì)使我們很迷惑。

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

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

實(shí)際上這是因?yàn)榘ㄗ兞亢秃瘮?shù)在內(nèi)的所有聲明(但不涉及賦值)都會(huì)在如何代碼被執(zhí)行前也就是js的預(yù)編譯階段被首先處理,會(huì)被”提升“到相應(yīng)作用域的頂部,這個(gè)過(guò)程就是聲明提升,關(guān)于聲明提升,我們只需要注意以下幾點(diǎn):

  1. 函數(shù)聲明也會(huì)被提升,但函數(shù)表達(dá)式不會(huì)。也就是說(shuō)上述兩種函數(shù)定義的方式,使用函數(shù)聲明語(yǔ)句,函數(shù)變量名和函數(shù)體均會(huì)提升,但使用var的表達(dá)式,只是變量聲明被提升,函數(shù)體會(huì)留在原來(lái)的位置等待執(zhí)行;

    f();
    
    function f(){
      console.log(a);  //會(huì)被執(zhí)行且輸出undefined
      var a = 2;
    }
    
    f(); //TypeError f is not a function
    var f = function bar() {
      console.log(a);  
      var a = 2;
    }
    
  2. var定義的具名的函數(shù)表達(dá)式,函數(shù)名變量也不會(huì)被提升;

    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 是因?yàn)樽兞棵呀?jīng)被提升但沒(méi)有被賦值,所以此時(shí)f是undefined,對(duì)undefined做調(diào)用執(zhí)行,因此拋TypeError異常。
    //這段代碼實(shí)際上被編譯成以下形式:
    var f;
    f(); //TypeError
    bar(); //ReferenceError
    f = function() {
      var bar = self;
      var a
      a = 2;
    }
    
  3. 函數(shù)聲明優(yōu)先,已知變量聲明和函數(shù)聲明都會(huì)被提升,如果同一作用域里有相同命名的函數(shù)聲明和變量聲明提升,函數(shù)聲明優(yōu)先。

    foo();  // foo2
    var foo = function() {
        console.log('foo1');
    }
    
    foo();  // foo1,foo重新賦值
    
    function foo() {
        console.log('foo2');
    }
    
    foo(); // foo1
    
    //這段代碼實(shí)際上被形容成
    function foo() {
      console.log('foo2');
    }
    foo();
    foo = function() {
      console.log('foo1');
    }
    foo();
    foo();
    //盡管var foo出現(xiàn)在function foo(){..}之前,但是函數(shù)聲明會(huì)被提升到普通變量聲明之前,因此重復(fù)的聲明會(huì)被忽略,但會(huì)被重新賦值。此外,后面的函數(shù)聲明還是可以覆蓋前面的,比如:
    foo(); // foo2
    function foo() {
      console.log('foo1');
    }
    function foo() {
      console.log('foo2');
    }
    
  4. 條件語(yǔ)句中的函數(shù)聲明:

    這個(gè)東西迷惑了我好久!??!浪費(fèi)我的時(shí)間!?。?/strong>

    MDN中的相關(guān)解釋?zhuān)?a target="_blank">https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function

    Segmentfault中回復(fù)樓的相關(guān)解釋?zhuān)?a target="_blank">https://segmentfault.com/q/1010000005337550

    以及下面的(【阮一峰】ES6入門(mén))中也有相關(guān)解釋?zhuān)?a target="_blank">【阮一峰】ES6入門(mén)

    foo();
    var a = true;
    if(a) {
     function foo(){console.log('foo1')}
    } else {
     function foo(){console.log('foo2')}
    }
    //理論上,這段代碼會(huì)輸出foo2
    //經(jīng)實(shí)驗(yàn),實(shí)際上在大多數(shù)瀏覽器控制臺(tái)里會(huì)拋出TypeError foo is not a function,在Safari里輸出了foo2
    
    (function () {
      if (false) {
        // 重復(fù)聲明一次函數(shù)f
        function f() { console.log('I am inside!'); }
      }
      f(); //TypeError foo is not a function
    }());
    
    //總之,在塊級(jí)作用域和條件語(yǔ)句中聲明函數(shù),在不同瀏覽器中解釋的標(biāo)準(zhǔn)都不一樣,考慮到環(huán)境導(dǎo)致的行為差異太大,不要在塊級(jí)作用域或條件語(yǔ)句中聲明函數(shù)!?。?// 等我有時(shí)間了再來(lái)糾結(jié)這個(gè)吧。。。。。。不過(guò)感覺(jué)遙遙無(wú)期。。。。。
    
    

塊級(jí)作用域

偷個(gè)懶~ 【阮一峰】ES6入門(mén)

注釋一點(diǎn):

絕大部分的文章里都提到在ES6之前,js中是沒(méi)有塊級(jí)作用域的,只有函數(shù)作用域和全局作用域,實(shí)際上,ES3中的try/catch語(yǔ)句中的catch語(yǔ)句就會(huì)創(chuàng)建一個(gè)塊級(jí)作用域

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

我試著在babel在線編譯器里轉(zhuǎn)換

{
  let a = 2;
  console.log(a);
}
console.log(a);

但是得到的是

"use strict";

{
var _a = 2;
console.log(_a);
}
console.log(a);

作用域鏈和閉包

作用域鏈

說(shuō)作用域鏈先理解一下自由變量:當(dāng)前作用域沒(méi)有定義的變量即自由變量。如何訪問(wèn)到自由變量:向父級(jí)作用域?qū)ふ摇H绻讣?jí)作用域沒(méi)有,則一層一層向上尋找,直到找到全局作用域,如果還是沒(méi)找到,則宣布放棄。

這個(gè)一層一層向上的層級(jí)關(guān)系,就是作用域鏈,它是一個(gè)對(duì)象列表。

只用記住一點(diǎn):自由變量將從作用域鏈中去尋找, 依據(jù)的是函數(shù)定義時(shí)的作用域鏈,而不是函數(shù)執(zhí)行時(shí),JS采用的是靜態(tài)作用域,依據(jù)的是函數(shù)定義時(shí)的位置。

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

閉包

當(dāng)函數(shù)可以記住并訪問(wèn)函數(shù)體內(nèi)的變量時(shí),就形成了閉包,因此嚴(yán)格來(lái)說(shuō)每個(gè)函數(shù)都是閉包。

注意三點(diǎn):

  1. 同一個(gè)作用域鏈中的多個(gè)閉包共享私有變量或變量

    function countFunc() {
     var funcs = [];
     for(var i = 0; i < 10; i++){
        funcs[i] = function (){ return i }
      }
      return funcs
    }
    countFunc()[0]() //10
    //這段代碼創(chuàng)建了10個(gè)閉包,但是都是在一個(gè)函數(shù)調(diào)用中定義的,因此它們共享變量i
    
  2. 每次調(diào)用js函數(shù)的時(shí)候,都會(huì)創(chuàng)建一個(gè)新的作用域鏈

    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關(guān)鍵字而不是變量,每一個(gè)函數(shù)調(diào)用都包含一個(gè)this值,如果閉包在外部函數(shù)中,是無(wú)法訪問(wèn)外部函數(shù)的this值的,arguments同理,雖然它不是關(guān)鍵字,但是調(diào)用函數(shù)時(shí)會(huì)自動(dòng)聲明它。

執(zhí)行上下文this

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

img

小結(jié)


有點(diǎn)曲折的一章,被條件語(yǔ)句和塊級(jí)作用域中的函數(shù)聲明搞得頭大。

犀牛書(shū)沒(méi)有把這一塊說(shuō)的很清楚,看了《你不知道的JS》相關(guān)章節(jié)作補(bǔ)充

后續(xù)需要補(bǔ)充:js編譯執(zhí)行機(jī)制,this詳解,面試題詳解

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容