javascript從範圍到閉包-筆記

來源:互聯網
上載者:User

標籤:第一個   停止   class   程式   忽略   define   另一個   on()   集合   

讀《你不知道的javascript》一書做個筆記;
編譯原理:
    js是一門編譯型的語言,與傳統編譯語言類似,傳統編譯的過程分為三個階段 ; 
    1. 分詞/詞法分析; 2.解析/文法分析; 3.代碼產生 ; 
    js引擎在編譯時間會比較複雜 具體多麼複雜我也不造,大概就是對1,3 進行了最佳化使其快速編譯完成並立即執行,這裡就要注意了,,js是在執行前編譯的 也許幾微秒就OK了
1.範圍 :  // 收集並維護所有聲明的變數組成一個查詢機制,用一套嚴格的規則以確保當前執行的代碼對這些變數的存取權限;
    範圍有兩種工作模型 一種是普遍使用的靜態範圍也叫詞法範圍 另一種是動態範圍
    以 var a = 1; 為例
    編譯器(上面的 1 2 3)開始工作了,看到 var a 時,編譯器 會去詢問範圍 在當前範圍的集合中有沒有一個叫a的變數, 如果有 則忽略 繼續編譯,要是沒有,那就在當前範圍的集合中聲明一個叫a的變數;編譯完成後就要產生代碼以便引擎運行它,當引擎運行 a = 1;時,引擎也得詢問範圍 在當前範圍的集合中有沒有一個叫a的變數,如果有 引擎就使用並它為其賦值為1,要是沒用那就向當前範圍的外層範圍詢問尋找 一直到全域範圍為止(要是全域範圍也沒用 範圍會說“你的小祖宗沒找到但幫你建立了一個”非strict 模式下);
    引擎在向範圍詢問變數時 有兩種方式 LHS 與 RHS; 上面那個就是LHS;簡單過一下 LHS即 賦值操作的對象 RHS即賦值操作的源頭; 簡單講 var a = 1; 這裡a 就是LHS; console.log( a ) 這個a就是RHS;換言之一個是賦值 一個是取值(恩 可以這麼簡單理解);
    非strict 模式下 RHS 尋找失敗時( 尋找到全域範圍也沒找到 ) 引擎會拋出ReferenceError 異常; 
                          LHS 尋找失敗時( 尋找到全域範圍也沒找到 ) 全域範圍會非常熱心的給你建立一個;
    strict 模式下 LHS 尋找失敗時( 尋找到全域範圍也沒找到 ) 引擎會拋出類似 ReferenceError 的異常; 
                      RHS 尋找失敗時 同非strict 模式一樣;
    RHS如果尋找到了該變數,但做了不合理的操作時( 比如一個數值型變數當方法使用 )或是引用了null / undefined類型值得屬性時,引擎會拋出TypeError;
    ReferenceError 是範圍尋找失敗時拋出的;
    TypeError 是範圍尋找成功了,但做了不合理的操作拋出的;
1.0 範圍鏈 // 當一個函數嵌套另一個函數時 就形成了範圍嵌套 產生了範圍鏈
 

 var b = 1;    function fn(a){        return a + b;    }    fn(2);// 3

 

當b進行RHS查詢時在fn的範圍集合中找不到 會沿著範圍鏈向上尋找 這裡是全域範圍

    
1.1.詞法範圍 // 就是編譯原理的第一階段 定義詞法的範圍 就是寫代碼時的範圍 也叫靜態範圍
  

   function fn(a){        var b = a.num*2;        function fn1(c){            console.log(c);        }        fn1(b+1);    }    fn({"name":2});//5

    這個例子中有三層範圍即 1.全域範圍 一個標識符 fn;2.fn範圍 三個標識符 a ,b ,fn1;3.fn1範圍 一個標識符 c;

    範圍尋找某個標識符時會在遇到的第一個匹配的標識符時停止,多層嵌套範圍中定義同名標識符這種做法稱為"遮蔽效應‘(內部標識符遮蔽了外部標識符);
    全域變數會成為全域對象window的屬性; 利用這一特性可以訪問被遮蔽的全域識別碼,但除了全域識別碼以外的標識符如果被遮蔽 是沒有辦法訪問的;
    無論函數怎麼調用以及如何調用 他的詞法範圍都只由函式宣告時所在的位置決定;
    詞法範圍只尋找一級標識符 如 a, fn, arr; 對於尋找像json.name.value 這樣的對象訪問時,詞法範圍只會尋找json 找到該標識符後 由對象屬性訪問規則分別接管訪問name和value;
1.2 欺騙詞法範圍// 詞法範圍時由寫代碼時決定的,在代碼運行時來修改(欺騙)詞法範圍的手段/方式  就達成了欺騙詞法範圍的目的
    以eval();為例:
    js中的eval();接受一個字串作為參數;可以將代碼用程式產生 就像程式本就寫在那一樣;根據這個原理 eval()可以達到欺騙詞法範圍的目的;
 

