成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

弄懂JavaScript的作用域和閉包

everfly / 1798人閱讀

摘要:關(guān)于本書,我會寫好幾篇讀書筆記用以記錄那些讓我恍然大悟的瞬間,本文是第一篇弄懂的作用域和閉包。作用域也可以看做是一套依據(jù)名稱查找變量的規(guī)則。聲明實際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域。

《你不知道的JavaScript》真的是一本好書,閱讀這本書,我有多次“哦,原來是這樣”的感覺,以前自以為理解了(其實并非真的理解)的概念,這一次真的理解得更加透徹了。關(guān)于本書,我會寫好幾篇讀書筆記用以記錄那些讓我恍然大悟的瞬間,本文是第一篇《弄懂JavaScript的作用域和閉包》。

看正文之前,先考你幾個問你,如果你能清晰的回答,那本文可能對你作用不大,如果有一些疑問,那我們就一起來解開這些疑問吧。

考考你

標(biāo)識符是什么?LHS,RHS又是什么,其意義何在?

什么是詞法作用域?javascript語言中那些東西會影響作用域?

我們一直都在聽說的各種提升(函數(shù)提升,變量提升)究竟要怎么理解?

在我們平時的編程中,那些地方用到了閉包?(悄悄告訴你,我之前也能把閉包的概念背的滾瓜亂熟,但是卻一直以為自己平時很少用到閉包,后來才發(fā)現(xiàn),原來一直都在用啊。。)

正文從這里開始 從瀏覽器如何編譯JS代碼說起

很久以來我就在思考,當(dāng)我們把代碼交給瀏覽器,瀏覽器是如何把代碼轉(zhuǎn)換為活靈活現(xiàn)的網(wǎng)頁的。JS引擎在執(zhí)行我們的代碼前,瀏覽器對我們的代碼還做了什么,這個過程對我來說就像黑匣子一般,神秘而又讓人好奇。

理解var a = 2

我們每天都會寫類似var a = 2這樣的簡單的JS代碼,可是瀏覽器是機器,它可只認(rèn)識二進制的0和1,var a = 2對它來說肯定比外語對我們還難。不過有困難不要緊,至少我們現(xiàn)在問題清晰了,要知道它是如何把有意義的人類字符轉(zhuǎn)化為符合一定規(guī)則的機器的0 和 1 。

想想我們是如何閱讀一句話的(可以想想我們不那么熟悉的外語),我們不熟悉英語的時候,我們其實優(yōu)先去理解的是一個個的詞,這些詞按照一定的規(guī)則就成了有意義的句子。瀏覽器其實也是如此var a = 2,瀏覽器其實看到的是var,a,=,2這是一個個的詞。這個過程叫做詞法解析階段,換句話說是這個過程會將由字符組成的字符串分解成(對編程語言來說)有意義的代碼塊。
就像我們按照語法規(guī)則組合單詞為句子一樣,瀏覽器也會把上述已經(jīng)分解好的代碼塊組合為代表了程序語法結(jié)構(gòu)的樹(AST),這個階段稱為語法分析階段,AST對瀏覽器來說已經(jīng)是有意義的外語了,不過距離它直接理解還差一步代碼生成,轉(zhuǎn)換代碼為有意義的機器語言(二進制語言)。

我們總結(jié)一下經(jīng)歷的三階段

- 詞法分析:分解代碼為有意義的詞語;
* 語法分析:把有意義的詞語按照語法規(guī)則組合成代表程序語法結(jié)構(gòu)的樹(AST);
* 代碼生成:將 AST 轉(zhuǎn)換為可執(zhí)行代碼

通過上述三個階段,瀏覽器已經(jīng)可以運行我們得到的可執(zhí)行代碼了,這三個階段還有一個合稱呼叫做編譯階段。我們把之后對可執(zhí)行代碼的執(zhí)行稱為運行階段

JS的作用域在何時確定

