首先說明,本文很長,請泡一杯咖啡,抽出至少半個小時來慢慢回味。html
不少大公司面試喜歡問這樣一道面試題,輸入URL到看見頁面發生了什麼?
,今天咱們來總結一下。 簡單來講,共有如下幾個過程前端
下面咱們來看看具體的細節node
DNS解析實際上就是尋找你所須要的資源的過程。假設你輸入www.baidu.com
,而這個網址並非百度的真實地址,互聯網中每一臺機器都有惟一標識的IP地址,這個纔是關鍵,可是它很差記,亂七八糟一串數字誰記得住啊,因此就須要一個網址和IP地址的轉換,也就是DNS解析。下面看看具體的解析過程面試
DNS解析實際上是一個遞歸的過程 算法
輸入www.google.com
網址後,首先在本地的域名服務器中查找,沒找到去根域名服務器查找,沒有再去
com
頂級域名服務器查找,,如此的類推下去,直到找到IP地址,而後把它記錄在本地,供下次使用。大體過程就是
.
-> .com ->
google.com.
->
www.google.com.
。 (你可能以爲我多寫 .,並木有,這個.對應的就是根域名服務器,默認狀況下全部的網址的最後一位都是.,既然是默認狀況下,爲了方便用戶,一般都會省略,瀏覽器在請求DNS的時候會自動加上)
既然已經懂得了解析的具體過程,咱們能夠看到上述一共通過了N個過程,每一個過程有必定的消耗和時間的等待,所以咱們得想辦法解決一下這個問題!chrome
DNS存在着多級緩存,從離瀏覽器的距離排序的話,有如下幾種: 瀏覽器緩存,系統緩存,路由器緩存,IPS服務器緩存,根域名服務器緩存,頂級域名服務器緩存,主域名服務器緩存。數據庫
在你的chrome瀏覽器中輸入:chrome://dns/,你能夠看到chrome瀏覽器的DNS緩存。json
系統緩存主要存在/etc/hosts(Linux系統)中數組
不知道大家有沒有注意這樣一件事,你訪問baidu.com
的時候,每次響應的並不是是同一個服務器(IP地址不一樣),通常大公司都有成百上千臺服務器來支撐訪問,假設只有一個服務器,那它的性能和存儲量要多大才能支撐這樣大量的訪問呢?DNS能夠返回一個合適的機器的IP給用戶,例如能夠根據每臺機器的負載量,該機器離用戶地理位置的距離等等,這種過程就是DNS負載均衡promise
TCP提供一種可靠的傳輸,這個過程涉及到三次握手,四次揮手,下面咱們詳細看看 TCP提供一種面向鏈接的,可靠的字節流服務。 其首部的數據格式以下
源端口:源端口和IP地址的做用是標識報文的返回地址。
目的端口:端口指明接收方計算機上的應用程序接口。
TCP報頭中的源端口號和目的端口號同IP數據報中的源IP與目的IP惟一肯定一條TCP鏈接。
序號:是TCP可靠傳輸的關鍵部分。序號是該報文段發送的數據組的第一個字節的序號。在TCP傳送的流中,每個字節都有一個序號。好比一個報文段的序號爲300,報文段數據部分共有100字節,則下一個報文段的序號爲400。因此序號確保了TCP傳輸的有序性。
確認號:即ACK,指明下一個期待收到的字節序號,代表該序號以前的全部數據已經正確無誤的收到。確認號只有當ACK標誌爲1時纔有效。好比創建鏈接時,SYN報文的ACK標誌位爲0。
首部長度/數據偏移:佔4位,它指出TCP報文的數據距離TCP報文段的起始處有多遠。因爲首部可能含有可選項內容,所以TCP報頭的長度是不肯定的,報頭不包含任何任選字段則長度爲20字節,4位首部長度字段所能表示的最大值爲1111,轉化爲10進製爲15,15*32/8=60,故報頭最大長度爲60字節。首部長度也叫數據偏移,是由於首部長度實際上指示了數據區在報文段中的起始偏移值。
保留:佔6位,保留從此使用,但目前應都位0。
控制位:URG ACK PSH RST SYN FIN,共6個,每個標誌位表示一個控制功能。
緊急URG:當URG=1,代表緊急指針字段有效。告訴系統此報文段中有緊急數據
確認ACK:僅當ACK=1時,確認號字段纔有效。TCP規定,在鏈接創建後全部報文的傳輸都必須把ACK置1。
推送PSH:當兩個應用進程進行交互式通訊時,有時在一端的應用進程但願在鍵入一個命令後當即就能收到對方的響應,這時候就將PSH=1。
復位RST:當RST=1,代表TCP鏈接中出現嚴重差錯,必須釋放鏈接,而後再從新創建鏈接。
同步SYN:在鏈接創建時用來同步序號。當SYN=1,ACK=0,代表是鏈接請求報文,若贊成鏈接,則響應報文中應該使SYN=1,ACK=1。
終止FIN:用來釋放鏈接。當FIN=1,代表此報文的發送方的數據已經發送完畢,而且要求釋放。
窗口:滑動窗口大小,用來告知發送端接受端的緩存大小,以此控制發送端發送數據的速率,從而達到流量控制。窗口大小時一個16bit字段,於是窗口大小最大爲65535。
校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發送端計算和存儲,並由接收端進行驗證。
緊急指針:只有當 URG 標誌置 1 時緊急指針纔有效。緊急指針是一個正的偏移量,和順序號字段中的值相加表示緊急數據最後一個字節的序號。 TCP 的緊急方式是發送端向另外一端發送緊急數據的一種方式。
選項和填充:最多見的可選字段是最長報文大小,又稱爲MSS(Maximum Segment Size),每一個鏈接方一般都在通訊的第一個報文段(爲創建鏈接而設置SYN標誌爲1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不必定是32位的整數倍,因此要加填充位,即在這個字段中加入額外的零,以保證TCP頭是32的整數倍。
數據部分: TCP 報文段中的數據部分是可選的。在一個鏈接創建和一個鏈接終止時,雙方交換的報文段僅有 TCP 首部。若是一方沒有數據要發送,也使用沒有任何數據的首部來確認收到的數據。在處理超時的許多狀況中,也會發送不帶任何數據的報文段。
須要注意的是: (A)不要將確認序號Ack與標誌位中的ACK搞混了。 (B)確認方Ack=發起方Req+1,兩端配對。
客戶端發送syn包(Seq=x)到服務器,並進入SYN_SEND狀態,等待服務器確認;
服務器收到syn包,必須確認客戶的SYN(ack=x+1),同時本身也發送一個SYN包(Seq=y),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=y+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
握手過程當中傳送的包裏不包含數據,三次握手完畢後,客戶端與服務器才正式開始傳送數據。理想狀態下,TCP鏈接一旦創建,在通訊雙方中的任何一方主動關閉鏈接以前,TCP 鏈接都將被一直保持下去。
創建鏈接的過程是利用客戶服務器模式,假設主機A爲客戶端,主機B爲服務器端。
採用三次握手是爲了防止失效的鏈接請求報文段忽然又傳送到主機B,於是產生錯誤。失效的鏈接請求報文段是指:主機A發出的鏈接請求沒有收到主機B的確認,因而通過一段時間後,主機A又從新向主機B發送鏈接請求,且創建成功,順序完成數據傳輸。考慮這樣一種特殊狀況,主機A第一次發送的鏈接請求並無丟失,而是由於網絡節點致使延遲達到主機B,主機B覺得是主機A又發起的新鏈接,因而主機B贊成鏈接,並向主機A發回確認,可是此時主機A根本不會理會,主機B就一直在等待主機A發送數據,致使主機B的資源浪費。
採用兩次握手不行,緣由就是上面說的失效的鏈接請求的特殊狀況。而在三次握手中, client和server都有一個發syn和收ack的過程, 雙方都是發後能收, 代表通訊則準備工做OK.
爲何不是四次握手呢? 你們應該知道通訊中著名的藍軍紅軍約定, 這個例子說明, 通訊不可能100%可靠, 而上面的三次握手已經作好了通訊的準備工做, 再增長握手, 並不能顯著提升可靠性, 並且也沒有必要。
數據傳輸完畢後,雙方均可釋放鏈接。最開始的時候,客戶端和服務器都是處於ESTABLISHED狀態,假設客戶端主動關閉,服務器被動關閉。
客戶端發送一個FIN,用來關閉客戶端到服務器的數據傳送,也就是客戶端告訴服務器:我已經不 會再給你發數據了(固然,在fin包以前發送出去的數據,若是沒有收到對應的ack確認報文,客戶端依然會重發這些數據),可是,此時客戶端還可 以接受數據。 FIN=1,其序列號爲seq=u(等於前面已經傳送過來的數據的最後一個字節的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即便不攜帶數據,也要消耗一個序號。
服務器收到FIN包後,發送一個ACK給對方而且帶上本身的序列號seq,確認序號爲收到序號+1(與SYN相同,一個FIN佔用一個序號)。此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP服務器通知高層的應用進程,客戶端向服務器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,可是服務器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待服務器發送鏈接釋放報文(在這以前還須要接受服務器發送的最後的數據)。
服務器發送一個FIN,用來關閉服務器到客戶端的數據傳送,也就是告訴客戶端,個人數據也發送完了,不會再給你發數據了。因爲在半關閉狀態,服務器極可能又發送了一些數據,假定此時的序列號爲seq=w,此時,服務器就進入了LAST-ACK(最後確認)狀態,等待客戶端的確認。
主動關閉方收到FIN後,發送一個ACK給被動關閉方,確認序號爲收到序號+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP鏈接尚未釋放,必須通過2∗MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。
服務器只要收到了客戶端發出的確認,當即進入CLOSED狀態。一樣,撤銷TCB後,就結束了此次的TCP鏈接。能夠看到,服務器結束TCP鏈接的時間要比客戶端早一些。
至此,完成四次揮手。
MSL(Maximum Segment Lifetime),TCP容許不一樣的實現能夠設置不一樣的MSL值。
創建鏈接的時候, 服務器在LISTEN狀態下,收到創建鏈接請求的SYN報文後,把ACK和SYN放在一個報文裏發送給客戶端。 而關閉鏈接時,服務器收到對方的FIN報文時,僅僅表示對方再也不發送數據了可是還能接收數據,而本身也未必所有數據都發送給對方了,因此己方能夠當即關閉,也能夠發送一些數據給對方後,再發送FIN報文給對方來表示贊成如今關閉鏈接,所以,己方ACK和FIN通常都會分開發送,從而致使多了一次。
首先科補一個小知識,HTTP的端口爲80/8080,而HTTPS的端口爲443
發送HTTP請求的過程就是構建HTTP請求報文並經過TCP協議中發送到服務器指定端口 請求報文由請求行,請求抱頭,請求正文組成。
請求行的格式爲Method Request-URL HTTP-Version CRLF
eg: GET index.html HTTP/1.1
經常使用的方法有: GET
,POST
, PUT
, DELETE
, OPTIONS
, HEAD
。
這裏主要展現POST
和GET
的區別
常見的區別
注意一點你也能夠在GET裏面藏body,POST裏面帶參數
重點區別
GET
會產生一個TCP
數據包,而POST
會產生兩個TCP
數據包。
詳細的說就是:
對於GET方式的請求,瀏覽器會把http header和data一併發送出去,服務器響應200(返回數據);
而對於POST,瀏覽器先發送header,服務器響應100 continue,瀏覽器再發送data,服務器響應200 ok(返回數據)。
注意一點,並非全部的瀏覽器都會發送兩次數據包,Firefox就發送一次
請求報頭容許客戶端向服務器傳遞請求的附加信息和客戶端自身的信息。
從圖中能夠看出,請求報頭中使用了Accept, Accept-Encoding, Accept-Language, Cache-Control, Connection, Cookie等字段。Accept用於指定客戶端用於接受哪些類型的信息,Accept-Encoding與Accept相似,它用於指定接受的編碼方式。Connection設置爲Keep-alive用於告訴客戶端本次HTTP請求結束以後並不須要關閉TCP鏈接,這樣可使下次HTTP請求使用相同的TCP通道,節省TCP鏈接創建的時間。當使用POST, PUT等方法時,一般須要客戶端向服務器傳遞數據。這些數據就儲存在請求正文中。在請求包頭中有一些與請求正文相關的信息,例如: 如今的Web應用一般採用Rest架構,請求的數據格式通常爲json。這時就須要設置Content-Type: application/json
。
HTTP屬於客戶端緩存,咱們常認爲瀏覽器有一個緩存數據庫,用來保存一些靜態文件,下面咱們分爲如下幾個方面來簡單介紹HTTP緩存
緩存規則分爲強制緩存和協商緩存
當緩存數據庫中有客戶端須要的數據,客戶端直接將數據從其中拿出來使用(若是數據未失效),當緩存服務器沒有須要的數據時,客戶端纔會向服務端請求。
又稱對比緩存。客戶端會先從緩存數據庫拿到一個緩存的標識,而後向服務端驗證標識是否失效,若是沒有失效服務端會返回304,這樣客戶端能夠直接去緩存數據庫拿出數據,若是失效,服務端會返回新的數據
強制緩存的優先級高於協商緩存,若兩種緩存皆存在,且強制緩存命中目標,則協商緩存再也不驗證標識。
上面的內容讓咱們大概瞭解了緩存機制是怎樣運行的,可是,服務器是如何判斷緩存是否失效呢?咱們知道瀏覽器和服務器進行交互的時候會發送一些請求數據和響應數據,咱們稱之爲HTTP報文。報文中包含首部header和主體部分body。與緩存相關的規則信息就包含在header中。boby中的內容是HTTP請求真正要傳輸的部分。舉個HTTP報文header部分的例子以下:
咱們依舊分爲強制緩存和協商緩存來分析。對於強制緩存,服務器響應的header中會用兩個字段來代表——Expires和Cache-Control。
Exprires的值爲服務端返回的數據到期時間。當再次請求時的請求時間小於返回的此時間,則直接使用緩存數據。但因爲服務端時間和客戶端時間可能有偏差,這也將致使緩存命中的偏差,另外一方面,Expires是HTTP1.0的產物,故如今大多數使用Cache-Control替代。
Cache-Control有不少屬性,不一樣的屬性表明的意義也不一樣。
協商緩存須要進行對比判斷是否可使用緩存。瀏覽器第一次請求數據時,服務器會將緩存標識與數據一塊兒響應給客戶端,客戶端將它們備份至緩存中。再次請求時,客戶端會將緩存中的標識發送給服務器,服務器根據此標識判斷。若未失效,返回304狀態碼,瀏覽器拿到此狀態碼就能夠直接使用緩存數據了。
對於協商緩存來講,緩存標識咱們須要着重理解一下,下面咱們將着重介紹它的兩種緩存方案。
Last-Modified:服務器在響應請求時,會告訴瀏覽器資源的最後修改時間。
if-Modified-Since
:瀏覽器再次請求服務器的時候,請求頭會包含此字段,後面跟着在緩存中得到的最後修改時間。服務端收到此請求頭髮現有if-Modified-Since,則與被請求資源的最後修改時間進行對比,若是一致則返回304和響應報文頭,瀏覽器只須要從緩存中獲取信息便可。 從字面上看,就是說:從某個時間節點算起,是否文件被修改了
if-Unmodified-Since
:從字面上看, 就是說: 從某個時間點算起, 是否文件沒有被修改
這兩個的區別是一個是修改了才下載一個是沒修改才下載。
Last-Modified 說好卻也不是特別好,由於若是在服務器上,一個資源被修改了,但其實際內容根本沒發生改變,會由於Last-Modified時間匹配不上而返回了整個實體給客戶端(即便客戶端緩存裏有個如出一轍的資源)。爲了解決這個問題,HTTP1.1推出了Etag。
Etag:服務器響應請求時,經過此字段告訴瀏覽器當前資源在服務器生成的惟一標識(生成規則由服務器決定)
If-None-Match:再次請求服務器時,瀏覽器的請求報文頭部會包含此字段,後面的值爲在緩存中獲取的標識。服務器接收到次報文後發現If-None-Match則與被請求資源的惟一標識進行對比。
可是實際應用中因爲Etag的計算是使用算法來得出的,而算法會佔用服務端計算的資源,全部服務端的資源都是寶貴的,因此就不多使用Etag了。
它會對TCP鏈接進行處理,對HTTP協議進行解析,並按照報文格式進一步封裝成HTTP Request對象,供上層使用。這一部分工做通常是由Web服務器去進行,我使用過的Web服務器有Tomcat, Nginx和Apache等等 HTTP報文也分紅三份,狀態碼 ,響應報頭和響應報文
狀態碼是由3位數組成,第一個數字定義了響應的類別,且有五種可能取值:
1xx:指示信息–表示請求已接收,繼續處理。
2xx:成功–表示請求已被成功接收、理解、接受。
3xx:重定向–要完成請求必須進行更進一步的操做。
4xx:客戶端錯誤–請求有語法錯誤或請求沒法實現。
5xx:服務器端錯誤–服務器未能實現合法的請求。 平時遇到比較常見的狀態碼有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500
請求成功,一般服務器提供了須要的資源。
服務器成功處理了請求,但沒有返回任何內容。
請求的網頁已永久移動到新位置。 服務器返回此響應(對 GET 或 HEAD 請求的響應)時,會自動將請求者轉到新位置。
服務器目前從不一樣位置的網頁響應請求,但請求者應繼續使用原有位置來進行之後的請求。
自從上次請求後,請求的網頁未修改過。 服務器返回此響應時,不會返回網頁內容。
服務器不理解請求的語法。
請求要求身份驗證。 對於須要登陸的網頁,服務器可能返回此響應。
服務器拒絕請求。
服務器找不到請求的網頁。
請求格式正確,可是因爲含有語義錯誤,沒法響應
服務器遇到錯誤,沒法完成請求。
常見的響應報頭字段有: Server, Connection...。
你從服務器請求的HTML,CSS,JS文件就放在這裏面
Webkit
解析渲染頁面的過程。
當Render Tree中部分或所有元素的尺寸、結構、或某些屬性發生改變時,瀏覽器從新渲染部分或所有文檔的過程稱爲迴流。
會致使迴流的操做:
一些經常使用且會致使迴流的屬性和方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
當頁面中元素樣式的改變並不影響它在文檔流中的位置時(例如:color、background-color、visibility等),瀏覽器會將新樣式賦予給元素並從新繪製它,這個過程稱爲重繪。
JS的解析是由瀏覽器的JS引擎完成的。因爲JavaScript是單進程運行,也就是說一個時間只能幹一件事,幹這件事情時其餘事情都有排隊,可是有些人物比較耗時(例如IO操做),因此將任務分爲同步任務和異步任務,全部的同步任務放在主線程上執行,造成執行棧,而異步任務等待,當執行棧被清空時纔去看看異步任務有沒有東西要搞,有再提取到主線程執行,這樣往復循環(冤冤相報什麼時候了,阿彌陀佛),就造成了Event Loop事件循環,下面來看看大人物
先看一段代碼
setTimeout(function(){
console.log('定時器開始啦')
});
new Promise(function(resolve){
console.log('立刻執行for循環啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('執行then函數啦')
});
console.log('代碼執行結束');
複製代碼
結果我想你們都應該知道。主要來介紹JavaScript的解析,至於Promise等下一節再說
JavaScript是一門單線程語言,儘管H5中提出了Web-Worker
,可以模擬實現多線程,但本質上仍是單線程,說它是多線程就是扯淡。
既然是單線程,每一個事件的執行就要有順序,好比你去銀行取錢,前面的人在進行,後面的就得等待,要是前面的人弄個一兩個小時,估計後面的人都瘋了,所以,瀏覽器的JS引擎處理JavaScript時分爲同步任務和異步任務
這張圖咱們能夠清楚看到js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。 估計看完這些你對事件循環有必定的瞭解,可是事實上咱們看對的沒這麼簡單,一般咱們會看到Promise,setTimeout,process.nextTick(),這個時候你和我就懵逼。
除了同步任務和異步任務,咱們還分爲宏任務和微任務,常見的有如下幾種
script裏面先執行,不過我喜歡把它拎出來,直接稱其進入執行棧
),當主線程執行棧所有任務被清空後去微任務看看,若是有等待執行的任務,執行所有的微任務(其實將其回調函數推入執行棧來執行),再去宏任務找最早進入隊列的任務執行,執行這個任務後再去主線程執行任務(例如執行```console.log("hello world")這種任務),執行棧被清空後再去微任務,這樣往復循環(冤冤相報什麼時候了)Tip:微任務會所有執行,而宏任務會一個一個來執行
下面來看一段代碼
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
複製代碼
咱們看看它的執行狀況
promise
,遇到then
,將其分發到微任務console.log("console")
,直接輸出console
then
setTimeout
,總體執行完畢。 具體的執行過程大體就是這樣,可能我有疏忽的地方,還望指正。
再來看看一段複雜的代碼console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
複製代碼
咱們來分析一下
console.log('1')
,直接輸出setTimeout
,將其回調函數分發到宏任務事件隊列,暫時標記爲setTimeout1process.nextTick()
,將其回調函數分發到微任務事件隊列,標記爲process.nextTick1
(這個地方有點出入,我通常認爲```process.nextTick()推入主線程執行棧棧底,做爲執行棧最後一個任務執行)7
,then函數分發的微任務事件隊列,標記爲Promise1。1,7
,宏任務和微任務的事件隊列 狀況以下
咱們接着來看process.nextTick1
,輸出6
,接着執行Promise1
,輸出8
。至此,第一輪循環已經結束,輸出了
1,7,6,8
,接下來執行第二輪循環 ,先從宏任務的setTimeout1
開始
console.log('2')
,執行輸出。process.nextTick()
,將其回調函數分發到微任務,標記爲process.nextTick2
,又遇到 Promise
,當即執行,輸出4
,將then函數推入微任務事件隊列,標記爲Promise2
2,4
,來看看事件隊列
process.nextTick2
,輸出3
,接着再來執行Promise2
,輸出5
。 第二輪循環執行完畢。如今一共輸出了1,7,6,8,2,4,3,5
setTimeout2
開始第三輪循環 ,先直接輸出9
,遇到process.nextTick()
,將其回調函數分發到微任務事件隊列,標記爲process.nextTick3
,又遇到噁心的Promise,當即執行輸出11
,將then函數分發到微任務,標記爲Promise3。10,12
。 至此,所有任務執行完畢,輸出順序爲1,7,6,8,2,4,3,5,9,11,10,12
.注意,這段代碼執行結果可能與node等環境不一樣而發生變化。
我想說的也說完了,不知道您懂了嘛
這篇文章由一個簡單的問題扯出了不少前端工程師必學也是很重要的東西,可是因爲我本人水平較低,不少地方都是一筆帶過,甚至有些地方還有錯誤,望各位同仁指正批評。