<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      前言:
      所有的編程語言都具備一個基本功能,儲存、訪問、修改變量的值,這種能力將狀態帶給了程序。
      那問題來了:變量儲存在哪里?如何訪問到它們?
      一,編譯原理
      一段源代碼在執行前會經歷的三個步驟,統稱為編譯。
      1,傳統編譯語言(這里以執行“var a = 2;”為例)
      1?? 分詞/詞法分析(Tokenizing/Lexing):
      詞法單元生成器會將字符串分解成一個一個代碼塊,稱為詞法單元。如:var、a、=、2、;。一共5個,空格是否被當作詞法單元取決于空格在這門語言中是否有意義。
      這里譯為兩個單詞,說明這其實是兩個步驟,但是在編譯過程中兩者放在了同一個步驟里,說明兩者差異性并不是很大,并且兩者的功能或者目的實際上是差不多的,那么分詞和詞法分析的異同是什么:這里我的理解是詞法分析是為了更好的分詞,兩者其實是一個過程,唯一的區別是,分詞只是簡單地把字符串拆開來,拆成一個一個代碼塊,這個過程是無狀態的,它不會去識別這些詞法單元究竟是獨立的還是和其它詞法單元有關聯(比如某個詞法單元是另一個的一部分,它并非獨立)。而詞法分析就是調用有狀態的解析規則,在分詞的基礎上再進行一次分析,兩者實際上是做的同一塊東西。
      貼一張圖:
       
       
      添加圖片注釋,不超過 140 字(可選)
       
      2?? 解析/語法分析(Parsing):
      上一步分析完的一個一個的詞法單元,會組成一個詞法單元流(數組),這一步會將這個流轉換成一個由元素逐級嵌套所組成的代表了程序語法結構的樹,這個樹稱為“抽象語法樹”(AST)。以var a = 2; 為例:
       
       
      添加圖片注釋,不超過 140 字(可選)
      這里再貼一張語法結構圖,更清晰:
       
       
      添加圖片注釋,不超過 140 字(可選)
      3?? 代碼生成:
      將AST轉換成可執行代碼的過程被稱為代碼生成,簡單來說,就是將var a = 2;的AST轉化成一組機器指令,用來創建一個叫作a的變量(包括內存分配等),并將一個值儲存在a中。
      普通編譯器的編譯過程一般就是以上三步,(上面的圖片中的解析器鏈接:http://esprima.org/demo/parse.html#)而JavaScript引擎要復雜得多,在語法分析和代碼生成階段有特定步驟來對運行性能進行優化,對于JavaScript來說,編譯發生在代碼執行前的幾微秒(甚至更短)。
      二,引擎、編譯器和作用域
      編譯器:上面剛剛提到的三個步驟就是編譯器做的事情。
      引擎:負責整個JavaScript程序的編譯及執行過程。(主要是執行)
      作用域:負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。
      例如,執行var a = 2;的時候:
      1,編譯器先進行編譯(上面的三步),前兩步一致(分詞/詞法分析、解析AST),但是當執行到第三步的時候(代碼生成),有不同了:
      1?? 遇到var a,編譯器會先詢問作用域是否已經有一個變量a在該作用域的集合中,如果是,則忽略該聲明(因為作用域里已經有了一個a,不用再聲明一個a,注意,此時沒有分配內存,因為還在編譯階段,分配內存是引擎做的),如果不是,則在當前作用域下聲明一個新的變量,命名為a。
      2?? 接下來就是為引擎生成運行時所需的代碼,也就是機器指令,它會告訴引擎“要給a這個變量賦值2”。
      2,上面就是編譯器做的事情,而引擎在運行時會先詢問作用域,在當前作用域的集合中是否存在一個叫作a的變量,如果是,引擎就會使用這個變量,如果不是,引擎會繼續查找該變量(沿著作用域鏈)。引擎最終如果找到了a變量,就會將2賦值給它,如果沒有找到,則會拋出一個異常。
       
      總結就是,編譯器只負責編譯部分,它不會去執行代碼,而是把js語言編譯成引擎能讀懂的機器指令,而引擎負責執行這一系列指令。作用域的作用其實就比較大了,無論是在編譯器編譯的時候還是在引擎執行的時候,都起到了關鍵作用。
      這里其實有一個疑問:既然編譯器已經做了一次a的作用域查找,并且如果當前作用域下沒有,就新增一個a,那為什么引擎又要去找一遍,這不是重復工作了么?原因是兩者運行的階段時機不同,編譯器是在執行代碼前幾微秒甚至更短的時候進行編譯的,而引擎則是代碼在執行的時候執行,所以需要進行再次判斷。
      所以對于變量的賦值操作來說,會執行兩個動作,第一是編譯器先在當前作用域下聲明一個變量(如果沒有的話),第二是引擎會在運行時在作用域中查找該變量,如果能找到就對它進行賦值。
      三,LHS和RHS
      二者分別位于賦值操作的左側和右側
      1,LHS:查詢一個變量的容器本身,就是LHS查詢,這里強調的是查到這個變量的位置,并不是要得到它的值,例如a = 2;中,a就需要我們去進行LHS查詢。
      2,RHS:查詢一個變量的值,就是RHS查詢,這里強調的是查到這個變量的值是什么,并不在意它在哪里,例如console.log(a),我們這里只要關心a的值是多少,不關心a放哪里了,就要對a進行RHS查詢。
      舉一些例子:
      function foo(a) { console.log(a); } foo(2);
      1?? 這里foo(...)就是一次RHS查詢,我們在調用foo函數時,首先要知道foo這個函數是什么;
      2?? 再把2賦值給這個函數的形參,那么要先進行一次LHS查詢,查到這個函數的形參的容器是什么;
      3?? 查到是a以后,對a進行賦值操作,a = 2;
      4?? 然后執行下面的console.log(a);console.log(...)本身也是一次RHS查詢,要查到這個console對象是什么,并且這個對象下面有沒有log(...)這個方法;
      5?? 查到了console.log方法后,這里對a進行一次RHS查詢,要查到這個a是多少,查到a = 2,接著執行打印;
      這就是一套完整的流程。
      再來做一題:
      function foo(a) { var b = a; return a + b; } var c = foo (2);
      1?? 首先對c進行變量賦值操作,則先要對c進行LHS,不關心c是多少,因為要對這個容器重新賦值;
      2?? foo(2),首先對foo(...)函數進行RHS查詢,然后對它的形參進行賦值操作,所以查找形參容器是一次LHS,查到為a容器,并賦值為2,a=2;
      3?? var b = a;對b賦值,因此先對b進行容器查詢,即為LHS,然后賦值為a,那么對a進行RHS查詢,因為我只關心a的值是多少;
      4?? return a + b;這里還是只關心a和b的值是多少,所以兩次都是RHS;計算出值后return結果;
      綜上,一共3次LHS,4次RHS。
      三,作用域
      關于作用域,前面有一篇隨筆已經很詳細的寫過了,包括變量對象、活動對象、作用域鏈等概念。但是這里想要重點細說的是查找的過程。上面介紹了LHS和RHS,真正的目的就是為了說這個:
      1,LHS查找的時候,要找到變量的容器,這里要分兩種情況,一種是var一個變量,還有一種是直接調用某個變量,如
      var a = 2和a = 2,兩者的區別是,
      如果是var聲明一個變量,那么只會查找當前作用域,如果有,就忽略它,不必重新聲明,如果沒有,就在當前作用域下重新聲明一個變量a,(再說一次,這里不做內存分配,因為還處于編譯階段);
      如果是直接調用a變量,那么就會沿著作用域鏈查找,當前的找不到就去上一級,直到最外層的全局作用域下,在非嚴格模式下,如果直到全局作用域下都沒有找到,那么就會在全局作用域下聲明一個變量a,但是如果是嚴格模式下,就會報一個ReferenceError的異常。
      2,RHS查找的時候,會沿著作用域鏈一直查找,如果找不到該變量,就會報一個ReferenceError的異常;如果找到了,但是用法不對,比如foo(...)是一個函數,結果查找到的結果foo是一個普通對象或者基本類型的值,再比如引用null或undefined中的屬性,那么引擎就會報一個TypeError的異常。
      四,詞法作用域、函數作用域、塊作用域
      1,詞法作用域:
      在編譯第一步時,詞法單元生成器會將字符串分解成一個一個的詞法單元,這個叫分詞,如果是有狀態的解析,還會給詞法單元賦予語義。此時會進行作用域查找(這里不是LHS查找,也不是RHS,跟那個無關),這時候的作用域就是詞法作用域。詞法作用域是在分詞和詞法分析階段(編譯第一步)形成的,它由變量的位置來決定。一般情況下,詞法作用域在編譯時就確定了,不會再變。(除非遇到特殊的欺騙作用域)
      函數的詞法作用域只取決于函數被聲明時所處的位置決定。所有的標識符查找都是沿著詞法作用域來查找,因為在編譯時就已經確定了。
      欺騙詞法:eval和with(這兩個在嚴格模式下被禁止,后面也被廢棄了,所以只需了解一下就行)
      1?? eval:接收一個字符串為參數,并將其內容視為在書寫時就存在于程序中這個位置的代碼。因為詞法作用域是在編譯的第一階段就形成的,在分詞的時候,eval里的字符串代碼還沒有執行,當前詞法作用域下還沒有eval里定義的某個變量,而執行eval()函數是引擎在編譯之后才去執行,所以相當于只有在執行的時候,這個變量才會添加進去。通常情況下,詞法作用域是在編譯的時候就確定了,并且不會再變,而eval是在執行的時候強行修改了作用域(在作用域里添加一些東西)。
      舉個例子
      function foo(str, a) { eval(str); // 欺騙 console.log(a,b); } var b = 2; foo('var b = 3', 1); // 1, 3
      在編譯時,console.log(a, b)里面的b就是外層的var b = 2; ,因為根據詞法作用域,在函數作用域中未找到b的定義,因此沿著作用域鏈繼續找,在全局下找到了b的定義。但是執行的時候,eval強行在函數作用域里新增了一個b,因此遮蔽了外層的b,所以b的值為3。
      2??with:先看下用法
      var obj = { a: 1, b: 2, c: 3 }; // 修改obj的三個屬性值 obj.a = 2; obj.b = 3; obj.c = 4; //等同于 with(obj) { a = 2; b = 3; c = 4; }
      這里是為了不重復引用obj自身。
      再看一個問題:
      function foo(obj) { with(obj) { a = 2 } } var o1 = { a: 1 }; var o2 = { b: 1 }; foo(o1); o1.a; // 2 foo(o2); o2.a; // undefined a; // 2
      實際上with語句內部將對象處理為一個完全隔離的詞法作用域,因此這個對象的屬性就被處理為這個作用域中的詞法標識符。所以說,在with語句中的a = 2在執行的時候,相當于進行了LHS引用,在o1這個“對象作用域”中進行a的LHS查找,發現有a這個“標識符”,所以這時候對a進行重新賦值,但是在o2中沒有找到,因此繼續到外層詞法作用域查找,一直到全局下都沒找到,因此在全局作用域下生成一個a,然而這并不是我們想要的。
      所以eval和with的區別就是,eval是在當前所在的詞法作用域下進行修改,而with則是根據傳遞的對象新增了一個詞法作用域。
      之所以不建議使用eval和with的原因是,前面提到了,JavaScript引擎會在編譯階段進行數項的性能優化,有些優化依賴于靜態分析時所有的變量及函數定義的位置,才能在執行的過程中快速定義標識符。如果在執行的時候改變了詞法作用域,那么之前做的優化等于無用功,反而會拖慢引擎,性能降低。所以這也是eval和with被禁止的原因。
      2,函數作用域
      在任意代碼塊外面添加包裝函數,可以將部分變量變為私有,外部無法訪問到包裝函數內部的任何內容。但是帶來的問題是,會在全局下定義一堆函數聲明(函數聲明簡單來說就是function開頭的代碼塊,注意:(function.... 這個不是函數聲明,因為它不是function開頭,而是(function開頭 ),并且函數聲明必須命名,所以也會污染到全局環境,而且必須手動調用才行。
      解決方案是,將函數聲明改為函數表達式。
      (function foo() { var a = 3 })()
      這種函數表達式無法在全局下被訪問到,不會污染全局,并且會立即執行,無需手調。(補充一點,后面的括號里可以傳參)
      關于立即執行的函數,有兩種寫法:
      (function foo(){})(); (function foo(){}());
      兩者在功能上一致,使用哪個全憑個人喜好。第一種好像偏多,還有一個專業術語:IIFE
      函數表達式也可以是匿名的,比如setTimeout的回調函數,一般都是匿名的,但是匿名函數的缺點就是不方便進行調試,并且執行遞歸的過程中,無法使用自身,argument.callee已經被禁止了,因此解決方案是在匿名函數前面加一個函數名:
      setTimeout(function foo() { // ... }, 100);
      3,塊作用域
      ES6之前,JavaScript幾乎沒有塊級作用域。(除了with語句和try/catch語句中的catch)。ES6之后就出現了塊級作用域,尤其是let和const能對當前作用域進行挾持。
      以上就是今天分享的內容了,感興趣的同學可以留言一起討論哈!
      文字免費,但碼字不易,記得點贊!
      感興趣可關注一波,謝謝!
      posted on 2025-05-29 14:53  言先生  閱讀(10)  評論(0)    收藏  舉報

      主站蜘蛛池模板: 成人免费乱码大片a毛片| 久久中文字幕无码专区| 久久天天躁综合夜夜黑人鲁色| 精品av综合导航| 日韩一区二区三区日韩精品| 好男人日本社区www| 国产一区二区三区在线观看免费| 久操热在线视频免费观看| 亚洲一区中文字幕人妻| 国产精品福利自产拍久久| 五月天免费中文字幕av| 国产精品一区在线蜜臀| 骚虎视频在线观看| 77777五月色婷婷丁香视频| 成人午夜福利精品一区二区| 国产不卡免费一区二区| 久久先锋男人AV资源网站| 久久毛片少妇高潮| 中文字幕人妻中出制服诱惑| 图片区 小说区 区 亚洲五月 | 在线精品国精品国产不卡| 无码人妻斩一区二区三区 | 中文字幕日韩有码国产| 暖暖 免费 高清 日本 在线观看5| 亚洲精品综合网中文字幕| 中文字幕一区二区三区四区五区| 五月综合激情婷婷六月| 年轻女教师hd中字3| 91精品国产免费人成网站| 91色老久久精品偷偷蜜臀| 久久国产精品老人性| 国产精品青草久久久久福利99| 国产综合色一区二区三区| 久久人体视频| 高清破外女出血AV毛片| 黄页网站在线观看免费视频| 四虎影视一区二区精品| 九九热精品在线免费视频| 欧美性色黄大片| 无码一区二区三区久久精品| 红杏av在线dvd综合|