編程語言中,作用域一般來說有兩種,詞法作用域和動態(tài)作用域。詞法作用域就是依賴編程時所寫的代碼結(jié)構(gòu)確定的作用域,一般來說在編譯結(jié)束后,作用域就已經(jīng)確定,代碼運行過程中不再改變。而動態(tài)作用域聽名字就知道是在代碼運行過程中作用域會動態(tài)改變。一般認(rèn)為我們的javascript的作用域是詞法作用域(說一般,是因為javascript提供了一些動態(tài)改變作用域的方法,后文會有介紹)。

詞法作用域就是依賴編程時所寫的代碼結(jié)構(gòu)確定的作用域,對比一下瀏覽器在編譯階段做的事情,我們發(fā)現(xiàn),詞法作用域就是在編譯階段確定的??吹竭@里是不是突然理解了為什么以前我們常常聽到的“函數(shù)的作用域在函數(shù)定義階段就確定了”這句話了。接下來我們就來說明函數(shù)作用域是按照什么規(guī)則確定的。

JS中的作用域 作用域是什么?

關(guān)于作用域是什么?《You don’t know js》給出了這么一個概念:

使用一套嚴(yán)格的規(guī)則來分辨哪些標(biāo)識符對那些語法有訪問權(quán)限。

好吧,好抽象的一句話,標(biāo)識符又是什么呢?作用域到底要怎么理解???我們一個個來看。

標(biāo)識符:

我們知道,當(dāng)我們的程序運行的時候,我們的數(shù)據(jù)(”字符串”,“對象”,“函數(shù)”等等都是要載入內(nèi)存的)。那我們該如何訪問到對應(yīng)的內(nèi)存區(qū)域呢,標(biāo)識符就在這時候起作用了,通過它我們就能找到對應(yīng)的數(shù)據(jù),從這個角度來看,變量名,函數(shù)名等等都是標(biāo)識符。

對標(biāo)識符的操作
知道了標(biāo)識符,我們來想想,平時我們會對標(biāo)識符進行哪些操作。其實無外乎兩種,看下面的代碼:

// 第一種定義了標(biāo)識符`a`并把數(shù)值2賦值給了`a`這種操作有一個專門的術(shù)語叫做`LHS`
var a = 2;

// 第二種,var b = a ,其實對應(yīng)a ,b 兩個操作符是不同的操作,對b來說是一個賦值操作,這是LHS,但是對a來說卻是取到a對應(yīng)的值,這種操作也有一個專門的術(shù)語叫做“RHS”
var b = a;

小結(jié)一下,對標(biāo)識符來說有以下兩種操作

- 賦值操作(LHS);常見的是函數(shù)定義,函數(shù)傳參,變量賦值等等
* 取值操作(RHS);常見包括函數(shù)調(diào)用,
再回過頭來看作用域

明白了標(biāo)識符及對標(biāo)識符的兩種操作,我們可以很容易的理解作用域了,作用域其實就是定義了我們的呈現(xiàn)在運行期,進行標(biāo)識符操作的范圍,對應(yīng)到實際問題來說,就是我們熟悉的函數(shù)或者變量可以在什么地方調(diào)用。

作用域也可以看做是一套依據(jù)名稱查找變量的規(guī)則。那我們再細看一下這個規(guī)則,在當(dāng)前作用域中無法找到某個變量時,引擎就會在外層嵌套的作用域中繼續(xù)查找,直到找到該變量, 或抵達最外層的作用域(也就是全局作用域)為止。

這里提到了嵌套一詞,我們接下來看js中那些因素可以形成作用域。

JS中的作用域類型
函數(shù)作用域

函數(shù)作用域是js中最常見的作用域了,函數(shù)作用域給我們最直觀的體會就是,內(nèi)部函數(shù)可以調(diào)用外部函數(shù)中的變量。一層層的函數(shù),很直觀的就形成了嵌套的作用域。不過只說這一點真對不起本文的標(biāo)題,還記得我們常常聽到的“如果在函數(shù)內(nèi)部我們給一個未定義的變量賦值,這個變量會轉(zhuǎn)變?yōu)橐粋€全局變量”。對我來說之前這句話幾乎是背下來的,我一直都沒能理解。我們從對標(biāo)識符的操作的角度來理解這句話。