var a = 2;    function fn(str){        eval(str);// 欺騙        console.log(a);    }    fn("var a = 1;"); // 1

    這裡的str是寫死的 如果需要完全可以用程式自己產生;

    strict 模式下 eval(); 有自己的詞法範圍 不會影響所在的範圍;
    類似eval();的還有 setInterval(),setTimeout()的第一個參數可以是字串,字串會被解釋成一段動態函數代碼;還有new Function();
 這裡同樣提示大家不要使用它們,這裡附一個原生封裝的一個輪子 可相容至IE7的字串轉json解析函數 : https://github.com/liuyushao147/javascript_minCode
    with();也是如此 這裡不作闡述了;
    都知道eval() with() Function() 是魔鬼,如果僅僅是因為它們欺騙詞法範圍而定義為魔鬼的話那就太極端了; js引擎在編譯階段會進行各種最佳化,其中一項就是根據代碼的詞法進行靜態分析,預先確定它們的位置 ,以便運行時快速找到它們;但發現eval() with() 等的時候引擎無法確定它們會接受什麼樣的代碼,對範圍做什麼樣的修改,因此引擎會忽略它們不作任何最佳化,代碼中過多使用eval()等函數程式會啟動並執行相當慢,儘管引擎很聰明;有時使用不當它們會不只不覺的改變全域變數 這個就更神奇了..
1.3函數範圍 //  是指這個函數的全部變數/標識符 都可在其內部範圍內被使用及複用(嵌套的範圍也能使用);
  

function fn(){        var a = 1;        function fn1(){        // 代碼....        }    };

    上面這段代碼 同樣的三層範圍 全域 fn 及fn1 , 在fn函數內部 都可以訪問使用變數a( fn1中也可以使用 ), 但在全域範圍下 是訪問不到這個a的,它是fn私人的; 接著我們寫一段類似這樣的代碼:

var b;    function getadd(a){        b = a+add(a+2);        console.log(b)    }    function add(a){        return a*2    }    getadd(2);//10

    有點眼熟額,這可能是入門或初級學者普遍寫法,這樣寫講真,前期問題不大,但後期維護成本就高了,在版本迭代的時候 保不準可能標識符覆蓋 ,為什麼這麼說呢,變數b與函數add應該是getadd函數的私人屬性應在其內部實現,要是在外部能訪問使用他們不僅沒必要也可能會產生超出getadd的使用條件;是很危險的;so 應將其私人化

   

function getadd(a){        var b;        function add(a){            return a*2        }        b = a+add(a+2);        console.log(b);    }    getadd(2);//10

    這樣就舒服多了,b與add都無法從外部被訪問而只被getadd所控制了;功能上也沒有影響,並且也體現了私人化的設計,更符合了最小授權或最小暴露原則;在看一段代碼:

function fn(){        function fn1(a){            i = 2;            console.log(a*i);        }        for(var i=0;i<5;i++){            fn1(--i);        }    }    fn();// 完美的讓瀏覽器崩掉了...i=2 意外的覆蓋for中的i了 迴圈條件永遠滿足.


    我知道實際中一定不會有這麼寫的, 提這段代碼主要是為了理清一個概念 隱藏範圍 即隱藏範圍中的變數及函數; 好處多多可以避免標識符衝突也可預防類似上面的問題(遮蔽效應可完美解決這個尷尬);另外提一下關於全域命名衝突的解決方案有個專業的叫法 全域命名空間;其就是將自己私人的變數函數都給隱藏起來 對外只提供了一個變數(一般是json對象); 在任意程式碼片段外部添加一個封裝函數就可以實現隱藏範圍的目的了,外部函數即便是上天了也訪問不到被封裝函數內部的任何內容(不要提閉包) 上代碼:
  

 var a = 2;    function fn(){ // 添加一個封裝函數        var a = 3;    }    fn();    console.log(a);// 2

    同樣的實際中相信即使不加這個封裝函數也不會有人這麼寫的;一方面用來闡述封裝函數的意義;另一方面得找個坑跳下去; 問題就是 在添加封裝函數的時候 這個函數本身 (fn)就已經汙染了所在的範圍啊 想一下 在一個函數中有N多封裝函數 場面一定混亂不堪;好了 函數運算式上場了; 函式宣告與運算式的區別即 function 如果是聲明的第一個詞 那就是函式宣告 否則就是運算式

    運算式可分為 匿名運算式 立即執行運算式 (IIFE),對於這塊日後會專門記錄一篇的 ; 函數運算式可以解決這個尷尬.
    呼呼,接下來瞭解下塊範圍的概念 js是沒有塊級範圍的(es3),with 關鍵字是個異類它類似塊範圍的形式 可以自行瞭解這貨,除了with外 try catch語句中的catch也是一個塊範圍,es6 有了塊範圍的概念及用法 後期會一一交流;
    塊範圍有什麼卵用?這麼說吧 它是最小授權原則的一個擴充 在簡單點 如果有塊範圍就不需要封裝函數了..看代碼:
    for(var i=0;i<5;i+=1){...};
    如果js有塊範圍那麼 for中的i只在for中使用 ,不出所料 i在for外邊也能訪問使用了,
    js有標識符提升的概念; 筆者也不在贅述 但給段代碼自行感受下
  

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

 

3.閉包 //當函數記住並可以訪問所在詞法範圍時 就產生了閉包 即便函數在當前範圍之外調用;
  

function fn(){        var a = 1;        function fn1(){            console.log(a)        }        fn1();    }    fn();//1

 

    這段代碼與前面範圍嵌套類似 按照詞法範圍尋找規則 fn1範圍可以訪問fn範圍下的標識符a;這個是閉包麼?貌似像是個閉包昂,但嚴格的根據上面所述 他還不是 雖然他能訪問當前詞法範圍;繼續
    

function fn(){        var a = 1;        function fn1(){            console.log(a)        }        return fn1;    }    var f = fn();    f();// 1  沒錯這裡才是一個標準(便於理解閉包)的閉包

    分析下 fn1()的詞法範圍能夠訪問fn的內部範圍,然後我們將fn1()當一個傳回值(fn1當作一個值的類型進行傳遞,這個實值型別就是函數類型,換言之就是當作函數類型的值);然後定義f用於接受fn的傳回值(就是fn1()函數),再調用自身f();簡單講就是通過不同變數引用fn內建函式fn1而已 ;

    在執行完fn的時候 通常引擎的記憶體回收機制會對不再使用的標識符回收掉,從而釋放記憶體,表面看 貌似fn 可以被回收了;事實上閉包的優點就體現出來了,,fn的內部範圍並沒被回收;怎麼會這樣呢,哦,其內部範圍下的fn1還在使用啊,fn1聲明在fn的內部範圍中,使其擁有涵蓋fn內部範圍的許可權,從而fn範圍一直存在,以便fn1在任何時間引用;沒錯 這個引用就是閉包;
    關於閉包 有多種多樣的寫法 無論以何種方式對函數類型的值進行傳遞,函數調用時都會產生一個閉包:
    

function fn(){        var a  =1;        function fn1(){            console.log(a);        }        fn2(fn1);    }    function fn2(f){        f();    }    fn();// 1

    沒錯f()處就是一個閉包了.接下來說說IIFE( 立即執行函數 );

   

function fn(){        var a = 1;        (function f(){            conosle.log(a);        }())    }    fn();

    這個f函數並不是在其本身詞法範圍之外執行的,根據這個觀點來看IIFE貌似不是閉包昂;IIFE 也就是上面的函數f 並不是在詞法範圍外執行的,而是在定義時的詞法範圍執行的 a 是通過普通的詞法範圍尋找規則找到的而不是閉包發現的;理論上講閉包應該發生在定義時的,IIFE 確實建立了閉包, 還是用於建立閉包最常有的工具,儘管本身並不會真的使用閉包;

    

function a(){        var b = new Array();        for(var i = 0; i < 10; i++ ){             b[i] = function (){                return i;            }        }        return b    }    var c = a();    for(var i = 0,len = c.length; i < len; i++){        console.log( c[i]() )//10個10     }

    這就尷尬了,用IIFE 改良下

function a(){        var b = new Array();        for(var i = 0; i < 10; i++ ){        (function (){                 b[i] = function (){                    return i;                    }        })();        }        return b    }    var c = a();    for(var i = 0,len = c.length; i < len; i++){        console.log( c[i]() )//10個10     }

     這就更加尷尬了,,依然不行,怎麼回事 IIFE不是可以建立閉包麼。仔細看下原來是我們建立的IIFE範圍是空的啊,,什麼都沒有 簡單講 我們需要為iife 包含點東西 在這就是i了;

 

 function a(){        var b = new Array();        for(var i = 0; i < 10; i++ ){        (function (){            var j = i;                    b[j] = function (){                    return j;                    }        })();        }        return b    }    var c = a();    for(var i = 0,len = c.length; i < len; i++){        console.log( c[i]() )//0-9    } 

 

    呼呼。。這下可以了,其實任何使用回調的地都在使用閉包,閉包實質上是一個標準,是關於如何在函數按值傳遞的詞法範圍中寫的代碼.無疑閉包是強大的,可以用他實現各種模組等 同時閉包也是無處不在的;

最後歡迎大神指正 !

javascript從範圍到閉包-筆記

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.