網站開發進階(四十九)由JS報“未結束的字符串常量”引發的思考

      網友投稿 716 2022-05-30

      #網站開發進階(四十九)由JS報“未結束的字符串常量”引發的思考

      ##報錯

      在做公司項目開發過程中,后期生產環境上報JS出現“未結束的字符串常量”錯,如下:

      后期經過不斷調試,發現是由于Js引擎在解析帶有換行字符串時引起的異常。解析后的js代碼類似于

      If (“12345 abc” == “12345abc”){...

      1

      2

      由以上異常,下面主要講解JavaScript引擎的工作原理。

      Javascript解析引擎在執行代碼前后是怎么工作的,ecma英文版實在看不下去呵呵。

      ##什么是JavaScript解析引擎

      javascript解析引擎(簡稱javascript引擎),是一個程序,是瀏覽器引擎的一部分。每個瀏覽器的javascript解析引擎都不一樣(因為每個瀏覽器編寫Javascript解析引擎的語言(C或者C++)以及解析原理都不相同)。標準的Javascript解析引擎會按照 ECMAScript文檔來實現。雖然每個瀏覽器的Javascript解析引擎不同,但Javascript的語言性質決定了Javascript關鍵的渲染原理仍然是動態執行Javascript字符串。只是詞法分析、語法分析、變量賦值、字符串拼接的實現方式有所不同。

      簡單地說,JavaScript解析引擎就是能夠“讀懂”JavaScript代碼,并準確地給出代碼運行結果的一段程序。比方說,當你寫了 var a = 1 + 1; 這樣一段代碼,JavaScript引擎做的事情就是看懂(解析)你這段代碼,并且將a的值變為2。

      學過編譯原理的人都知道,對于靜態語言來說(如Java、C++、C),處理上述這些事情的叫編譯器(Compiler),相應地對于JavaScript這樣的動態語言則叫解釋器(Interpreter)。這兩者的區別用一句話來概括就是:編譯器是將源代碼編譯為另外一種代碼(比如機器碼,或者字節碼),而解釋器是直接解析并將代碼運行結果輸出。比方說,firebug的console就是一個JavaScript的解釋器。

      但是,現在很難去界定說,JavaScript引擎它到底算是個解釋器還是個編譯器,因為,比如像V8(Chrome的JS引擎),它其實為了提高JS的運行性能,在運行之前會先將JS編譯為本地的機器碼(native machine code),然后再去執行機器碼(這樣速度就快很多),相信大家對JIT(Just In Time Compilation)一定不陌生吧。

      個人認為,不需要過分去強調JavaScript解析引擎到底是什么,了解它究竟做了什么事情就可以了。對于編譯器或者解釋器究竟是如何看懂代碼的,翻出大學編譯課的教材就可以了。

      這里還要強調的就是,JavaScript引擎本身也是程序,由代碼編寫而成。比如V8就是用C/C++寫的。

      ##JavaScript解析引擎到底是干什么的

      JavaScript解析引擎就是根據ECMAScript定義的語言標準來動態執行JavaScript字符串。雖然之前說現在很多瀏覽器不全是按照標準來的,解釋機制也不盡相同,但動態解析JS的過程還是分成兩個階段:語法檢查階段和運行階段。

      語法檢查包括詞法分析和語法分析,運行階段又包括預解析和運行階段(像V8引擎會將JavaScript字符串編譯成二進制代碼,此過程應該歸到語法檢查過程中)。

      ###JavaScript解析過程

      在JavaScript解析過程中,如遇錯誤就直接跳出當前代碼塊,直接執行下一個 script 代碼段。所以在同一個 script 內的代碼段有錯誤的話就不會執行下去,但是不會影響下一個 script 內的代碼段。

      ####第一階段:語法檢查

      語法檢查也是JavaScript解析器的工作之一,包括 詞法分析 和 語法分析,過程大致如下:

      #####一:詞法分析

      詞法分析:JavaScript解釋器先把JavaScript代碼(字符串)的字符流按照ECMAScript標準轉換為記號流。

      例如:把字符流:

      a = (b - c);

      1

      轉換為記號流:

      NAME “a”

      EQUALS

      OPEN_PARENTHESIS

      NAME “b”

      MINUS

      NAME “c”

      CLOSE_PARENTHESIS

      SEMICOLON

      #####二:語法分析

      語法分析:JavaScript語法分析器在經過詞法分析后,將記號流按照ECMAScript標準把詞法分析所產生的記號生成語法樹。

      通俗地說就是把從程序中收集的信息存儲到數據結構中,每取一個詞法記號,就送入語法分析器進行分析。

      語法分析不做的事:去掉注釋,自動生成文檔,提供錯誤位置(可以通過記錄行號來提供)。

      當語法檢查正確無誤之后,就可以進入運行階段了。

      ####第二階段:運行階段

      #####一:預解析

      第一步:創建執行上下文。JavaScript引擎將語法檢查正確后生成的語法樹復制到當前執行上下文中。

      第二步:屬性填充。JavaScript引擎會對語法樹當中的變量聲明、函數聲明以及函數的形參進行屬性填充。

      “預解析”從語法檢查階段復制過來的信息如下:

      內部變量表varDecls:varDecls保存的用var進行顯式聲明的局部變量。

      內嵌函數表funDecls:在“預解析”階段,發現有函數定義的時候,除了記錄函數的聲明外,還會創建一個原型鏈對象(prototype)。

      …其他的信息。

      ####執行上下文(execution context)

      (一)預解析階段創建的執行上下文包括:變量對象、作用域鏈、this。

      變量對象(Variable Object):由var declaration、function

      declaration(變量聲明、函數聲明)、arguments(參數)構成。變量對象是以單例形式存在。

      作用域鏈(Scope Chain):variable object + all parent

      scopes(變量對象以及所有父級作用域)構成。

      this值:(thisValue):content

      object。this值在進入上下文階段就確定了。一旦進入執行代碼階段,this值就不會變了。

      (二)“預解析”階段創建執行上下文之后,還會對變量對象/活動對象(VO/AO)的一些屬性填充數值。

      (函數申明提升優先級高于變量聲明提升)

      函數的形參:執行上下文的變量對象的一個屬性,其屬性名就是形參的名字,其值就是實參的值;對于沒有傳遞的參數,其值為undefined。

      函數聲明:執行上下文的變量對象的一個屬性,屬性名和值都是函數對象創建出來的;如果變量對象已經包含了相同名字的屬性,則會替換它的值。

      變量聲明:執行上下文的變量對象的一個屬性,其屬性名即為變量名,其值為undefined;如果變量名和已經聲明的函數名或者函數的參數名相同,則不會影響已經存在的函數聲明的屬性。

      變量對象/活動對象(VO/AO)填充的順序也是按照以上順序:函數的形參->函數聲明->變量聲明;

      在變量對象/活動對象(VO/AO)中權重高低也按照函數的形參->函數聲明->變量聲明順序來。

      如下代碼:

      var a=1; function b(a) { alert(a); } var b; alert(b); // function b(a) { alert(a); } b(); //undefined

      1

      2

      3

      4

      5

      6

      7

      以上代碼在進入執行上下文時,按照函數的形參->函數聲明->變量聲明順序來填充,并且優先權永遠都是函數的形參>函數聲明>變量聲明,所以只要alert(a)中的a是函數中的形參,就永遠不會被函數和變量聲明覆蓋。就算沒有賦值也是默認填充的undefined值。

      第二部分:執行代碼

      經過“預解析”創建執行上下文之后,就進入執行代碼階段,VO/AO就會重新賦予真實的值,“預解析”階段賦予的undefined值會被覆蓋。

      此階段才是程序真正進入執行階段,Javascript引擎會一行一行的讀取并運行代碼。此時那些變量都會重新賦值。

      假如變量是定義在函數內的,而函數從頭到尾都沒被激活(調用)的話,則變量值永遠都是undefined值。

      進入了執行代碼階段,在“預解析”階段所創建的任何東西可能都會改變,不僅僅是VO/AO,this和作用域鏈也會因為某些語句而改變,后面會講到。

      了解完Javascript的解析過程最后我們再來了解下firebug的控制臺對Javascript的報錯提示吧。

      其實firebug的控制臺也算是JavaScript的解釋器,而且他們會提示我們哪行出現了錯誤或者錯誤發生在哪個時期,語法檢查階段錯誤,還是運行期錯誤。

      如下:

      alert(var);// SyntaxError: syntax error 語法分析階段錯誤 :語法錯誤 var=1; // SyntaxError: missing variable name 語法分析階段錯誤 :var是保留字符,導致變量名丟失 a=b=v // ReferenceError: v is not defined 運行期錯誤: v 是未定義的JavaScript錯誤信息)

      1

      2

      3

      有如此詳細的錯誤提示,是不是就很快就知道代碼中到底是哪里錯了呢!

      ##作用域鏈(Scope Chain)

      作用域鏈是處理標識符時進行變量查詢的變量對象列表,每個執行上下文都有自己的變量對象:對于全局上下文而言,其變量對象就是全局對象本身;對于函數而言,其變量對象就是活動對象。

      ###作用域鏈以及執行上下文的關系

      在Javascript中只有函數能規定作用域,全局執行上下文中的 Scope 是全局上下文中的屬性,也是最外層的作用域鏈。

      函數的屬性[[Scope]]是在“預解析”的時候就已經存在的了,它包含了所有上層變量對象,并一直保存在函數中。就算函數永遠都沒被激活(調用),[[Scope]]也都還是存在函數對象上。

      創建執行上下文的 Scope 屬性和進入執行上下文的過程如下:

      Scope = AO + [[Scope]] //預解析時的 Scope 屬性

      Scope = [AO].concat([[Scope]]); //執行階段,將AO添加到作用域鏈的最前端

      ###執行上下文定義的 Scope 屬性變化過程

      執行上下文中的[AO]是函數的活動對象,而[[Scope]]則是該函數屬性作用域。當前函數的AO永遠是在最前面的,保存在堆棧上,而每當函數激活的時候,這些AO都會壓棧到該堆棧上,查詢變量是先從棧頂開始查找,也就是說作用域鏈的棧頂永遠是當前正在執行的代碼所在環境的VO/AO(當函數調用結束后,則會從棧頂移除)。

      通俗點講就是:JavaScript解釋器通過作用域鏈將不同執行位置上的變量對象串連成列表,并借助這個列表幫助JavaScript解釋器檢索變量的值。作用域鏈相當于一個索引表,并通過編號來存儲它們的嵌套關系。當JavaScript解釋器檢索變量的值,會按著這個索引編號進行快速查找,直到找到全局對象為止,如果沒有找到值,則傳遞一個特殊的 undefined值。

      是不是又想到了一條JavaScript高效準則:為什么說在該函數內定義的變量,能減少函數嵌套,能提高JavaScript的效率?因為函數定義的變量,此變量永遠在棧頂,這樣子查詢變量的時間變短了。

      ###作用域的特性

      保證有序的訪問所有變量和函數;

      作用域鏈感覺就是一個VO鏈表,當訪問一個變量時,先在鏈表的第一個VO上查找,如果沒有找到則繼續在第二個VO上查找,直到搜索結束,也就是搜索到全局執行環境的VO中。這也就形成了作用域鏈的概念。

      var color="blue"; function changecolor(){ var anothercolor="red"; function swapcolors(){ var tempcolor=anothercolor; anothercolor=color; color=tempcolor; // Todo something } swapcolors(); } changecolor();//這里不能訪問tempcolor和anothercolor;但是可以訪問color; alert("Color is now "+color); 

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      函數的作用域是在函數創建即“預解析”階段就已經就已經定義了,而在代碼執行階段則是將函數的作用域添加到作用域鏈上。

      ##原型鏈查詢

      在介紹“預解析”階段時,我們有提到當創建函數時,同時也會創建原型鏈對象。原型鏈對象在作用域鏈中沒有找到變量時,那么就會通過原型鏈來查找。

      function Foo() { function bar() { alert(x); } bar(); } Object.prototype.x = 10; Foo(); // 10

      1

      2

      3

      4

      5

      6

      7

      8

      上例中在作用域鏈中遍歷查詢,到了全局對象了,該對象繼承自Object.prototype,因此,最終變量“x”的值就變成了10。不過,在原型鏈上定義變量對象有些瀏覽器不支持,譬如IE6,而且這樣增加了變量對象的查詢時間。所以變量聲明盡量在調用函數AO里,即在用到該變量的函數內聲明變量對象。

      作用域是在“預解析”時就已經決定的,所以作用域被叫做靜態作用域,而在執行階段的則被叫做動態鏈,因為在執行階段會改變作用域鏈中填充的值。

      代碼執行階段對“預解析”的改變

      創建了函數就有一個閉包,而變量是在函數的執行上下文保存起來的靜態作用域鏈上查詢的,而當前函數內創建的的變量會在函數結束后就被銷毀。而閉包就能在函數結束之后還能讓這些變量一直保存在作用域鏈上。

      ##自由變量

      自由變量是指在函數中使用的,但既不是函數參數也不是函數局部變量的變量。

      ##閉包

      理論角度:所有函數都是閉包。因為它們都在創建的時候就將上層上下文的數據保存起來了。哪怕是簡單的全局變量也是如此,因為函數中訪問全局變量就相當于是在訪問自由變量。

      應用角度:當在代碼中引用了自由變量,即使創建它的上下文已經銷毀,此變量還能訪問。

      網站開發進階(四十九)由JS報“未結束的字符串常量”引發的思考

      ECMAScript標準中,同一個上下文創建的閉包(理論上的閉包)是共用一個作用域的,也就是說閉包中對其中變量修改會影響到其他閉包對其變量的讀取。

      所謂創建額外的閉包就是創建函數,不管是匿名函數、函數表達式、函數聲明(除了構造函數),只要能創建作用域鏈就行,與函數類型無關,然而創建額外的函數不是唯一的方法。

      遍歷最外層代碼:

      for (i = 0; i < len; i++) {…//①}

      遍歷獲取索引值最外層代碼

      使用函數創建閉包:

      方法一:使用函數閉包獲取索引值

      (function(i){ lists[i].onmouseover = function () { alert(i); }; })(i);

      1

      2

      3

      4

      5

      直接將匿名函數賦值給事件,創建額外的函數來創建多個作用域。

      方法二:使用函數閉包獲取索引值

      lists[i].onmouseover = (function (x) { return function (){ alert(i); }; })(i);

      1

      2

      3

      4

      5

      利用return在閉包中返回,而閉包中返回的語句會將控制流返回給調用上下文,也就是返回幾個就有幾個執行上下文,相應的作用域鏈也有相同的個數。

      使用try { … } catch (ex) { … }改變作用域鏈:

      try-catch改變作用域鏈的原理跟with一樣,try 部分包含需要運行的代碼,而 catch 部分包含錯誤發生時運行的代碼。如下:

      var array = null; var x=10; try { document.write(array[0]); } catch(x) { x =20; document.writeln("catch內的x值"+x); //20 } document.writeln("catch外的x值"+x); //10

      1

      2

      3

      4

      5

      6

      7

      8

      9

      try-catch語句在代碼調試和異常處理中非常有用,因此不建議完全避免。

      ##JavaScript解析引擎與ECMAScript是什么關系

      JavaScript引擎是一段程序,我們寫的JavaScript代碼也是程序,如何讓程序去讀懂程序呢?這就需要定義規則。比如,之前提到的var a = 1 + 1;,它表示:

      左邊var代表了這是申明(declaration),它申明了a這個變量;

      右邊的+表示要將1和1做加法;

      中間的等號表示了這是個賦值語句;

      最后的分號表示這句語句結束了;

      上述這些就是規則,有了它就等于有了衡量的標準,JavaScript引擎就可以根據這個標準去解析JavaScript代碼了。那么這里的ECMAScript就是定義了這些規則。其中ECMAScript 262這份文檔,就是對JavaScript這門語言定義了一整套完整的標準。其中包括:

      var,if,else,break,continue等是JavaScript的關鍵詞;

      abstract,int,long等是JavaScript保留詞;

      怎么樣算是數字、怎么樣算是字符串等等;

      定義了操作符(+,-,>,<等);

      定義了JavaScript的語法;

      定義了對表達式,語句等標準的處理算法,比如遇到==該如何處理;

      ??

      ![這里寫圖片描述](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwOTI3MTk1MzAwMzQ4?x-oss-process=image/format,png)

      ![這里寫圖片描述](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwOTI3MTk1MzEzMDYx?x-oss-process=image/format,png)

      ![這里寫圖片描述](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwOTI3MTk1MzIyNzQ4?x-oss-process=image/format,png)

      JavaScript 云速建站 網站

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:部署Tomcat負載均衡
      下一篇:《云計算與虛擬化技術叢書 Service Mesh實戰》—3.2.3服務注冊:Registrator
      相關文章
      亚洲高清中文字幕免费| 亚洲成a人片在线观看老师| 亚洲国产精品无码中文lv| 久久久久久亚洲AV无码专区| 亚洲国产成人VA在线观看| 亚洲欧洲av综合色无码| 国产人成亚洲第一网站在线播放| 日韩精品一区二区亚洲AV观看| 亚洲成人在线电影| 亚洲欧洲日韩不卡| 久久亚洲精品人成综合网| 亚洲天堂中文字幕| 亚洲91av视频| 亚洲精品资源在线| 亚洲另类古典武侠| 中文字幕乱码亚洲无线三区 | 亚洲av成人中文无码专区| 亚洲91精品麻豆国产系列在线| 亚洲电影一区二区三区| 亚洲娇小性色xxxx| 美女视频黄免费亚洲| 亚洲区视频在线观看| 亚洲精品无码Av人在线观看国产| 亚洲视频在线观看免费视频| 亚洲乱码日产一区三区| 亚洲av综合avav中文| 久久青青草原亚洲av无码app| 亚洲日本国产精华液| 亚洲免费中文字幕| 亚洲精品久久无码| 亚洲精品无码日韩国产不卡?V| 亚洲午夜久久久久妓女影院 | 亚洲日韩精品无码专区加勒比☆| 亚洲高清毛片一区二区| 亚洲成av人在片观看| 亚洲一区二区三区自拍公司| 麻豆亚洲AV永久无码精品久久| 亚洲国产精品免费在线观看| 亚洲色偷偷色噜噜狠狠99网| 伊人久久亚洲综合影院| 亚洲精品成人网站在线观看|