var a = 1;

function foo(){
// b第一次出現(xiàn)在函數(shù)foo中
    b = a ;
}

foo();

// 全局可以訪問到b
console.log(b); //1

在我們調(diào)用foo()時,對b其實是進行了LHS操作(取得a的值并賦值給b),b前面并不存在var let 等,因此瀏覽器首先在foo()作用域里面查找b這個標(biāo)識符,結(jié)果在b里面沒有找到,安裝作用域的規(guī)則,瀏覽器會繼續(xù)在foo()的外層作用域?qū)ふ覙?biāo)識符b,結(jié)果還是沒有找到,說明在這次查詢標(biāo)識符b的范圍內(nèi)并不存在已經(jīng)定義的b,在非嚴(yán)格模式下LHS操作會在可查找范圍的最外層(也就是全局)定義一個b,因此b也就成了一個全局的變量了(嚴(yán)格模式LHS找不到返回ReferenceError錯誤)。這樣那句話就可以理解了。同樣值得我們注意的是對操作符進行RHS操作會出現(xiàn)不同的情況,無論嚴(yán)格或者非嚴(yán)格模式RHS找不到對返回ReferenceError錯誤(對RHS找到的值進行不合理的操作會返回錯誤TypeError(作用域判別成功,操作非法。))。

閉包:閉包是基于詞法作用域書寫代碼時所產(chǎn)生的自然結(jié)果,你甚至不需要為了利用它們而有意 識地創(chuàng)建閉包。閉包的創(chuàng)建和使用在你的代碼中隨處可見。你缺少的是根據(jù)你自己的意愿 來識別、擁抱和影響閉包的思維環(huán)境。

塊作用域

除了函數(shù)作用域,JS也提供塊作用域。我們應(yīng)該明確,作用域是針對標(biāo)識符來說的,塊作用域把標(biāo)識符限制在{}中。

ES6 提供的let,const方法聲明的標(biāo)識符都會固定于塊中。常被大家忽略的try/catchcatch語句也會創(chuàng)建一個塊作用域。

改變函數(shù)作用域的方法

一般說來詞法作用域在代碼編譯階段就已經(jīng)確定,這種確定性其實是很有好處的,代碼在執(zhí)行過程中,能夠預(yù)測在執(zhí)行過程中如何對它們進行查找。能夠提高代碼運行階段的執(zhí)行效率。不過JS也提供動態(tài)改變作用域的方法。eval()函數(shù)和with關(guān)鍵字.

eval()方法:
這個方法接受一個字符串為參數(shù),并將其中的內(nèi)容視為好像在書寫時就存在于程序中這個位置的代碼。換句話說,可以在你寫的代碼中用程序生成代碼并運行,就好像代碼是寫在那個位置的一樣。

 function foo(str,a){
     eval(str);//欺騙作用域,詞法階段階段foo()函數(shù)中并沒有定義標(biāo)識符,但是在函數(shù)運行階段卻臨時定義了一個b;
     console.log(a,b);
 }
 
 var b = 2;
 
 foo("var b =3;",1);//1,3

 // 嚴(yán)格模式下,`eval()`會產(chǎn)生自己的作用域,無法修改所在的作用域
 function foo(str){
     "use strict";
     eval(str);
     console.log(a);//ReferenceError: a is not de ned
 }
 
 foo("var a =2");

eval()有時候挺有用,但是性能消耗很大,可能也會帶來安全隱患,因此不推薦使用。

with關(guān)鍵字:

