《你不知道的JavaScript》之作用域和閉包

一、LHS和RHS查詢

當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí),進(jìn)行LSH查詢;當(dāng)變量出現(xiàn)在賦值操作的右側(cè),即非左側(cè)時(shí),進(jìn)行RSH查詢。
或者說(shuō)
LSH查詢是在找變量容器的本身,從而對(duì)其賦值,RHS是查找某個(gè)變量的值
現(xiàn)在我們就要舉很多例子來(lái)看看

console.log(b)
a = 2

console.log(...)需要一個(gè)引用才能執(zhí)行,查是否有console,一次RSH,再?gòu)腸onsole對(duì)象中查是否有l(wèi)og方法,一次RHS。要打印出b,那就要查找b的值,這是一個(gè)RSH;為a賦值的時(shí)候,則要先找到a這個(gè)容器然后再賦值操作,這是一個(gè)LSH。


function foo(a) {
    console.log(a);
}
foo(1)

② 對(duì)foo調(diào)用,就要去找foo的值,一次RSH查詢;
實(shí)參到形參,a=2的操作,一次LSH查詢;
打印a的時(shí)候,查詢?nèi)纰僦兴觯?/p>


function foo(a) {
    var b = a;
    return a + b;
}
var c = foo(1)

③ 查詢foo,一次RHS
為c賦值操作c = ...,一次LHS
傳參賦值操作a=...,一次LHS
查詢a的值,一次RHS
為b進(jìn)行賦值b=...,一次LHS
查詢a和b的值,兩次RHS


二、作用域

作用域是根據(jù)名稱查找變量的一套規(guī)則,從當(dāng)前的范圍逐層往上查找,如下圖所示,LSHRSH引用都會(huì)在當(dāng)前作用域往上查。

來(lái)自你不知道的JavaScript

RHS在任何相關(guān)的作用域都取不到變量的值時(shí),則會(huì)拋出ReferenceError異常。如果查到了這個(gè)變量,但是你對(duì)他進(jìn)行了不合理的操作,比如:
① 對(duì)非函數(shù)進(jìn)行調(diào)用
② 引用null或者undefined的屬性
則會(huì)拋出TypeError異常
LHS在頂層的作用域都找不到變量的話,如果是非嚴(yán)格模式,則會(huì)幫我們默認(rèn)創(chuàng)建一個(gè),默認(rèn)值為undefined,如果是嚴(yán)格模式,那就不好意思了,一樣會(huì)拋出ReferenceError異常。

三、提升

(1)變量的提升

a = 1;
var a;
console.log(a)  >>> 1

console.log(b)  >>> undefined
var b = 2;     

var a的聲明被提前了,所以能成功賦值,也能成功打印出來(lái)
var b = 2可以看成是①var b; ② b =2;第一步聲明被提前了,第二步則留在原地,等到執(zhí)行打印時(shí)候,b還未被賦值,所以是undefined。
總結(jié):聲明會(huì)提升,賦值或者其他運(yùn)行邏輯會(huì)留在原地
(2)函數(shù)的提升

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

等價(jià)于:

function foo() {
    var a; 
    console.log( a ); >>> undefined 
    a = 2; 
}
foo();

foo是一個(gè)函數(shù)的聲明,則會(huì)將整個(gè)函數(shù)提升


但是如果是一個(gè)表達(dá)式

foo();  // TypeError!
bar();  // referenceError
var foo = function bar() { 
    // ... 
};

因?yàn)?code>var foo會(huì)被先執(zhí)行,則foo未定義,而被當(dāng)作函數(shù)調(diào)用,則會(huì)出現(xiàn)TypeError異常,而且此時(shí)bar在賦值給foo之前,也不能在之前使用。
等價(jià)于:

var foo; 
foo(); // TypeError 
bar(); // ReferenceError 
foo = function() {
    var bar = ... 
    // ... 
}

舉個(gè)例子:

foo(); // ->>> 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 ); 
};

foo(); // ->>>3
function foo() { 
    console.log( 1 ); 
}
var foo = function() { 
    console.log( 2 ); 
};
function foo() { 
    console.log( 3 ); 
}

第一段代碼:對(duì) foo函數(shù)的定義會(huì)被提前聲明,而后面的var foo 重復(fù)定義則會(huì)被忽略,所以打印的是1
第二段代碼:同樣是函數(shù)提升,但是后面定義的會(huì)覆蓋前面的,所以是打印出3

四、作用域閉包

函數(shù)與對(duì)其狀態(tài)即詞法環(huán)境lexical environment)的引用共同構(gòu)成閉包closure)。也就是說(shuō),閉包可以讓你從內(nèi)部函數(shù)訪問(wèn)外部函數(shù)作用域。在JavaScript,函數(shù)在每次創(chuàng)建時(shí)生成閉包。

