寫這篇文章之前,小編工作中從來沒有問過自己這個問題,不就是寫代碼,編譯器將代碼編輯成計算機能識別的01代碼,有什么好了解的。其實不然,編譯器在將JS代碼變成可執行代碼,做了很多繁雜的工作,只有深入了解背后編譯的原理,我們才能寫出更優質的代碼,了解各種前端框架背后的本質。為了寫這篇文章,小編也是誠惶誠恐,閱讀了相關的資料,也是一個學習了解的過程,難免有些問題,歡迎各位指正,共同提高。
現在的生活節奏和壓力,也許讓我們透不過氣,我們日復一日的寫著代碼,疲于學習各種各樣前端框架,學習的速度總是趕不上更新的速度,經常去尋找解決問題或修復BUG的最佳方式,卻很少有時間去真正的靜下心來研究我們最基礎工具——JavaScript語言。不知道大家是否還記得自己孩童時代,看到一個新鮮的事物或玩具,是否有很強的好奇心,非要打破砂鍋問你到底。但是在我們的工作中,遇到的各種代碼問題,你是否有很強的好奇心,一探究竟,還是把這些問題加入"黑名單",下次不用而已,不知所以然。
其實我們應該重回孩童時代,不應滿足會用,只讓代碼工作而已,我們應該弄清楚"為什么",只有這樣你才能擁抱整個JavaScript。掌握了這些知識后,無論什么技術、框架你都能輕松理解,這也前端達人公眾號一直更新javaScript基礎的原因。
語言和環境是兩個不同的概念。提及JavaScript,大多數人可能會想到瀏覽器,脫離瀏覽器JavaScipt是不可能運行的,這與其他系統級的語言有著很大的不同。例如C語言可以開發系統和制造環境,而JavaScript只能寄生在某個具體的環境中才能夠工作。
JavaScipt運行環境一般都有宿主環境和執行期環境。如下圖所示:
宿主環境是由外殼程序生成的,比如瀏覽器就是一個外殼環境(但是瀏覽器并不是唯一,很多服務器、桌面應用系統都能也能夠提供JavaScript引擎運行的環境)。執行期環境則有嵌入到外殼程序中的JavaScript引擎(比如V8引擎,稍后會詳細介紹)生成,在這個執行期環境,首先需要創建一個代碼解析的初始環境,初始化的內容包含:
雖然,不同的JavaScript引擎定義初始化環境是不同的,這就形成了所謂的瀏覽器兼容性問題,因為不同的瀏覽器使用不同JavaScipt引擎。不過最近的這條消息想必大家都知道——瀏覽器市場,微軟居然放棄了自家的EDGE(IE的繼任者),轉而投靠競爭對手Google主導的Chromium核心(國產瀏覽器百度、搜狗、騰訊、獵豹、UC、傲游、360用的都是Chromium(Chromium用的是鼎鼎大名的V8引擎,想必大家都十分清楚吧),可以認為全是Chromium的馬甲),真是大快人心,我們終于在同一環境下愉快的編寫代碼了,想想真是開心!
一提起JavaScript語言,大部分的人都將其歸類為“動態”或“解釋執行”語言,其實他是一門“編譯性”語言。與傳統的編譯語言不同,它不是提前編譯的,編譯結果也不能在分布式系統中進行移植。在介紹JavaScript編譯器原理之前,小編和大家一起重溫下基本的編譯器原理,因為這是最基礎的,了解清楚了我們更能了解JavaScript編譯器。
編譯程序一般步驟分為:詞法分析、語法分析、語義檢查、代碼優化和生成字節碼。具體的編譯流程如下圖:
分詞/詞法分析(Tokenizing/Lexing)
所謂的分詞,就好比我們將一句話,按照詞語的最小單位進行分割。計算機在編譯一段代碼前,也會將一串串代碼拆解成有意義的代碼塊,這些代碼塊被稱為詞法單元(token)。例如,考慮程序var a=2。這段程序通常會被分解成為下面這些詞法單元:var、a、=、2、;空格是否作為當為詞法單位,取決于空格在這門語言中是否具有意義。
解析/語法分析(Parsing)
這個過程是將詞法單元流轉換成一個由元素逐級嵌套所組成的代表了程序語法結構的樹。這個樹稱為“抽象語法樹”(Abstract Syntax Tree,AST)。
詞法分析和語法分析不是完全獨立的,而是交錯進行的,也就是說,詞法分析器不會在讀取所有的詞法記號后再使用語法分析器來處理。在通常情況下,每取得一個詞法記號,就將其送入語法分析器進行分析。
語法分析的過程就是把詞法分析所產生的記號生成語法樹,通俗地說,就是把從程序中收集的信息存儲到數據結構中。注意,在編譯中用到的數據結構有兩種:符號表和語法樹。
符號表:就是在程序中用來存儲所有符號的一個表,包括所有的字符串變量、直接量字符串,以及函數和類。
語法樹:就是程序結構的一個樹形表示,用來生成中間代碼。下面是一個簡單的條件結構和輸出信息代碼段,被語法分析器轉換為語法樹之后,如
if( typeof a==" undefined"){ a=0; }else{ a=a; } alert( a);
如果JavaScript解釋器在構造語法樹的時候發現無法構造,就會報語法錯誤,并結束整個代碼塊的解析。對于傳統強類型語言來說,在通過語法分析構造出語法樹后,翻譯出來的句子可能還會有模糊不清的地方,需要進一步的語義檢查。語義檢查的主要部分是類型檢查。例如,函數的實參和形參類型是否匹配。但是,對于弱類型語言來說,就沒有這一步。
經過編譯階段的準備, JavaScript代碼在內存中已經被構建為語法樹,然后 JavaScript引擎就會根據這個語法樹結構邊解釋邊執行。
代碼生成
將AST轉換成可執行代碼的過程被稱為代碼生成。這個過程與語言、目標平臺相關。
了解完編譯原理后,其實JavaScript引擎要復雜的許多,因為大部分情況,JavaScript的編譯過程不是發生在構建之前,而是發生在代碼執行前的幾微妙,甚至時間更短。為了保證性能最佳,JavaScipt使用了各種辦法,稍后小編將會詳細介紹。
由于JavaScipt大多數都是運行在瀏覽器上,不同瀏覽器的使用的引擎也各不相同,以下是目前主流瀏覽器引擎:
由于谷歌的V8編譯器的出現,由于性能良好吸引了相當的注目,正式由于V8的出現,我們目前的前端才能大放光彩,百花齊放,V8引擎用C++進行編寫, 作為一個 JavaScript 引擎,最初是服役于 Google Chrome 瀏覽器的。它隨著 Chrome 的第一版發布而發布以及開源。現在它除了 Chrome 瀏覽器,已經有很多其他的使用者了。諸如 NodeJS、MongoDB、CouchDB 等。最近最讓人振奮前端新聞莫過于微軟居然放棄了自家的EDGE(IE的繼任者),轉而投靠競爭對手Google主導的Chromium核心(國產瀏覽器百度、搜狗、騰訊、獵豹、UC、傲游、360用的都是Chromium(Chromium用的是鼎鼎大名的V8引擎,想必大家都十分清楚吧),看來V8引擎在不久的將來就會一統江湖,下面小編將重點介紹V8引擎。
當 V8 編譯JavaScript 代碼時,解析器(parser)將生成一個抽象語法樹(上一小節已介紹過)。語法樹是 JavaScript 代碼的句法結構的樹形表示形式。解釋器 Ignition 根據語法樹生成字節碼。TurboFan 是 V8 的優化編譯器,TurboFan將字節碼(Bytecode)生成優化的機器代碼(Machine Code)。
V8曾經有兩個編譯器
在5.9版本之前,該引擎曾經使用了兩個編譯器:
full-codegen - 一個簡單而快速的編譯器,可以生成簡單且相對較慢的機器代碼。
Crankshaft - 一種更復雜的(即時)優化編譯器,可生成高度優化的代碼。
V8引擎還在內部使用多個線程:
主線程:獲取代碼,編譯代碼然后執行它
優化線程:與主線程并行,用于優化代碼的生成
Profiler線程:它將告訴運行時我們花費大量時間的方法,以便Crankshaft可以優化它們
其他一些線程來處理垃圾收集器掃描
字節碼
字節碼是機器代碼的抽象。如果字節碼采用和物理 CPU 相同的計算模型進行設計,則將字節碼編譯為機器代碼更容易。這就是為什么解釋器(interpreter)常常是寄存器或堆棧。 Ignition 是具有累加器的寄存器。
您可以將 V8 的字節碼看作是小型的構建塊(bytecodes as small building blocks),這些構建塊組合在一起構成任何 JavaScript 功能。V8 有數以百計的字節碼。比如 Add 或 TypeOf 這樣的操作符,或者像 LdaNamedProperty 這樣的屬性加載符,還有很多類似的字節碼。 V8還有一些非常特殊的字節碼,如 CreateObjectLiteral 或 SuspendGenerator。頭文件bytecodes.h(https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h) 定義了 V8 字節碼的完整列表。
在早期的V8引擎里,在多數瀏覽器都是基于字節碼的,V8引擎偏偏跳過這一步,直接將jS編譯成機器碼,之所以這么做,就是節省了時間提高效率,但是后來發現,太占用內存了。最終又退回字節碼了,之所以這么做的動機是什么呢?
每個字節碼指定其輸入和輸出作為寄存器操作數。Ignition 使用寄存器 r0,r1,r2,... 和累加器寄存器(accumulator register)。幾乎所有的字節碼都使用累加器寄存器。它像一個常規寄存器,除了字節碼沒有指定。 例如,Add r1 將寄存器 r1 中的值和累加器中的值進行加法運算。這使得字節碼更短,節省內存。
許多字節碼以 Lda 或 Sta 開頭。Lda 和 Stastands 中的 a 為累加器(accumulator)。例如,LdaSmi [42] 將小整數(Smi)42 加載到累加器寄存器中。Star r0 將當前在累加器中的值存儲在寄存器 r0 中。
以現在掌握的基礎知識,花點時間來看一個具有實際功能的字節碼。
function incrementX(obj) { return 1 + obj.x; } incrementX({x: 42}); // V8 的編譯器是惰性的,如果一個函數沒有運行,V8 將不會解釋它
如果要查看 V8 的 JavaScript 字節碼,可以使用在命令行參數中添加 --print-bytecode 運行 D8 或Node.js(8.3 或更高版本)來打印。對于 Chrome,請從命令行啟動 Chrome,使用 --js-flags="--print-bytecode",請參考 Run Chromium with flags。
$ node --print-bytecode incrementX.js ... [generating bytecode for function: incrementX] Parameter count 2 Frame size 8 12 E> 0x2ddf8802cf6e @ StackCheck 19 S> 0x2ddf8802cf6f @ LdaSmi [1] 0x2ddf8802cf71 @ Star r0 34 E> 0x2ddf8802cf73 @ LdaNamedProperty a0, [0], [4] 28 E> 0x2ddf8802cf77 @ Add r0, [6] 36 S> 0x2ddf8802cf7a @ Return Constant pool (size=1) 0x2ddf8802cf21: [FixedArray] in OldSpace - map=0x2ddfb2d02309 <Map(HOLEY_ELEMENTS)> - length: 1 0: 0x2ddf8db91611 <String[1]: x> Handler Table (size=16)
我們忽略大部分輸出,專注于實際的字節碼。
這是每個字節碼的意思,每一行:
LdaSmi [1]
LdaSmi [1] 將常量 1 加載到累加器中。
Star r0
接下來,Star r0 將當前在累加器中的值 1 存儲在寄存器 r0 中。
LdaNamedProperty a0, [0], [4]
LdaNamedProperty 將 a0 的命名屬性加載到累加器中。ai 指向 incrementX() 的第 i 個參數。在這個例子中,我們在 a0 上查找一個命名屬性,這是 incrementX() 的第一個參數。該屬性名由常量 0 確定。LdaNamedProperty 使用 0 在單獨的表中查找名稱:
- length: 1 0: 0x2ddf8db91611 <String[1]: x>
可以看到,0 映射到了 x。因此這行字節碼的意思是加載 obj.x。
那么值為 4 的操作數是干什么的呢? 它是函數 incrementX() 的反饋向量的索引。反饋向量包含用于性能優化的 runtime 信息。
現在寄存器看起來是這樣的:
Add r0, [6]
最后一條指令將 r0 加到累加器,結果是 43。 6 是反饋向量的另一個索引。
Return 返回累加器中的值。返回語句是函數 incrementX() 的結束。此時 incrementX() 的調用者可以在累加器中獲得值 43,并可以進一步處理此值。
由于JavaScript弱語言的特性(一個變量可以賦值不同的數據類型),同時很彈性,允許我們在任何時候在對象上新增或是刪除屬性和方法等, JavaScript語言非常動態,我們可以想象會大大增加編譯引擎的難度,盡管十分困難,但卻難不倒V8引擎,v8引擎運用了好幾項技術達到加速的目的:
內聯(Inlining):
內聯特性是一切優化的基礎,對于良好的性能至關重要,所謂的內聯就是如果某一個函數內部調用其它的函數,編譯器直接會將函數中的執行內容,替換函數方法。如下圖所示:
如何理解呢?看如下代碼
function add(a, b) { return a + b; } function calculateTwoPlusFive() { var sum; for (var i=0; i <=1000000000; i++) { sum=add(2+5); } } var start=new Date(); calculateTwoPlusFive(); var end=new Date(); var timeTaken=end.valueOf() - start.valueOf(); console.log("Took " + timeTaken + "ms");
由于內聯屬性特性,在編譯前,代碼將會被優化成
function add(a, b) { return a + b; } function calculateTwoPlusFive() { var sum; for (var i=0;i<=1000000000;i++){ sum=2 + 5; } } var start=new Date(); calculateTwoPlusFive(); var end=new Date(); var timeTaken=end.valueOf() - start.valueOf(); console.log("Took " + timeTaken + "ms");
如果沒有內聯屬性的特性,你能想想運行的有多慢嗎?把第一段JS代碼嵌入HTML文件里,我們用不同的瀏覽器打開(硬件環境:i7,16G內存,mac系統),用safari打開如下圖所示,17秒:
如果用Chrome打開,還不到1秒,快了16秒!
隱藏類(Hidden class):
例如C++/Java這種靜態類型語言的每一個變量,都有一個唯一確定的類型。因為有類型信息,一個對象包含哪些成員和這些成員在對象中的偏移量等信息,編譯階段就可確定,執行時CPU只需要用對象首地址 —— 在C++中是this指針,加上成員在對象內部的偏移量即可訪問內部成員。這些訪問指令在編譯階段就生成了。
但對于JavaScript這種動態語言,變量在運行時可以隨時由不同類型的對象賦值,并且對象本身可以隨時添加刪除成員。訪問對象屬性需要的信息完全由運行時決定。為了實現按照索引的方式訪問成員,V8“悄悄地”給運行中的對象分了類,在這個過程中產生了一種V8內部的數據結構,即隱藏類。隱藏類本身是一個對象。
考慮以下代碼:
function Point(x, y) { this.x=x; this.y=y; } var p1=new Point(1, 2);
如果new Point(1, 2)被調用,v8引擎就會創建一個引隱藏的類C0,如下圖所示:
由于Point沒有定于任何屬性,因此“C0”為空
一旦“this.x=x”被執行,v8引擎就會創建一個名為“C1”的第二個隱藏類。基于“c0”,“c1”描述了可以找到屬性X的內存中的位置(相當指針)。在這種情況下,隱藏類則會從C0切換到C1,如下圖所示:
每次向對象添加新的屬性時,舊的隱藏類會通過路徑轉換切換到新的隱藏類。由于轉換的重要性,因為引擎允許以相同的方式創建對象來共享隱藏類。如果兩個對象共享一個隱藏類的話,并且向兩個對象添加相同的屬性,轉換過程中將確保這兩個對象使用相同的隱藏類和附帶所有的代碼優化。
當執行this.y=y,將會創建一個C2的隱藏類,則隱藏類更改為C2。
隱藏類的轉換的性能,取決于屬性添加的順序,如果添加順序的不同,效果則不同,如以下代碼:
function Point(x, y) { this.x=x; this.y=y; } var p1=new Point(1, 2); p1.a=5; p1.b=6; var p2=new Point(3, 4); p2.b=7; p2.a=8;
你可能以為P1、p2使用相同的隱藏類和轉換,其實不然。對于P1對象而言,隱藏類先a再b,對于p2而言,隱藏類則先b后a,最終會產生不同的隱藏類,增加編譯的運算開銷,這種情況下,應該以相同的順序動態的修改對象屬性,以便可以復用隱藏類。
內聯緩存(Inline caching)
正常訪問對象屬性的過程是:首先獲取隱藏類的地址,然后根據屬性名查找偏移值,然后計算該屬性的地址。雖然相比以往在整個執行環境中查找減小了很大的工作量,但依然比較耗時。能不能將之前查詢的結果緩存起來,供再次訪問呢?當然是可行的,這就是內嵌緩存。
內嵌緩存的大致思路就是將初次查找的隱藏類和偏移值保存起來,當下次查找的時候,先比較當前對象是否是之前的隱藏類,如果是的話,直接使用之前的緩存結果,減少再次查找表的時間。當然,如果一個對象有多個屬性,那么緩存失誤的概率就會提高,因為某個屬性的類型變化之后,對象的隱藏類也會變化,就與之前的緩存不一致,需要重新使用以前的方式查找哈希表。
內存管理:
內存的管理組要由分配和回收兩個部分構成。V8的內存劃分如下:
垃圾回收:
V8 使用了分代和大數據的內存分配,在回收內存時使用精簡整理的算法標記未引用的對象,然后消除沒有標記的對象,最后整理和壓縮那些還未保存的對象,即可完成垃圾回收。為了控制 GC 成本并使執行更加穩定, V8 使用增量標記, 而不是遍歷整個堆, 它試圖標記每個可能的對象, 它只遍歷一部分堆, 然后恢復正常的代碼執行. 下一次 GC 將繼續從之前的遍歷停止的位置開始. 這允許在正常執行期間非常短的暫停. 如前所述, 掃描階段由單獨的線程處理.
熱點函數直接編譯成機器碼(優化回退):
V8 為了進一步提升JavaScript代碼的執行效率,編譯器生直接生成更高效的機器碼。程序在運行時,V8會采集JavaScript代碼運行數據。當V8發現某函數執行頻繁(內聯函數機制),就將其標記為熱點函數。針對熱點函數,V8的策略較為樂觀,傾向于認為此函數比較穩定,類型已經確定,于是編譯器,生成更高效的機器碼。后面的運行中,萬一遇到類型變化,V8采取將JavaScript函數回退到優化前的編譯成機器字節碼。如以下代碼:
function add(a, b){ return a + b } for(var i=0; i<10000; ++i){ add(i, i); } add('a', 'b');//千萬別這么做!
再來看下面的一個例子:
// 片段 1 var person={ add: function(a, b){ return a + b; } }; obj.name='li'; // 片段 2 var person={ add: function(a, b){ return a + b; }, name: 'li' };
以上代碼實現的功能相同,都是定義了一個對象,這個對象具有一個屬性name和一個方法add()。但使用片段2的方式效率更高。片段1給對象obj添加了一個屬性name,這會造成隱藏類的派生。給對象動態地添加和刪除屬性都會派生新的隱藏類。假如對象的add函數已經被優化,生成了更高效的代碼,則因為添加或刪除屬性,這個改變后的對象無法使用優化后的代碼。
從例子中我們可以看出:
函數內部的參數類型越確定,V8越能夠生成優化后的代碼。
好了,本篇的內容終于完了,說了這么多,你是否真正的理解了,我們如何迎合編譯器的嗜好編寫更優化的代碼呢?
接下來小編將和大家繼續分享作用域的內容,敬請期待...
更多精彩內容,請微信關注”前端達人”公眾號!
文:https://www.cnblogs.com/dadouF4/p/10032888.html
JavaScript:
基本概念:
JavaScript一種直譯式腳本語言,是一種動態類型、弱類型、基于原型的語言,內置支持類型。它的解釋器被稱為JavaScript引擎,為瀏覽器的一部分,廣泛用于瀏覽器客戶端的腳本語言。
組成部分
ECMAScript,描述了該語言的語法和基本對象。
文檔對象模型(DOM),描述處理網頁內容的方法和接口。
瀏覽器對象模型(BOM),描述與瀏覽器進行交互的方法和接口。
基本特點
JavaScript是一種屬于網絡的腳本語言,已經被廣泛用于Web應用開發,常用來為網頁添加各式各樣的動態功能,為用戶提供更流暢美觀的瀏覽效果。通常JavaScript腳本是通過嵌入在HTML中來實現自身的功能的。
是一種解釋性腳本語言(代碼不進行預編譯)。
主要用來向HTML(標準通用標記語言下的一個應用)頁面添加交互行為。
可以直接嵌入HTML頁面,但寫成單獨的js文件有利于結構和行為的分離。
跨平臺特性,在絕大多數瀏覽器的支持下,可以在多種平臺下運行(如Windows、Linux、Mac、Android、iOS等)。
Javascript腳本語言同其他語言一樣,有它自身的基本數據類型,表達式和算術運算符及程序的基本程序框架。Javascript提供了四種基本的數據類型和兩種特殊數據類型用來處理數據和文字。而變量提供存放信息的地方,表達式則可以完成較復雜的信息處理。
日常用途
嵌入動態文本于HTML頁面。
對瀏覽器事件做出響應。
讀寫HTML元素。
在數據被提交到服務器之前驗證數據。
檢測訪客的瀏覽器信息。
控制cookies,包括創建和修改等。
基于Node.js技術進行服務器端編程。
TypeScript:
基本概念:
TypeScript是一種由微軟開發的自由和開源的編程語言。它是JavaScript的一個超集,而且本質上向這個語言添加了可選的靜態類型和基于類的面向對象編程。安德斯·海爾斯伯格,C#的首席架構師,已工作于TypeScript的開發。
TypeScript擴展了JavaScript的語法,所以任何現有的JavaScript程序可以不加改變的在TypeScript下工作。TypeScript是為大型應用之開發而設計,而編譯時它產生 JavaScript 以確保兼容性。
TypeScript 支持為已存在的 JavaScript 庫添加類型信息的頭文件,擴展了它對于流行的庫如 jQuery,MongoDB,Node.js 和 D3.js 的好處。
特性
類 Classes
接口 Interfaces
模塊 Modules
類型注解 Type annotations
編譯時類型檢查 Compile time type checking
Arrow 函數 (類似 C# 的 Lambda 表達式)
JavaScript 與 TypeScript 的區別
TypeScript 是 JavaScript 的超集,擴展了 JavaScript 的語法,因此現有的 JavaScript 代碼可與 TypeScript 一起工作無需任何修改,TypeScript 通過類型注解提供編譯時的靜態類型檢查。
TypeScript 可處理已有的 JavaScript 代碼,并只對其中的 TypeScript 代碼進行編譯。
JSX:
JSX就是Javascript和XML結合的一種格式。React發明了JSX,利用HTML語法來創建虛擬DOM。當遇到<,JSX就當HTML解析,遇到{就當JavaScript解析。
jsx常用語法:
嘍,大家好,我是雷工!
今天繼續學習JavaScript基礎語法,JS的書寫位置,俗話說:好記性不如爛筆頭,邊學邊記,方便回顧。
代碼寫在標簽內部
示例:
<body>
<button onclick="alert('你還真信呀?~')">點擊關注【雷工筆記】月薪過萬</button>
</body>
2.1、要將JS代碼直接寫在HTML文件里面。
2.2、在HTML文件中添加一個script,用script標簽包住,script標簽中的代碼就是JS代碼。
2.3、script標簽的位置可以在HTML文件中的任何地方,但推薦在head標簽中或者body標簽中。
示例:
<body>
<script>
alert('hello,歡迎關注雷工筆記')
</script>
</body>
雷工提醒:
我們習慣將<script>標簽放在HTML文件的底部附近,原因是瀏覽器會按照代碼在文件中的順序加載HTML。
如果先加載的JS代碼希望修改其下方的HTML,那么其可能因為要修改的HTML還未被加載而失效。所以比較穩妥的策略是將JS代碼放在html文件的底部附近。
3.1、先創建一個JS文件,后綴名是xxxx.js。
3.2、使用script標簽引入JS文件。
示例:
<body>
<!--用src引入外部JS文件-->
<script src="leigong.js"></script>
</body>
雷工提醒:
外聯式JavaScript會讓代碼看上去更加有序,更容易復用,且沒有了腳本的混淆,html也更容易閱讀,因此這是值得我們學習的好習慣。
4.1、外聯式中,script標簽的位置可以在HTML文件中的任何地方,但推薦在body標簽中,盡量寫到文件末尾</body>
前面。
4.2、JS中內嵌式寫法和外聯式寫法不可以混合使用,如果外聯式寫法,script標簽中間就不可以再寫代碼,否則會被忽略,只執行外聯部分。
以上是關于JavaScript基礎中書寫位置的相關知識的筆記,有不當之處還望指正。
想起一句話,貌似是錘子科技發布會上聽到的:從來沒有什么失敗的人,只有半途而廢的人。
每天進步一點點,加油。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。