with 通常被當(dāng)作重復(fù)引用同一個對象中的多個屬性的快捷方式。

    var obj = { 
        a: 1,
      b: 2,
      c: 3 
      };
    // 單調(diào)乏味的重復(fù) "obj" obj.a = 2;
    
    obj.b = 3;
    obj.c = 4;

    // 簡單的快捷方式 
      
   with (obj) {
        a = 3;
        b = 4;
        c = 5;
    }

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

    var o1 = { 
        a: 3
    };

    var o2 = { 
        b: 3
    };

    foo( o1 );
    console.log( o1.a ); // 2
    
    foo( o2 );
    console.log( o2.a ); // undefined
    
    console.log( a ); // 2——不好,a被泄漏到全局作用域上了!
    
    // 執(zhí)行了LHS查詢,不存在就在全局創(chuàng)建了一個。
    // with 聲明實際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域。 

with也會帶來性能的損耗。

JavaScript 引擎會在編譯階段進行數(shù)項的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標(biāo)識符。

聲明提升

作用域關(guān)系到的是標(biāo)識符的作用范圍,而標(biāo)識符的作用范圍和它的聲明位置是密切相關(guān)的。在js中有一些關(guān)鍵字是專門用來聲明標(biāo)識符的(比如var,let,const),非匿名函數(shù)的定義也會聲明標(biāo)識符。

關(guān)于聲明也許大家都聽說過聲明提升一詞。我們來分析一下造成聲明提升的原因。

我們已經(jīng)知道引擎會在解釋 JavaScript 代碼之前首先對其進行編譯。編譯階段中的一部分工作就是找到所有的聲明,并用合適的作用域?qū)⑺鼈冴P(guān)聯(lián)起來(詞法作用域的核心)。
這樣的話,聲明好像被提到了前面。
值得注意的是每個作用域都會進行提升操作。聲明會被提升到所在作用域的頂部。

不過并非所有的聲明都會被提升,不同聲明提升的權(quán)重也不同,具體來說函數(shù)聲明會被提升,函數(shù)表達式不會被提升(就算是有名稱的函數(shù)表達式也不會提升)。

通過var 定義的變量會提升,而letconst進行的聲明不會提升。

函數(shù)聲明和變量聲明都會被提升。但是一個值得注意的細節(jié)也就是函數(shù)會首先被提升,然后才是變量,也就是說如果一個變量聲明和一個函數(shù)聲明同名,那么就算在語句順序上變量聲明在前,該標(biāo)識符還是會指向相關(guān)函數(shù)。

如果變量或函數(shù)有重復(fù)聲明以會第一次聲明為主。

最后一點需要注意的是:
聲明本身會被提升,而包括函數(shù)表達式的賦值在內(nèi)的賦值操作并不會提升。

作用域的一些應(yīng)用

看到這里,我想大家對JS的作用域應(yīng)該有了一個比較細致的了解。下面說一下對JS作用域的一些拓展應(yīng)用。

最小特權(quán)原則

也叫最小授權(quán)或最小暴露原則。這個原則是指在軟件設(shè)計中,應(yīng)該最小限度地暴露必要內(nèi)容,而將其他內(nèi)容都“隱藏”起來,比如某個模塊或?qū)ο蟮?API 設(shè)計。也就是盡可能多的把部分代碼私有化。

函數(shù)可以產(chǎn)生自己的作用域,因此我們可以采用函數(shù)封裝(函數(shù)表達式和函數(shù)聲明都可以)的方法來實現(xiàn)這一原則。

    // 函數(shù)表達式
    var a = 2;
    (function foo() { // <-- 添加這一行 var a = 3;
       console.log(a); // 3 
    })(); // <-- 以及這一行 
    console.log( a ); // 2

這里順便說明一下如何區(qū)分函數(shù)表達式和函數(shù)聲明

如果 function 是聲明中 的第一個詞,那么就是一個函數(shù)聲明,否則就是一個函數(shù)表達式。
函數(shù)聲明和函數(shù)表達式之間最重要的區(qū)別是它們的名稱標(biāo)識符將會綁定在何處。函數(shù)表達式可以是匿名的,而函數(shù)聲明則不可以省略函數(shù)名——在 JavaScript 的語法中這是非法的。

可以使用立即執(zhí)行的函數(shù)表達式(IIFE)的方式來封裝。

立即執(zhí)行的函數(shù)表達式(IIFE)

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