來(lái)自MDN文檔
那我們就來(lái)看看:
① 變量間接傳遞

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

首先調(diào)用了foo,可以看到這個(gè)函數(shù)的返回值也是一個(gè)函數(shù)bar,bar函數(shù)打印了foo作用域內(nèi)的a的值。調(diào)用baz時(shí)候,可以正常打印出a的值。這樣就間接訪問(wèn)到a了。
② 函數(shù)間接傳遞

var fn;
function foo() {
    var a = 2;
    function baz() { 
        console.log( a ); 
    }
    fn = baz; 
}
function bar() { 
    fn(); 
}
foo(); 
bar(); // 2

foo被調(diào)用時(shí),給fn全局變量賦值了,是名為bazfoo內(nèi)部函數(shù)。調(diào)用bar,間接調(diào)用了fn,由于fn又指向了baz函數(shù),間接調(diào)用了baz,而他又能訪問(wèn)到a,即可成功打印。
可能引發(fā)的問(wèn)題:

  • 引用變量問(wèn)題

    我們用得最多的,當(dāng)然是for循環(huán)了,那么請(qǐng)看這個(gè)經(jīng)典的例子

function test() {
      var result = [];
      for (var i = 0; i < 10; i++) {
        result[i] = function () {
            console.info(i)
        }
     }
     return result
}
test()[0]() // >>> 10

result是一個(gè)包含十個(gè)函數(shù)的數(shù)組,按照預(yù)要分別打印0-9,但是我們?nèi)〉谝粋€(gè)函數(shù)執(zhí)行,結(jié)果卻是10。那是因?yàn)槊總€(gè)result里面的閉包函數(shù),訪問(wèn)的i都是test函數(shù)作用域內(nèi)的i,循環(huán)結(jié)束后,a=10,所以每個(gè)閉包函數(shù)都是訪問(wèn)到a=10時(shí)候的值。
解決方法:

function test () {
      var result = []
      for (var i = 0; i < 10; i++) {
        result[i] = (function (j) {
          return function () {
            console.log(j)
          }
        }(i))
      }
      return result
},
this.test()[0]() // >>> 0

我們可以把i的值賦值給內(nèi)層的j,這樣打印的j就是每個(gè)函數(shù)所形成閉包中自己的j,就不會(huì)造成指向同一個(gè)變量的問(wèn)題。

  • this指向問(wèn)題
test () {
    var people = {
      id: 1,
      name: '小飛',
      age: 18,
      getAge: function () {
        return function () {
          console.log(this.age)
        }
      }
    }
    people.getAge()() // TypeError: Cannot read property 'age' of undefined
},

當(dāng)使用people調(diào)用getAge函數(shù)時(shí),返回的是打印閉包函數(shù)下的age,而此時(shí)是在全局作用域window下調(diào)用的,沒(méi)有age這個(gè)變量,所以會(huì)找不到。
解決方法:

test () {
    var people = {
        id: 1,
        name: '小飛',
        age: 18,
        getAge: function () {
            return (function (that) { // 閉包函數(shù)
                console.log(that.age)
            })(this)
        }
    }
    people.getAge()   // 18
},

可以在getAge函數(shù)內(nèi)將this指向people作用域,傳遞給閉包函數(shù),給予作用域權(quán)限訪問(wèn)age
③ 內(nèi)存泄漏問(wèn)題
由于閉包會(huì)攜帶包含它的函數(shù)的作用域,因?yàn)闀?huì)比其他函數(shù)占用更多內(nèi)容,過(guò)度使用閉包,會(huì)導(dǎo)致內(nèi)存占用過(guò)多。 內(nèi)存泄漏是指,一塊被分配的內(nèi)存既不能使用,也不能回收,影響程序性能。
JS對(duì)象和DOM對(duì)象的引用造成的問(wèn)題:

function test() {
    var e = document.getElementByID("container");
    e.onclick = function() {
        console.log(e.name)
    }
}

先創(chuàng)建了DOM對(duì)象,為該對(duì)象設(shè)置點(diǎn)擊事件,引用匿名閉包函數(shù),該函數(shù)都可以訪問(wèn)test作用域內(nèi)的所有屬性,產(chǎn)生了一種關(guān)聯(lián),但是執(zhí)行完之后,由于這種關(guān)聯(lián),DOM對(duì)象無(wú)法被回收,形成了內(nèi)存占用
解決方法:

function test() {
    var e = document.getElementByID("container");
    var name = e.name
    e.onclick = function() {
        console.log(name)
    }
    e = null
}

手動(dòng)釋放DOM對(duì)象,保留有用數(shù)據(jù)
參考文章:
徹底搞懂JS閉包各種坑

最后編輯于
?著作權(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ù)。

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