函數(shù)表達式后面加上一個括號后會立即執(zhí)行。

(function(){ .. }())是IIFE的另外一種表達方式括號加在里面和外面,功能是一樣的。

順便說一下,IIFE 的另一個非常普遍的進階用法是把它們當(dāng)作函數(shù)調(diào)用并傳遞參數(shù)進去。

    var a = 2;
    (function IIFE(global) {
        var a = 3;
        console.log(a); // 3 console.log( global.a ); // 2
    })(window);
    console.log(a); // 2
閉包

一般大家都會這么形容閉包。

當(dāng)一個函數(shù)的返回值是另外一個函數(shù),而返回的那個函數(shù)如果調(diào)用了其父函數(shù)內(nèi)部的其它變量,如果返回的這個函數(shù)在外部被執(zhí)行,就產(chǎn)生了閉包。

    function foo() {
        var a = 2;
    
        function bar() {
            console.log(a);
        }
        return bar;
    }
    var baz = foo();
    baz(); // 2 —— 這就是閉包的效果。在函數(shù)外訪問了函數(shù)內(nèi)的標(biāo)識符
    
    // bar()函數(shù)持有對其父作用域的引用,而使得父作用域沒有被銷毀,這就是閉包

一般來說,由于垃圾回收機制的存在,函數(shù)在執(zhí)行完以后會被銷毀,不再使用的內(nèi)存空間。上例中由于看上去 foo()的內(nèi)容不會再被使用,所以很自然地會考慮對其進行回收。而閉包的“神奇”之處正是可以阻止這件事情的發(fā)生(以前總有人說要減少使用閉包,害怕內(nèi)存泄漏什么的,其實這個也不大比擔(dān)心)。

其實上面這個定義,在好久之前我就知道,不過同時我也誤以為我平時很少用到閉包,因為我真的并沒有主動去用過閉包,不過其實我錯了,無意中,我一直在使用閉包。

本質(zhì)上無論何時何地,如果將函數(shù)(訪問它們各自的詞法作用域)當(dāng)作第一 級的值類型并到處傳遞,你就會看到閉包在這些函數(shù)中的應(yīng)用。在定時器、事件監(jiān)聽器、 Ajax請求、跨窗口通信、Web Workers或者任何其他的異步(或者同步)任務(wù)中,只要使 用了回調(diào)函數(shù),實際上就是在使用閉包!
所以你應(yīng)該知道,你已經(jīng)用過很多次閉包了。

這里說一個大家可能都遇到過的坑,一個沒有正確理解作用域和閉包造成的坑。

    for (var i = 1; i <= 5; i++) {
        setTimeout(function timer() {
            console.log(i);
        }, i * 1000);
    }
// 其實我們想得到的結(jié)果是1,2,3,4,5,結(jié)果卻是五個6

我們分析一下造成這個結(jié)果的原因:
我們試圖假設(shè)循環(huán)中的每個迭代在運行時都會給自己“捕獲”一個 i 的副本。但是根據(jù)作用域的工作原理,實際情況是盡管循環(huán)中的五個函數(shù)是在各個迭代中分別定義的(前面說過以第一次定義為主,后面的會被忽略), 但是它們都被封閉在一個共享的全局作用域中,因為在時間到了執(zhí)行timer函數(shù)時,全局里面的這個i就是6,因此無法達到預(yù)期。

理解了是作用域的問題,這里我們有兩種解決辦法:

    // 辦法1
    for (var i = 1; i <= 5; i++) {
        (function(j) {
            setTimeout(function timer() {
                console.log(j);
            }, j * 1000);
        })(i);
    //通過一個立即執(zhí)行函數(shù),為每次循環(huán)創(chuàng)建一個多帶帶的作用域。
    }
    
    // 辦法2
    for (var i = 1; i <= 5; i++) {
        let j = i; // 是的,閉包的塊作用域! 
          setTimeout( function timer() {
        console.log(j);
        }, j * 1000);
    }
    // let 每次循環(huán)都會創(chuàng)建一個塊作用域

現(xiàn)在的開發(fā)都離不開模塊化,下面說說模塊是如何利用閉包的。

模塊是如何利用閉包的:
最常見的實現(xiàn)模塊模式的方法通常被稱為模塊暴露

我們來看看如何定義一個模塊

    function CoolModule() {
        var something = "cool";
        var another = [1, 2, 3];
    
        function doSomething() {
            console.log(something);
        }
    
        function doAnother() {
            console.log(another.join(" ! "));
        }
    
    // 返回的是一個對象,對象中可能包含各種函數(shù)
        return {
            doSomething: doSomething,
            doAnother: doAnother
        };
    }

    var foo = CoolModule();
// 在外面調(diào)用返回對象中的方法就形成了閉包
    foo.doSomething(); // cool
    foo.doAnother(); // 1 ! 2 ! 3

模塊的兩個必要條件:

必須有外部的封閉函數(shù),該函數(shù)必須至少被調(diào)用一次

封閉函數(shù)必須返回至少一個內(nèi)部函數(shù),這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態(tài)。

文章寫到這里也差不多該結(jié)束了,謝謝你的閱讀,希望你有所收獲。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/91183.html

相關(guān)文章

  • 圖解JS閉包形成原因

    摘要:閉包的出現(xiàn)正好結(jié)合了全局變量和局部變量的優(yōu)點。這就是閉包的一個使用場景保存現(xiàn)場。 前言 什么是閉包,其實閉包是可以重用一個對象,又保護對象不被篡改的一種機制。什么是重用一個對象又保護其不被篡改呢?請看下面的詳解。 作用域和作用域鏈 注意理解作用域和作用域鏈對理解閉包有非常大的幫助,所以我們先說一下作用域和作用域鏈 什么是作用域作用域表示的是一個變量的可用范圍、其實它是一個保存變量的對象...

    wind3110991 評論0 收藏0
  • 夯實基礎(chǔ)-作用域與閉包

    摘要:作用域分類作用域共有兩種主要的工作模型。換句話說,作用域鏈?zhǔn)腔谡{(diào)用棧的,而不是代碼中的作用域嵌套。詞法作用域詞法作用域中,又可分為全局作用域,函數(shù)作用域和塊級作用域。 一篇鞏固基礎(chǔ)的文章,也可能是一系列的文章,梳理知識的遺漏點,同時也探究很多理所當(dāng)然的事情背后的原理。 為什么探究基礎(chǔ)?因為你不去面試你就不知道基礎(chǔ)有多重要,或者是說當(dāng)你的工作經(jīng)歷沒有亮點的時候,基礎(chǔ)就是檢驗?zāi)愫脡牡囊豁?..

    daydream 評論0 收藏0
  • JavaScript作用域和閉包

    摘要:依然持有對該作用域的引用,而這個引用就叫作閉包。循環(huán)和閉包正常情況下,我們對這段代碼行為的預(yù)期是分別輸出數(shù)字,每秒一次,每次一個。 一、作用域 作用域共有兩種主要的工作模型:第一種是最為普遍的,被大多數(shù)編程語言所采用的詞法作用域,另外一種叫作動態(tài)作用域; JavaScript所采用的作用域模式是詞法作用域。 1.詞法作用域 詞法作用域意味著作用域是由書寫代碼時函數(shù)聲明的位置來決定...

    animabear 評論0 收藏0
  • JavaScript作用域和閉包

    摘要:依然持有對該作用域的引用,而這個引用就叫作閉包。循環(huán)和閉包正常情況下,我們對這段代碼行為的預(yù)期是分別輸出數(shù)字,每秒一次,每次一個。 一、作用域 作用域共有兩種主要的工作模型:第一種是最為普遍的,被大多數(shù)編程語言所采用的詞法作用域,另外一種叫作動態(tài)作用域; JavaScript所采用的作用域模式是詞法作用域。 1.詞法作用域 詞法作用域意味著作用域是由書寫代碼時函數(shù)聲明的位置來決定...

    CNZPH 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<