能每一個(gè)前端工程師都想要理解瀏覽器的工作原理。
我們希望知道從在瀏覽器地址欄中輸入 url 到頁面展現(xiàn)的短短幾秒內(nèi)瀏覽器究竟做了什么;
我們希望了解平時(shí)常常聽說的各種代碼優(yōu)化方案是究竟為什么能起到優(yōu)化的作用;
我們希望更細(xì)化的了解瀏覽器的渲染流程。
瀏覽器的多進(jìn)程架構(gòu)
一個(gè)好的程序常常被劃分為幾個(gè)相互獨(dú)立又彼此配合的模塊,瀏覽器也是如此,以 Chrome 為例,它由多個(gè)進(jìn)程組成,每個(gè)進(jìn)程都有自己核心的職責(zé),它們相互配合完成瀏覽器的整體功能,每個(gè)進(jìn)程中又包含多個(gè)線程,一個(gè)進(jìn)程內(nèi)的多個(gè)線程也會(huì)協(xié)同工作,配合完成所在進(jìn)程的職責(zé)。
對(duì)一些前端開發(fā)同學(xué)來說,進(jìn)程和線程的概念可能會(huì)有些模糊,為了更好的理解瀏覽器的多進(jìn)程架構(gòu),這里我們簡(jiǎn)單討論一下進(jìn)程和線程。
進(jìn)程(process)和線程(thread)
進(jìn)程就像是一個(gè)有邊界的生產(chǎn)廠間,而線程就像是廠間內(nèi)的一個(gè)個(gè)員工,可以自己做自己的事情,也可以相互配合做同一件事情。
當(dāng)我們啟動(dòng)一個(gè)應(yīng)用,計(jì)算機(jī)會(huì)創(chuàng)建一個(gè)進(jìn)程,操作系統(tǒng)會(huì)為進(jìn)程分配一部分內(nèi)存,應(yīng)用的所有狀態(tài)都會(huì)保存在這塊內(nèi)存中,應(yīng)用也許還會(huì)創(chuàng)建多個(gè)線程來輔助工作,這些線程可以共享這部分內(nèi)存中的數(shù)據(jù)。如果應(yīng)用關(guān)閉,進(jìn)程會(huì)被終結(jié),操作系統(tǒng)會(huì)釋放相關(guān)內(nèi)存。更生動(dòng)的示意圖如下:
一個(gè)進(jìn)程還可以要求操作系統(tǒng)生成另一個(gè)進(jìn)程來執(zhí)行不同的任務(wù),系統(tǒng)會(huì)為新的進(jìn)程分配獨(dú)立的內(nèi)存,兩個(gè)進(jìn)程之間可以使用 IPC (Inter Process Communication)進(jìn)行通信。很多應(yīng)用都會(huì)采用這樣的設(shè)計(jì),如果一個(gè)工作進(jìn)程反應(yīng)遲鈍,重啟這個(gè)進(jìn)程不會(huì)影響應(yīng)用其它進(jìn)程的工作。
如果對(duì)進(jìn)程及線程的理解還存在疑惑,可以參考下述文章:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
瀏覽器的架構(gòu)
有了上面的知識(shí)做鋪墊,我們可以更合理的討論瀏覽器的架構(gòu)了,其實(shí)如果要開發(fā)一個(gè)瀏覽器,它可以是單進(jìn)程多線程的應(yīng)用,也可以是使用 IPC 通信的多進(jìn)程應(yīng)用。
不同瀏覽器的架構(gòu)模型
不同瀏覽器采用了不同的架構(gòu)模式,這里并不存在標(biāo)準(zhǔn),本文以 Chrome 為例進(jìn)行說明 :
Chrome 采用多進(jìn)程架構(gòu),其頂層存在一個(gè) Browser process 用以協(xié)調(diào)瀏覽器的其它進(jìn)程。
Chrome 的不同進(jìn)程
具體說來,Chrome 的主要進(jìn)程及其職責(zé)如下:
Browser Process:
Renderer Process:
Plugin Process:
不同進(jìn)程負(fù)責(zé)的瀏覽器區(qū)域示意圖
Chrome 還為我們提供了「任務(wù)管理器」,供我們方便的查看當(dāng)前瀏覽器中運(yùn)行的所有進(jìn)程及每個(gè)進(jìn)程占用的系統(tǒng)資源,右鍵單擊還可以查看更多類別信息。
通過「頁面右上角的三個(gè)點(diǎn)點(diǎn)點(diǎn) — 更多工具 — 任務(wù)管理器」即可打開相關(guān)面板。
Chrome 多進(jìn)程架構(gòu)的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
某一渲染進(jìn)程出問題不會(huì)影響其他進(jìn)程
更為安全,在系統(tǒng)層面上限定了不同進(jìn)程的權(quán)限
缺點(diǎn)
由于不同進(jìn)程間的內(nèi)存不共享,不同進(jìn)程的內(nèi)存常常需要包含相同的內(nèi)容。
為了節(jié)省內(nèi)存,Chrome 限制了最多的進(jìn)程數(shù),最大進(jìn)程數(shù)量由設(shè)備的內(nèi)存和 CPU 能力決定,當(dāng)達(dá)到這一限制時(shí),新打開的 Tab 會(huì)共用之前同一個(gè)站點(diǎn)的渲染進(jìn)程。
測(cè)試了一下在 Chrome 中打開不斷打開知乎首頁,在 Mac i5 8g 上可以啟動(dòng)四十多個(gè)渲染進(jìn)程,之后新打開 tab 會(huì)合并到已有的渲染進(jìn)程中。
Chrome 把瀏覽器不同程序的功能看做服務(wù),這些服務(wù)可以方便的分割為不同的進(jìn)程或者合并為一個(gè)進(jìn)程。以 Broswer Process 為例,如果 Chrome 運(yùn)行在強(qiáng)大的硬件上,它會(huì)分割不同的服務(wù)到不同的進(jìn)程,這樣 Chrome 整體的運(yùn)行會(huì)更加穩(wěn)定,但是如果 Chrome 運(yùn)行在資源貧瘠的設(shè)備上,這些服務(wù)又會(huì)合并到同一個(gè)進(jìn)程中運(yùn)行,這樣可以節(jié)省內(nèi)存,示意圖如下。
iframe 的渲染 – Site Isolation
在上面的進(jìn)程圖中我們還可以看到一些進(jìn)程下還存在著 Subframe,這就是 Site Isolation 機(jī)制作用的結(jié)果。
Site Isolation 機(jī)制從 Chrome 67 開始默認(rèn)啟用。這種機(jī)制允許在同一個(gè) Tab 下的跨站 iframe 使用單獨(dú)的進(jìn)程來渲染,這樣會(huì)更為安全。
iframe 會(huì)采用不同的渲染進(jìn)程
Site Isolation 被大家看做里程碑式的功能, 其成功實(shí)現(xiàn)是多年工程努力的結(jié)果。Site Isolation 不是簡(jiǎn)單的疊加多個(gè)進(jìn)程。這種機(jī)制在底層改變了 iframe 之間通信的方法,Chrome 的其它功能都需要做對(duì)應(yīng)的調(diào)整,比如說 devtools 需要相應(yīng)的支持,甚至 Ctrl + F 也需要支持。關(guān)于 Site Isolation 的更多內(nèi)容可參考下述鏈接:
https://developers.google.com/web/updates/2018/07/site-isolation
介紹完了瀏覽器的基本架構(gòu)模式,接下來我們看看一個(gè)常見的導(dǎo)航過程對(duì)瀏覽器來說究竟發(fā)生了什么。
導(dǎo)航過程發(fā)生了什么
也許大多數(shù)人使用 Chrome 最多的場(chǎng)景就是在地址欄輸入關(guān)鍵字進(jìn)行搜索或者輸入地址導(dǎo)航到某個(gè)網(wǎng)站,我們來看看瀏覽器是怎么看待這個(gè)過程的。
我們知道瀏覽器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又對(duì)這些工作進(jìn)一步劃分,使用不同線程進(jìn)行處理:
瀏覽器主進(jìn)程中的不同線程
回到我們的問題,當(dāng)我們?cè)跒g覽器地址欄中輸入文字,并點(diǎn)擊回車獲得頁面內(nèi)容的過程在瀏覽器看來可以分為以下幾步:
1. 處理輸入
UI thread 需要判斷用戶輸入的是 URL 還是 query;
2. 開始導(dǎo)航
當(dāng)用戶點(diǎn)擊回車鍵,UI thread 通知 network thread 獲取網(wǎng)頁內(nèi)容,并控制 tab 上的 spinner 展現(xiàn),表示正在加載中。
network thread 會(huì)執(zhí)行 DNS 查詢,隨后為請(qǐng)求建立 TLS 連接。
UI thread 通知 Network thread 加載相關(guān)信息
如果 network thread 接收到了重定向請(qǐng)求頭如 301,network thread 會(huì)通知 UI thread 服務(wù)器要求重定向,之后,另外一個(gè) URL 請(qǐng)求會(huì)被觸發(fā)。
3. 讀取響應(yīng)
當(dāng)請(qǐng)求響應(yīng)返回的時(shí)候,network thread 會(huì)依據(jù) Content-Type 及 MIME Type sniffing 判斷響應(yīng)內(nèi)容的格式。
判斷響應(yīng)內(nèi)容的格式
如果響應(yīng)內(nèi)容的格式是 HTML ,下一步將會(huì)把這些數(shù)據(jù)傳遞給 renderer process,如果是 zip 文件或者其它文件,會(huì)把相關(guān)數(shù)據(jù)傳輸給下載管理器。
Safe Browsing 檢查也會(huì)在此時(shí)觸發(fā),如果域名或者請(qǐng)求內(nèi)容匹配到已知的惡意站點(diǎn),network thread 會(huì)展示一個(gè)警告頁。此外 CORB 檢測(cè)也會(huì)觸發(fā)確保敏感數(shù)據(jù)不會(huì)被傳遞給渲染進(jìn)程。
4. 查找渲染進(jìn)程
當(dāng)上述所有檢查完成,network thread 確信瀏覽器可以導(dǎo)航到請(qǐng)求網(wǎng)頁,network thread 會(huì)通知 UI thread 數(shù)據(jù)已經(jīng)準(zhǔn)備好,UI thread 會(huì)查找到一個(gè) renderer process 進(jìn)行網(wǎng)頁的渲染。
收到 Network thread 返回的數(shù)據(jù)后,UI thread 查找相關(guān)的渲染進(jìn)程
由于網(wǎng)絡(luò)請(qǐng)求獲取響應(yīng)需要時(shí)間,這里其實(shí)還存在著一個(gè)加速方案。當(dāng) UI thread 發(fā)送 URL 請(qǐng)求給 network thread 時(shí),瀏覽器其實(shí)已經(jīng)知道了將要導(dǎo)航到那個(gè)站點(diǎn)。UI thread 會(huì)并行的預(yù)先查找和啟動(dòng)一個(gè)渲染進(jìn)程,如果一切正常,當(dāng) network thread 接收到數(shù)據(jù)時(shí),渲染進(jìn)程已經(jīng)準(zhǔn)備就緒了,但是如果遇到重定向,準(zhǔn)備好的渲染進(jìn)程也許就不可用了,這時(shí)候就需要重啟一個(gè)新的渲染進(jìn)程。
5. 確認(rèn)導(dǎo)航
進(jìn)過了上述過程,數(shù)據(jù)以及渲染進(jìn)程都可用了, Browser Process 會(huì)給 renderer process 發(fā)送 IPC 消息來確認(rèn)導(dǎo)航,一旦 Browser Process 收到 renderer process 的渲染確認(rèn)消息,導(dǎo)航過程結(jié)束,頁面加載過程開始。
此時(shí),地址欄會(huì)更新,展示出新頁面的網(wǎng)頁信息。history tab 會(huì)更新,可通過返回鍵返回導(dǎo)航來的頁面,為了讓關(guān)閉 tab 或者窗口后便于恢復(fù),這些信息會(huì)存放在硬盤中。
6. 額外的步驟
一旦導(dǎo)航被確認(rèn),renderer process 會(huì)使用相關(guān)的資源渲染頁面,下文中我們將重點(diǎn)介紹渲染流程。當(dāng) renderer process 渲染結(jié)束(渲染結(jié)束意味著該頁面內(nèi)的所有的頁面,包括所有 iframe 都觸發(fā)了 onload 時(shí)),會(huì)發(fā)送 IPC 信號(hào)到 Browser process, UI thread 會(huì)停止展示 tab 中的 spinner。
Renderer Process 發(fā)送 IPC 消息通知 browser process 頁面已經(jīng)加載完成。
當(dāng)然上面的流程只是網(wǎng)頁首幀渲染完成,在此之后,客戶端依舊可下載額外的資源渲染出新的視圖。
在這里我們可以明確一點(diǎn),所有的 JS 代碼其實(shí)都由 renderer Process 控制的,所以在你瀏覽網(wǎng)頁內(nèi)容的過程大部分時(shí)候不會(huì)涉及到其它的進(jìn)程。不過也許你也曾經(jīng)監(jiān)聽過 beforeunload 事件,這個(gè)事件再次涉及到 Browser Process 和 renderer Process 的交互,當(dāng)當(dāng)前頁面關(guān)閉時(shí)(關(guān)閉 Tab ,刷新等等),Browser Process 需要通知 renderer Process 進(jìn)行相關(guān)的檢查,對(duì)相關(guān)事件進(jìn)行處理。
瀏覽器進(jìn)程發(fā)送 IPC 消息給渲染進(jìn)程,通知要離開當(dāng)前網(wǎng)站了
如果導(dǎo)航由 renderer process 觸發(fā)(比如在用戶點(diǎn)擊某鏈接,或者 JS 執(zhí)行 window.location="http://newsite.com" ) renderer process 會(huì)首先檢查是否有 beforeunload 事件處理器,導(dǎo)航請(qǐng)求由 renderer process 傳遞給 Browser process。
如果導(dǎo)航到新的網(wǎng)站,會(huì)啟用一個(gè)新的 render process 來處理新頁面的渲染,老的進(jìn)程會(huì)留下來處理類似 unload 等事件。
關(guān)于頁面的生命周期,更多內(nèi)容可參考 Page Lifecycle API 。
瀏覽器進(jìn)程發(fā)送 IPC 消息到新的渲染進(jìn)程通知渲染新的頁面,同時(shí)通知舊的渲染進(jìn)程卸載。
除了上述流程,有些頁面還擁有 Service Worker (服務(wù)工作線程),Service Worker 讓開發(fā)者對(duì)本地緩存及判斷何時(shí)從網(wǎng)絡(luò)上獲取信息有了更多的控制權(quán),如果 Service Worker 被設(shè)置為從本地 cache 中加載數(shù)據(jù),那么就沒有必要從網(wǎng)上獲取更多數(shù)據(jù)了。
值得注意的是 service worker 也是運(yùn)行在渲染進(jìn)程中的 JS 代碼,因此對(duì)于擁有 Service Worker 的頁面,上述流程有些許的不同。
當(dāng)有 Service Worker 被注冊(cè)時(shí),其作用域會(huì)被保存,當(dāng)有導(dǎo)航時(shí),network thread 會(huì)在注冊(cè)過的 Service Worker 的作用域中檢查相關(guān)域名,如果存在對(duì)應(yīng)的 Service worker,UI thread 會(huì)找到一個(gè) renderer process 來處理相關(guān)代碼,Service Worker 可能會(huì)從 cache 中加載數(shù)據(jù),從而終止對(duì)網(wǎng)絡(luò)的請(qǐng)求,也可能從網(wǎng)上請(qǐng)求新的數(shù)據(jù)。
Service Worker 依據(jù)具體情形做處理。
關(guān)于 Service Worker 的更多內(nèi)容可參考:
https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
如果 Service Worker 最終決定通過網(wǎng)上獲取數(shù)據(jù),Browser 進(jìn)程 和 renderer 進(jìn)程的交互其實(shí)會(huì)延后數(shù)據(jù)的請(qǐng)求時(shí)間 。Navigation Preload 是一種與 Service Worker 并行的加速加載資源的機(jī)制,服務(wù)端通過請(qǐng)求頭可以識(shí)別這類請(qǐng)求,而做出相應(yīng)的處理。
更多內(nèi)容可參考:
https://developers.google.com/web/updates/2017/02/navigation-preload
渲染進(jìn)程是如何工作的?
渲染進(jìn)程幾乎負(fù)責(zé) Tab 內(nèi)的所有事情,渲染進(jìn)程的核心目的在于轉(zhuǎn)換 HTML CSS JS 為用戶可交互的 web 頁面。渲染進(jìn)程中主要包含以下線程:
渲染進(jìn)程包含的線程
1. 主線程 Main thread
2. 工作線程 Worker thread
3. 排版線程 Compositor thread
4. 光柵線程 Raster thread
后文我們將逐步介紹不同線程的職責(zé),在此之前我們先看看渲染的流程。
1. 構(gòu)建 DOM
當(dāng)渲染進(jìn)程接收到導(dǎo)航的確認(rèn)信息,開始接受 HTML 數(shù)據(jù)時(shí),主線程會(huì)解析文本字符串為 DOM。
渲染 html 為 DOM 的方法由 HTML Standard 定義。
2. 加載次級(jí)的資源
網(wǎng)頁中常常包含諸如圖片,CSS,JS 等額外的資源,這些資源需要從網(wǎng)絡(luò)上或者 cache 中獲取。主進(jìn)程可以在構(gòu)建 DOM 的過程中會(huì)逐一請(qǐng)求它們,為了加速 preload scanner 會(huì)同時(shí)運(yùn)行,如果在 html 中存在 <img><link> 等標(biāo)簽,preload scanner 會(huì)把這些請(qǐng)求傳遞給 Browser process 中的 network thread 進(jìn)行相關(guān)資源的下載。
3.JS 的下載與執(zhí)行
當(dāng)遇到 <script> 標(biāo)簽時(shí),渲染進(jìn)程會(huì)停止解析 HTML,而去加載,解析和執(zhí)行 JS 代碼,停止解析 html 的原因在于 JS 可能會(huì)改變 DOM 的結(jié)構(gòu)(使用諸如 documwnt.write()等 API)。
不過開發(fā)者其實(shí)也有多種方式來告知瀏覽器應(yīng)對(duì)如何應(yīng)對(duì)某個(gè)資源,比如說如果在<script> 標(biāo)簽上添加了 async 或 defer 等屬性,瀏覽器會(huì)異步的加載和執(zhí)行 JS 代碼,而不會(huì)阻塞渲染。更多的方法可參考 Resource Prioritization – Getting the Browser to Help You。
4. 樣式計(jì)算
僅僅渲染 DOM 還不足以獲知頁面的具體樣式,主進(jìn)程還會(huì)基于 CSS 選擇器解析 CSS 獲取每一個(gè)節(jié)點(diǎn)的最終的計(jì)算樣式值。即使不提供任何 CSS,瀏覽器對(duì)每個(gè)元素也會(huì)有一個(gè)默認(rèn)的樣式。
渲染進(jìn)程主線程計(jì)算每一個(gè)元素節(jié)點(diǎn)的最終樣式值
5. 獲取布局
想要渲染一個(gè)完整的頁面,除了獲知每個(gè)節(jié)點(diǎn)的具體樣式,還需要獲知每一個(gè)節(jié)點(diǎn)在頁面上的位置,布局其實(shí)是找到所有元素的幾何關(guān)系的過程。其具體過程如下:
通過遍歷 DOM 及相關(guān)元素的計(jì)算樣式,主線程會(huì)構(gòu)建出包含每個(gè)元素的坐標(biāo)信息及盒子大小的布局樹。布局樹和 DOM 樹類似,但是其中只包含頁面可見的元素,如果一個(gè)元素設(shè)置了 display:none ,這個(gè)元素不會(huì)出現(xiàn)在布局樹上,偽元素雖然在 DOM 樹上不可見,但是在布局樹上是可見的。
6. 繪制各元素
即使知道了不同元素的位置及樣式信息,我們還需要知道不同元素的繪制先后順序才能正確繪制出整個(gè)頁面。在繪制階段,主線程會(huì)遍歷布局樹以創(chuàng)建繪制記錄。繪制記錄可以看做是記錄各元素繪制先后順序的筆記。
主線程依據(jù)布局樹構(gòu)建繪制記錄
7. 合成幀
熟悉 PS 等繪圖軟件的童鞋肯定對(duì)圖層這一概念不陌生,現(xiàn)代 Chrome 其實(shí)利用了這一概念來組合不同的層。
復(fù)合是一種分割頁面為不同的層,并單獨(dú)柵格化,隨后組合為幀的技術(shù)。不同層的組合由 compositor 線程(合成器線程)完成。
主線程會(huì)遍歷布局樹來創(chuàng)建層樹(layer tree),添加了 will-change CSS 屬性的元素,會(huì)被看做單獨(dú)的一層。
主線程遍歷布局樹生成層樹
你可能會(huì)想給每一個(gè)元素都添加上 will-change,不過組合過多的層也許會(huì)比在每一幀都柵格化頁面中的某些小部分更慢。為了更合理的使用層,可參考 堅(jiān)持僅合成器的屬性和管理層計(jì)數(shù) 。
一旦層樹被創(chuàng)建,渲染順序被確定,主線程會(huì)把這些信息通知給合成器線程,合成器線程會(huì)柵格化每一層。有的層的可以達(dá)到整個(gè)頁面的大小,因此,合成器線程將它們分成多個(gè)磁貼,并將每個(gè)磁貼發(fā)送到柵格線程,柵格線程會(huì)柵格化每一個(gè)磁貼并存儲(chǔ)在 GPU 顯存中。
柵格線程會(huì)柵格化每一個(gè)磁貼并存儲(chǔ)在 GPU 顯存中
一旦磁貼被光柵化,合成器線程會(huì)收集稱為繪制四邊形的磁貼信息以創(chuàng)建合成幀。
合成幀隨后會(huì)通過 IPC 消息傳遞給瀏覽器進(jìn)程,由于瀏覽器的 UI 改變或者其它拓展的渲染進(jìn)程也可以添加合成幀,這些合成幀會(huì)被傳遞給 GPU 用以展示在屏幕上,如果滾動(dòng)發(fā)生,合成器線程會(huì)創(chuàng)建另一個(gè)合成幀發(fā)送給 GPU。
合成器線程會(huì)發(fā)送合成幀給 GPU 渲染
合成器的優(yōu)點(diǎn)在于,其工作無關(guān)主線程,合成器線程不需要等待樣式計(jì)算或者 JS 執(zhí)行,這就是為什么合成器相關(guān)的動(dòng)畫 最流暢,如果某個(gè)動(dòng)畫涉及到布局或者繪制的調(diào)整,就會(huì)涉及到主線程的重新計(jì)算,自然會(huì)慢很多。
瀏覽器對(duì)事件的處理
瀏覽器通過對(duì)不同事件的處理來滿足各種交互需求,這一部分我們一起看看從瀏覽器的視角,事件是什么,在此我們先主要考慮鼠標(biāo)事件。
在瀏覽器的看來,用戶的所有手勢(shì)都是輸入,鼠標(biāo)滾動(dòng),懸置,點(diǎn)擊等等都是。
當(dāng)用戶在屏幕上觸發(fā)諸如 touch 等手勢(shì)時(shí),首先收到手勢(shì)信息的是 Browser process, 不過 Browser process 只會(huì)感知到在哪里發(fā)生了手勢(shì),對(duì) tab 內(nèi)內(nèi)容的處理是還是由渲染進(jìn)程控制的。
事件發(fā)生時(shí),瀏覽器進(jìn)程會(huì)發(fā)送事件類型及相應(yīng)的坐標(biāo)給渲染進(jìn)程,渲染進(jìn)程隨后找到事件對(duì)象并執(zhí)行所有綁定在其上的相關(guān)事件處理函數(shù)。
事件從瀏覽器進(jìn)程傳送給渲染進(jìn)程
前文中,我們提到過合成器可以獨(dú)立于主線程之外通過合成柵格化層平滑的處理滾動(dòng)。如果頁面中沒有綁定相關(guān)事件,組合器線程可以獨(dú)立于主線程創(chuàng)建組合幀。如果頁面綁定了相關(guān)事件處理器,主線程就不得不出來工作了。這時(shí)候合成器線程會(huì)怎么處理呢?
這里涉及到一個(gè)專業(yè)名詞「理解非快速滾動(dòng)區(qū)域(non-fast scrollable region)」由于執(zhí)行 JS 是主線程的工作,當(dāng)頁面合成時(shí),合成器線程會(huì)標(biāo)記頁面中綁定有事件處理器的區(qū)域?yàn)?non-fast scrollable region ,如果存在這個(gè)標(biāo)注,合成器線程會(huì)把發(fā)生在此處的事件發(fā)送給主線程,如果事件不是發(fā)生在這些區(qū)域,合成器線程則會(huì)直接合成新的幀而不用等到主線程的響應(yīng)。
涉及 non-fast scrollable region 的事件,合成器線程會(huì)通知主線程進(jìn)行相關(guān)處理。
web 開發(fā)中常用的事件處理模式是事件委托,基于事件冒泡,我們常常在最頂層綁定事件:
復(fù)制代碼
document.body.addEventListener('touchstart', event=> { if (event.target===area) { event.preventDefault(); } } );
上述做法很常見,但是如果從瀏覽器的角度看,整個(gè)頁面都成了 non-fast scrollable region 了。
這意味著即使操作的是頁面無綁定事件處理器的區(qū)域,每次輸入時(shí),合成器線程也需要和主線程通信并等待反饋,流暢的合成器獨(dú)立處理合成幀的模式就失效了。
由于事件綁定在最頂部,整個(gè)頁面都成為了 non-fast scrollable region。
為了防止這種情況,我們可以為事件處理器傳遞 passive: true 做為參數(shù),這樣寫就能讓瀏覽器即監(jiān)聽相關(guān)事件,又讓組合器線程在等等主線程響應(yīng)前構(gòu)建新的組合幀。
復(fù)制代碼
document.body.addEventListener('touchstart', event=> { if (event.target===area) { event.preventDefault() } }, {passive: true} );
不過上述寫法可能又會(huì)帶來另外一個(gè)問題,假設(shè)某個(gè)區(qū)域你只想要水平滾動(dòng),使用 passive: true 可以實(shí)現(xiàn)平滑滾動(dòng),但是垂直方向的滾動(dòng)可能會(huì)先于event.preventDefault()發(fā)生,此時(shí)可以通過 event.cancelable 來防止這種情況。
復(fù)制代碼
document.body.addEventListener('pointermove', event=> { if (event.cancelable) { event.preventDefault(); // block the native scroll /* * do what you want the application to do here */ } }, {passive: true});
也可以使用 css 屬性 touch-action 來完全消除事件處理器的影響,如:
復(fù)制代碼
#area { touch-action: pan-x; }
查找到事件對(duì)象
當(dāng)組合器線程發(fā)送輸入事件給主線程時(shí),主線程首先會(huì)進(jìn)行命中測(cè)試(hit test)來查找對(duì)應(yīng)的事件目標(biāo),命中測(cè)試會(huì)基于渲染過程中生成的繪制記錄( paint records )查找事件發(fā)生坐標(biāo)下存在的元素。
主線程依據(jù)繪制記錄查找事件相關(guān)元素。
事件的優(yōu)化
一般我們屏幕的刷新速率為 60fps,但是某些事件的觸發(fā)量會(huì)不止這個(gè)值,出于優(yōu)化的目的,Chrome 會(huì)合并連續(xù)的事件 (如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延遲到下一幀渲染時(shí)候執(zhí)行 。
而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非連續(xù)性事件則會(huì)立即被觸發(fā)。
Chrome 會(huì)合并連續(xù)事件到下一幀觸發(fā)。
合并事件雖然能提示性能,但是如果你的應(yīng)用是繪畫等,則很難繪制一條平滑的曲線了,此時(shí)可以使用 getCoalescedEvents API 來獲取組合的事件。示例代碼如下:
復(fù)制代碼
window.addEventListener('pointermove', event=> { const events=event.getCoalescedEvents(); for (let event of events) { const x=event.pageX; const y=event.pageY; // draw a line using x and y coordinates. } });
花了好久來整理上面的內(nèi)容,整理的過程收獲還挺大的,也希望這篇筆記能對(duì)你有所啟發(fā),如果有任何疑問,歡迎一起來討論。
本文經(jīng)作者授權(quán)轉(zhuǎn)載,原文鏈接為:
https://zhuanlan.zhihu.com/p/47407398
參考鏈接
avaScript
什么是
前端三大語言
HTML
專門編寫網(wǎng)頁內(nèi)容的語言
CSS
專門編寫網(wǎng)頁樣式的語言
JS
為什么
僅用HTML和CSS編寫的網(wǎng)頁,只能看不能用!
有些不需要服務(wù)器端數(shù)據(jù),就可執(zhí)行的功能,就不應(yīng)該反復(fù)與服務(wù)器交互,頻繁讓用戶等待。
發(fā)展史
專門編寫客戶端交互行為的語言
交互(IPO)
1. 用戶輸入
2. 程序接受輸入,處理數(shù)據(jù)
3. 輸出處理結(jié)果
何時(shí)
今后,凡是HTML和CSS編寫的網(wǎng)頁,必須用js添加交互行為后,才能讓用戶使用
今后,不需要服務(wù)器數(shù)據(jù),就可執(zhí)行的操作,只要在客戶端用JavaScript實(shí)現(xiàn)即可,不用頻繁與服務(wù)器交互
如何使用JavaScript
在何處編寫JavaScript
2處
1. HTML網(wǎng)頁中的script標(biāo)簽內(nèi)
問題:
無法重用
程序的準(zhǔn)則:
DRY
盡量:
一次定義,處處使用,一次修改,處處生效
解決
另見:
2. 在獨(dú)立的js文件中編寫JavaScript
再用script標(biāo)簽引入網(wǎng)頁中
<script src="url"></script>
如何運(yùn)行
腳本解釋引擎
解析并執(zhí)行js程序的小軟件
如何
1. 瀏覽器已經(jīng)自帶腳本解釋引擎
瀏覽器包含兩個(gè)小軟件
內(nèi)容排版引擎
解析HTML和CSS的程序
效率高
腳本解釋引擎
解析js并運(yùn)行js程序的小程序
效率比排版引擎略低
2. 獨(dú)立安裝
Node.js
JavaScript語言基本語法
1. 區(qū)分大小寫
2. 字符串必須用引號(hào)包裹, 但單雙引號(hào)都行
特殊
如果字符串內(nèi)又包含引號(hào)沖突, 只要內(nèi)外使用不同的引號(hào)區(qū)分即可
3. 每句話結(jié)尾必須用分號(hào)結(jié)束
4. 注釋
//單行注釋
/* 多行注釋 */
調(diào)試JavaScript
只要想要的效果沒出來,就是出錯(cuò)了
只要出錯(cuò),先開F12控制臺(tái)(Console)
Console中顯示錯(cuò)誤的原因,和出錯(cuò)位置
JavaScript能做什么
客戶端表單驗(yàn)證
客戶端數(shù)據(jù)計(jì)算
客戶端動(dòng)畫效果和網(wǎng)頁游戲
總之: js可對(duì)網(wǎng)頁中的一切內(nèi)容執(zhí)行一切想要的操作——只有想不到,沒有做不到
常用輸出
向網(wǎng)頁中輸出內(nèi)容
document.write("HTML片段或文字")
問題: 影響網(wǎng)頁中現(xiàn)有結(jié)構(gòu)和內(nèi)容
彈出警告框
alert("提示內(nèi)容...")
問題:
1. 樣式寫死,不可定制修改
2. 模態(tài),阻礙用戶操作
向控制臺(tái)輸出:
console.log("輸出內(nèi)容...")
今后,凡是調(diào)試程序,查看輸出結(jié)果,都要在控制臺(tái)輸出
控制臺(tái)的使用
1. 單行代碼按回車直接執(zhí)行
2. 切換出之前執(zhí)行過的代碼記錄,微調(diào)
按鍵盤上下按鍵切換
3. 清屏
點(diǎn)控制臺(tái)左上角圓形叉號(hào)
4. 僅換行輸入,不執(zhí)行
shift+enter
原生js
不依賴于第三方文件,僅依靠瀏覽器就可直接執(zhí)行的代碼
包含3大部分
ECMAScript
規(guī)定了js語言核心語法的標(biāo)準(zhǔn)
3 5 6(2015)
JavaScript
網(wǎng)景(Mozilla火狐)參照ES標(biāo)準(zhǔn)實(shí)現(xiàn)的JavaScript語言版本
JScript
微軟參照ES標(biāo)準(zhǔn)實(shí)現(xiàn)的JavaScript語言版本
DOM
專門操作網(wǎng)頁內(nèi)容的API
比如:
document.write(...)
向網(wǎng)頁內(nèi)容中輸出一行話
BOM
專門操作瀏覽器窗口的API
比如:
alert,prompt
彈出瀏覽器的提示框
變量
什么是
內(nèi)存中存儲(chǔ)*一個(gè)*數(shù)據(jù)的存儲(chǔ)空間,再起一個(gè)名字
為什么
重用一個(gè)數(shù)據(jù)
何時(shí)
只要一個(gè)數(shù)據(jù)可能被反復(fù)使用時(shí),都要保存在變量中
如何
聲明
什么是
在內(nèi)存中創(chuàng)建一塊存儲(chǔ)空間,再起一個(gè)名字
何時(shí)
任何變量在使用前,必須先聲明
如何
var 變量名;
變量名
1. 字母,數(shù)字,下劃線和$組成,但不能以數(shù)字開頭
2. 不能使用保留字
3. 見名知義
4. 駝峰命名
默認(rèn)值
僅聲明,未賦值的變量,默認(rèn)值都是undefined
簡(jiǎn)寫
var 變量1, 變量2, ...;
賦值
變量名=值
將等號(hào)右邊的值保存到等號(hào)左邊的變量中
只有=才能賦值
=不是追加,而是完全替換!
覆蓋原值
特殊
如果給未聲明的變量強(qiáng)行賦值
普通模式
不會(huì)報(bào)錯(cuò)!
會(huì)自動(dòng)在全局創(chuàng)建該變量
全局污染
ES5
嚴(yán)格模式
什么是
比普通js運(yùn)行模式要求更嚴(yán)格的運(yùn)行機(jī)制
要求1:
禁止給未聲明的變量賦值
報(bào)錯(cuò)!
變量未定義
如何啟用:
在當(dāng)前代碼段的頂部先插入"use strict";
總結(jié)
禁止使用!
簡(jiǎn)寫
問題:
變量的默認(rèn)值undefined是造成最多錯(cuò)誤的根源
強(qiáng)烈建議
聲明變量同時(shí)初始化變量的值
var 變量名=值
特殊
同時(shí)聲明并初始化多個(gè)變量
var 變量1=值1, 變量2=值2, ...;
鄙視
var a , b=2;
a: undefined
b: 2
取值
任何情況下使用變量名等效于復(fù)制出變量中的值使用
嘗試從未聲明的變量中取值都會(huì)報(bào)錯(cuò)!
ReferenceError
常量
什么是
一旦創(chuàng)建,值不能改變的量
何時(shí)
只要程序中的一個(gè)值,任何情況下都不能改變
如何
const 常量名=值;
強(qiáng)調(diào):
常量名必須全大寫
創(chuàng)建常量時(shí)必須立刻賦值
強(qiáng)行修改常量的值:
在最新的Chrome瀏覽器中,已經(jīng)內(nèi)置了對(duì)常量賦值的檢查
在ES3標(biāo)準(zhǔn)中,普通運(yùn)行模式下,強(qiáng)行給常量賦值
不報(bào)錯(cuò)!
也不讓修改
靜默失敗
解決
啟用嚴(yán)格模式
要求:
將所有靜默失敗升級(jí)為錯(cuò)誤!
數(shù)據(jù)類型
什么是
數(shù)據(jù)在內(nèi)存中的存儲(chǔ)格式
為什么
不同類型的數(shù)據(jù),可執(zhí)行的操作不一樣
不同的操作,要求專門的存儲(chǔ)結(jié)構(gòu)支持
包括
原始類型
值直接保存在變量本地的數(shù)據(jù)類型
number string boolean null undefined
引用類型
值無法保存在變量本地的復(fù)雜數(shù)據(jù)類型
如果一個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu),同時(shí)保存多個(gè)數(shù)據(jù),就不能直接保存在變量中
存儲(chǔ)在window之外的一塊獨(dú)立存儲(chǔ)空間
獨(dú)立存儲(chǔ)空間中可同時(shí)存儲(chǔ)多個(gè)數(shù)據(jù),擁有唯一的地址
變量名僅保存存儲(chǔ)空間的地址
number
什么是
專門存儲(chǔ)數(shù)字(即包含整數(shù),也包含小數(shù))的類型
何時(shí)
如果一個(gè)數(shù)值可能需要比較大小和進(jìn)行數(shù)學(xué)計(jì)算時(shí)
如何
程序中只要不加引號(hào)的數(shù)字,自動(dòng)就是number類型
底層都是二進(jìn)制存儲(chǔ)
n.toString(2)
存儲(chǔ)
整數(shù)占4字節(jié)
浮點(diǎn)數(shù)占8字節(jié)
存儲(chǔ)空間和數(shù)值大小無關(guān)
string
什么是
專門存儲(chǔ)一串用于顯示的文字
何時(shí)
記錄一串文字
不用做比較和計(jì)算時(shí)
如何
凡是加引號(hào)的一串字符直接量稱為字符串類型
存儲(chǔ)
unicode
對(duì)全球主要語言的每個(gè)字編一個(gè)號(hào)
為什么
計(jì)算機(jī)不認(rèn)識(shí)字符,只認(rèn)識(shí)數(shù)字
范圍:
"0"~"9"
48~57
"A"~"Z"
65~90
"a"~"z"
97~122
漢字
"\u4e0"~"\u9fa5"
每個(gè)字母/數(shù)字字符,占1字節(jié)
每個(gè)漢字,占2字節(jié)
其他
utf-8
字母數(shù)字1字節(jié)
漢字3字節(jié)
GBK
字母數(shù)字1字節(jié)
漢字2字節(jié)
boolean
什么是
專門表示判斷結(jié)果的
何時(shí)
只要作為判斷條件的結(jié)論時(shí)
如何
只有兩個(gè)值
true/false
不加引號(hào)
undefined
空
專門由程序自動(dòng)為一個(gè)變量賦初值
程序員很少主動(dòng)使用
null
空
專門給程序員用于手動(dòng)清空一個(gè)變量之用
數(shù)據(jù)類型轉(zhuǎn)換
弱類型語言
1. 聲明變量時(shí)無需提前規(guī)定變量中存儲(chǔ)數(shù)據(jù)的類型
2. 一個(gè)變量先后可保存不同類型的數(shù)據(jù)
3. js會(huì)根據(jù)自身的需要,隱式轉(zhuǎn)化數(shù)據(jù)的類型
什么是
將數(shù)據(jù)從一種類型轉(zhuǎn)化為另一種類型
何時(shí)
只要數(shù)據(jù)類型不是想要的
2大類
隱式轉(zhuǎn)換
無需程序員干預(yù),程序自動(dòng)完成的類型轉(zhuǎn)換
何時(shí)
如果給定的數(shù)據(jù)類型和js運(yùn)算要求的數(shù)據(jù)類型不相符,都會(huì)首先自動(dòng)類型轉(zhuǎn)換為規(guī)定的類型,再運(yùn)行
強(qiáng)制轉(zhuǎn)換
什么是
程序員主動(dòng)調(diào)用轉(zhuǎn)化函數(shù)實(shí)現(xiàn)的類型轉(zhuǎn)換
何時(shí)
只要隱式轉(zhuǎn)換的結(jié)果不是想要的
3種情況
轉(zhuǎn)為number
2種
1. 將純數(shù)字組成的字符串和bool類型轉(zhuǎn)為number
Number(x)
Number(true)->1 Number(false)->0
Number(null)->0 , Number("")->0
Number(undefined)->NaN
其實(shí)也可以轉(zhuǎn)字符串
只能轉(zhuǎn)純數(shù)字組成的字符串
其實(shí),所有隱式轉(zhuǎn)化都是自動(dòng)調(diào)用Number()
2. 將字符串轉(zhuǎn)為number
parseFloat(str)
將str轉(zhuǎn)為number類型的浮點(diǎn)數(shù)(小數(shù))
原理
從開頭開始,依次讀取每個(gè)字符
只要碰到數(shù)字和小數(shù)點(diǎn)就保留
直到碰到第一個(gè)不是數(shù)字和小數(shù)點(diǎn)的字符就不再繼續(xù)讀取
總結(jié):
可保留小數(shù)
僅認(rèn)識(shí)第一個(gè)小數(shù)點(diǎn)
去掉結(jié)尾的非數(shù)字字符
無法去掉開頭的非數(shù)字字符
何時(shí)
只要希望去掉字符串結(jié)尾的單位時(shí)
parseInt(str)
將str轉(zhuǎn)為number類型的整數(shù)
原理
和parseFloat完全一樣
只是不認(rèn)識(shí)小數(shù)點(diǎn)
總結(jié)
去掉小數(shù)
去掉結(jié)尾的非數(shù)字字符
何時(shí)
只要希望去掉結(jié)尾的單位,且同時(shí)舍棄小數(shù)部分時(shí)
特殊:
parseFloat(null/true/false)
NaN
先將null/true/false隱式轉(zhuǎn)化為字符串,再按照原理轉(zhuǎn)為數(shù)字
總結(jié):
如果將字符串轉(zhuǎn)數(shù)字
首選parseFloat
除非確定要舍棄小數(shù)部分采用parseInt
如果將非字符串轉(zhuǎn)數(shù)字
首選Number
很少主動(dòng)使用
NaN
Not a Number
js中number類型的一個(gè)特殊值
代表一切不是數(shù)字的值
只要將其它類型轉(zhuǎn)為數(shù)字時(shí),無法正常轉(zhuǎn)換,都轉(zhuǎn)為NaN
特點(diǎn):
1. 參與任何計(jì)算,結(jié)果依然是NaN
2. 不大于,不小于,不等于任何值
轉(zhuǎn)為string
x.toString()
問題:
x不能是null和undefined
解決
另見:
String(x)
萬能
其實(shí)隱式轉(zhuǎn)換都是自動(dòng)調(diào)用String()
轉(zhuǎn)為boolean
Boolean(x)
規(guī)則
只有五個(gè)值被轉(zhuǎn)為false
0, null, NaN, undefined, ""
其余任何值都轉(zhuǎn)為true
運(yùn)算符和表達(dá)式
概念
程序
人的想法在計(jì)算機(jī)中的執(zhí)行
運(yùn)算符
程序中模擬人的想法的特殊符號(hào)
表達(dá)式
由數(shù)據(jù),變量和運(yùn)算符組成的一條程序的語句
執(zhí)行過程
從左向右,依次執(zhí)行
每讀到一個(gè)變量就將變量換成變量中的值
包括
算數(shù)計(jì)算
+ - * / %
%
被除數(shù)/除數(shù),不取商,取除不盡的余數(shù)部分
何時(shí):
1. 取余數(shù)
2. 判斷能否整除
比如
判斷奇偶數(shù)
判斷閏年
隱式轉(zhuǎn)換
默認(rèn)
一切轉(zhuǎn)為number,再算數(shù)計(jì)算
特殊
+運(yùn)算中只要有一個(gè)字符串
則全部轉(zhuǎn)為字符串
+計(jì)算改為字符串拼接
問題:
凡是從頁面上獲得的數(shù)據(jù)都是字符串
解決:
做算數(shù)計(jì)算前,必須用parseFloat()強(qiáng)轉(zhuǎn)為數(shù)字類型
關(guān)系運(yùn)算
做比較,做判斷
> < >=<=!===
返回值
只能是true/false
隱式轉(zhuǎn)換
默認(rèn)
一切都轉(zhuǎn)為number,再做比較
特殊
兩個(gè)string類型做比較
不再轉(zhuǎn)為number
而是按位PK每個(gè)字符的unicode號(hào)
只要一位字符能比較出結(jié)果,就不再繼續(xù)
null和undefined
問題
用==無法區(qū)分null和undefined
普通關(guān)系運(yùn)算會(huì)將undefined先隱式轉(zhuǎn)為null再做比較
解決
===
先要類型相同,然后再值相等
不帶隱式轉(zhuǎn)換的==比較
強(qiáng)烈建議使用===代替==
!==
不帶隱式轉(zhuǎn)換的!=比較
強(qiáng)烈建議使用!==代替!=
NaN
問題
無法判斷是不是NaN
NaN不等于,不大于,不小于一切
因?yàn)镹aN表示所有不是數(shù)字的內(nèi)容
是一個(gè)范圍,不表示一個(gè)具體值
做比較無任何意義
解決
isNaN(num)
專門判斷num是不是NaN
經(jīng)常反用
只要不是NaN,一定是數(shù)字
!isNaN(num)
專門用來判斷num是不是有效的數(shù)字
兩個(gè)對(duì)象做==比較
不再做任何轉(zhuǎn)換,而是比較兩個(gè)對(duì)象的*地址*是否相同
判斷兩個(gè)變量是否引用同一個(gè)對(duì)象
比如
[]==[]
false
邏輯運(yùn)算
將多個(gè)關(guān)系運(yùn)算,組合起來,綜合得出最終的結(jié)論
返回true/false
3種
1. 邏輯與&&
而且
條件1&&條件2
必須同時(shí)滿足條件1和條件2才返回true
只要一個(gè)條件不滿足,都返回false
2. 邏輯或
或/要么
條件1||條件2
只要滿足條件1或條件2中任意一個(gè)條件就返回true
除非所有條件都為false時(shí),才為false
3. 邏輯非
不/沒有
!條件
顛倒條件的判斷結(jié)果
隱式轉(zhuǎn)換
默認(rèn)
每個(gè)條件都轉(zhuǎn)為bool類型,再聯(lián)合判斷
短路邏輯
在邏輯運(yùn)算中,如果前一個(gè)條件已經(jīng)可以得出最終的結(jié)論,則后續(xù)條件不再執(zhí)行
&&
如果前一個(gè)條件為false,則后續(xù)條件不再執(zhí)行
如果前一個(gè)條件為true,則后續(xù)條件才可能執(zhí)行
||
如果前一個(gè)條件為true,則后續(xù)條件不再執(zhí)行
如果前一個(gè)條線為false, 則后續(xù)條件才有必要執(zhí)行
利用短路
利用&&的短路
1. 實(shí)現(xiàn)簡(jiǎn)單分支
一個(gè)條件一件事,滿足條件才執(zhí)行,不滿足就不執(zhí)行
條件&&(操作)
利用||的短路
2. 定義默認(rèn)值
值1||默認(rèn)值
優(yōu)先使用值1
如果值1轉(zhuǎn)為bool后為false,才使用默認(rèn)值作為備用
位運(yùn)算
左移和右移
左移
m<<n
將m的二進(jìn)制數(shù)左移n位
相當(dāng)于m* 2的n次方
右移
m>>n
將m的二進(jìn)制數(shù)右移n位
相當(dāng)于m/ 2的n次方
下取整
m>>>0
m|0
m^0
不聲明第三個(gè)變量,交換兩變量的值
方法一:
a^=b; b^=a; a^=b;
方法二:
a+=b; b=a-b; a-=b;
問題: 只能交換數(shù)字類型的數(shù)據(jù)
方法三:
a=[b,b=a][0]
賦值運(yùn)算
賦值運(yùn)算也有返回值
返回保存到等號(hào)左邊的變量中的新值
擴(kuò)展賦值運(yùn)算
一種簡(jiǎn)寫
包括
a+=b
累加
a=a+b
如果只是+1
更簡(jiǎn)化
遞增
a++
a-=b
a=a-b
如果只是-1
更簡(jiǎn)化
遞減
a--
a*=b
a/=b
a%=b
何時(shí)
只要先取出變量值,計(jì)算后,想再保存回原變量時(shí)
遞增/遞減1
++a vs a++
單獨(dú)使用
沒有差別
如果嵌入其它表達(dá)式中
相同
變量a中的值,一定都會(huì)被+1
不同
返回值
++a,返回+1后的新值
a++,返回+1前的舊值
運(yùn)算符優(yōu)先級(jí)
20 圓括號(hào) ( x+y )*3
19 成員訪問 arr.length 需計(jì)算的成員訪問 arr[ i+1 ] new (帶參數(shù)列表) new Array( 1,2,3 ) 函數(shù)調(diào)用 fun ( 1,2,3 )
18 new (無參數(shù)列表) new Array
17 后置遞增(運(yùn)算符在后) n++ 后置遞減(運(yùn)算符在后) n --
16 邏輯非 !bool 按位非 ~ … 正號(hào) +1 負(fù)號(hào) -1 前置遞增 ++n 前置遞減 --n typeof typeof n void void … delete delete lilei.sname await await new Promise( ... )
15 冪 2**3
14 乘法 2*3 除法 6 / 2 取模 6 % 2
13 加法 a+b 減法 a-b
12 按位左移 2<<3 按位右移 8 >>3 無符號(hào)右移 8>>> 3
11 小于 a< b 小于等于 a<=b 大于 a>b 大于等于 a>=b in math in ym instanceof arr instanceof Array
10 等號(hào) a==b 非等號(hào) a !=b 全等號(hào) a===b 非全等號(hào) a !==b
9 按位與 … & …
8 按位異或 … ^ …
7 按位或 … | …
6 邏輯與 xxx && xxx
5 邏輯或 xxx || xxx
4 條件運(yùn)算符 sex==1 ? "男" : "女"
3 賦值 var a=5 a+=1 a-=1 a*=3 a/=3 a%=2 … <<=… … >>=… … >>>=… … &=… … ^=… … |=…
2 yield
1 展開運(yùn)算符 ...arr
0 逗號(hào) a+=b, b=a-b, a-=b
函數(shù)
什么是
封裝一項(xiàng)任務(wù)步驟清單的代碼段,再起一個(gè)名字
函數(shù)也是引用類型的對(duì)象
函數(shù)名其實(shí)只是保存函數(shù)對(duì)象地址的變量
為什么
代碼重用
何時(shí)
只要一項(xiàng)任務(wù)可能被反復(fù)使用,都要先定義在函數(shù)中,再反復(fù)調(diào)用函數(shù)
如何
聲明函數(shù)
function 函數(shù)名(參數(shù)列表){ 函數(shù)體; return 返回值; }
參數(shù)
函數(shù)執(zhí)行過程中必須的數(shù)據(jù)
為什么
有些功能必須某些外來數(shù)據(jù)才能正常執(zhí)行
何時(shí)
只要函數(shù)本身需要某些數(shù)據(jù)才能正常執(zhí)行時(shí)
如何
聲明
函數(shù)名后的圓括號(hào)中, 用逗號(hào)分隔每個(gè)參數(shù)名
不用加var
訪問
在函數(shù)內(nèi),參數(shù)的用法和普通變量完全一樣
返回值
函數(shù)執(zhí)行的結(jié)果
何時(shí)
只要調(diào)用者需要獲得函數(shù)的執(zhí)行結(jié)果時(shí)
如何
函數(shù)結(jié)尾 return 值
向外部拋出函數(shù)處理結(jié)果的主要手段
調(diào)用
讓引擎找到函數(shù),按照函數(shù)的步驟清單執(zhí)行程序
如何
var 返回值=函數(shù)名(參數(shù)值列表)
如果函數(shù)定義時(shí),定義了參數(shù)列表,調(diào)用時(shí)必須傳入?yún)?shù)值
傳入的參數(shù)值的順序和個(gè)數(shù)必須和函數(shù)定義中的參數(shù)列表一致
如果函數(shù)定義時(shí),定義了返回值,調(diào)用時(shí)可以用變量接收函數(shù)的返回值
作用域(scope)
什么是
一個(gè)變量的可用范圍
為什么
避免不同范圍的變量間互相污染
包含:
2種
全局作用域
window
全局變量
在函數(shù)外聲明的不屬于任何函數(shù)的變量
特點(diǎn)
隨處可用
可反復(fù)使用
何時(shí):
只要一個(gè)變量希望反復(fù)使用或跨多個(gè)函數(shù)隨處可用時(shí)
函數(shù)作用域
函數(shù)內(nèi)部
局部變量
包含2種
在函數(shù)內(nèi)用var聲明的變量
參數(shù)變量也是局部變量
特點(diǎn)
僅函數(shù)內(nèi)可用
不可重用
何時(shí):
如果希望一個(gè)變量?jī)H在函數(shù)內(nèi)可用,不希望污染函數(shù)外部時(shí)
使用順序
優(yōu)先在函數(shù)作用域中找局部變量使用
如果局部沒有,才去全局找
如果全局沒有,才報(bào)錯(cuò)
聲明提前
在程序正式執(zhí)行前,先將所有var聲明的變量和function聲明的函數(shù)提前到當(dāng)前作用域的頂部,集中創(chuàng)建
賦值留在原地
問題
破壞了程序執(zhí)行的順序
避免
1. 盡量將變量或函數(shù)的聲明集中在當(dāng)前作用域頂部創(chuàng)建
2. 用let代替var
3. 用var 函數(shù)名=function(...){...}代替function 函數(shù)名(...){...}
按值傳遞byValue
在兩變量間賦值時(shí),或?qū)⒆兞孔鳛閰?shù)傳入函數(shù)時(shí),僅將原變量中的值復(fù)制一個(gè)副本給對(duì)方
影響
如果傳遞的是原始類型的值,在函數(shù)中修改新變量,不會(huì)影響原變量
如果傳遞的是引用類型的對(duì)象,在函數(shù)中用新變量修改對(duì)象,等效于直接修改原對(duì)象
全局函數(shù)
ES標(biāo)準(zhǔn)中規(guī)定的,不需要任何前綴.就可直接調(diào)用的函數(shù)
比如:
Number() String() Boolean() isNaN() parseFloat/parseInt()
encodeURI/decodeURI()
encodeURIComponent()/decodeURIComponent()
eval()
反例
document.write() console.log();
alert() prompt()
分支結(jié)構(gòu)
讓程序根據(jù)不同的條件,執(zhí)行不同的任務(wù)
包括
1. 一個(gè)條件一件事,滿足就做,不滿足就不做
如果操作簡(jiǎn)單
條件&&(操作1,操作2,...)
如果操作復(fù)雜
if(條件){ 操作 }
2. 一個(gè)條件2件事,二選一執(zhí)行
如果操作復(fù)雜
if(條件){ 操作1 }else{ 操作2 }
如果操作簡(jiǎn)單
三目/三元/條件
條件?操作1:操作2;
如果根據(jù)不同條件選擇不同的值
三目
條件?值1:值2
3. 多個(gè)條件多件事,多選一執(zhí)行
如果操作復(fù)雜
if(條件1){ 操作1 }else if(條件2){ 操作2 }else if(...){ ... }else{ 默認(rèn)操作 }
最后一個(gè)else不是必須
如果操作簡(jiǎn)單
條件1?操作1: 條件2?操作2: ... ? ... : 默認(rèn)操作
最后必須有一個(gè)默認(rèn)操作
如果只是根據(jù)不同條件返回不同的值時(shí)
條件1?值1: 條件2?值2: ... ? ... : 默認(rèn)值
默認(rèn)值不能省略!
特殊
如果所有條件都是等于比較時(shí)
switch(表達(dá)式){ case 值1: 操作1; break; case 值2: 操作2; break; case ... : ... ; break; default: 默認(rèn)操作 }
原理:
先計(jì)算表達(dá)式的值
再用表達(dá)式的值和每個(gè)case做===比較
只要表達(dá)式的值和某個(gè)case的值全等,就進(jìn)入執(zhí)行該case下的操作
強(qiáng)調(diào):
表達(dá)式的值和case的值做全等比較,意味著首先類型必須相同
每個(gè)case其實(shí)僅是一個(gè)入口而已
問題
默認(rèn)
一但進(jìn)入一個(gè)case開始執(zhí)行,則會(huì)連續(xù)觸發(fā)之后所有case和default的操作
解決
在每個(gè)case之間加break
break退出當(dāng)前結(jié)構(gòu)
循環(huán)結(jié)構(gòu)
讓程序反復(fù)執(zhí)行一段相同代碼
循環(huán)三要素
循環(huán)條件
控制循環(huán)可以繼續(xù)反復(fù)執(zhí)行的條件
每執(zhí)行完一次重復(fù)的操作,都要重新判斷一次循環(huán)條件是否滿足
一旦循環(huán)條件不再滿足,則退出循環(huán),不再反復(fù)執(zhí)行
循環(huán)變量
循環(huán)條件中用作比較和判斷的變量
都要考慮
從幾開始,到幾結(jié)束,每次增/減幾
通常都會(huì)向著不滿足循環(huán)條件的趨勢(shì)不斷變化
循環(huán)體
循環(huán)反復(fù)執(zhí)行的代碼段
三種
while
聲明并初始化循環(huán)變量 while(循環(huán)條件){ 循環(huán)體; 修改循環(huán)變量; }
何時(shí)
循環(huán)變量的變化沒有規(guī)律時(shí)
do while
聲明循環(huán)變量 do{ 循環(huán)體; 修改循環(huán)變量; }while(循環(huán)條件);
vs while
如果第一次循環(huán)條件都滿足
二者效果完全相同
如果第一次循環(huán)條件不滿足
while是一次都不執(zhí)行
do while至少可以執(zhí)行一次
何時(shí)
即使第一次條件就不滿足,也希望至少能執(zhí)行一次時(shí)
for
for(聲明循環(huán)變量; 循環(huán)條件; 修改循環(huán)變量){ 循環(huán)體; }
何時(shí)
循環(huán)變量的變化是有規(guī)律的
簡(jiǎn)寫
1. 聲明循環(huán)變量部分,可同時(shí)聲明并初始化多個(gè)變量,用逗號(hào)分隔
問題: 循環(huán)內(nèi)聲明的變量,循環(huán)外是否可用
答: 可用
因?yàn)閖s沒有塊級(jí)作用域
vs Java
有塊級(jí)作用域
2. 修改循環(huán)變量部分,可同時(shí)執(zhí)行多個(gè)短小的操作,用逗號(hào)分隔
不能修改原程序的執(zhí)行順序
結(jié)尾的分號(hào)不能省
了解
其實(shí)分支結(jié)構(gòu)和循環(huán)結(jié)構(gòu)中,如果if/else/else if/for/while之后只有一句話,可省略{}
禁止使用!
死循環(huán)
循環(huán)條件永遠(yuǎn)為true的不能自己退出的循環(huán)
while(true)
for(;;)
break和continue
break
退出當(dāng)前結(jié)構(gòu),不再循環(huán)
continue
僅跳過本輪循環(huán),依然繼續(xù)執(zhí)行下一輪
退出循環(huán)
2種辦法
1. 用循環(huán)條件控制退出
優(yōu)雅
難度高
2. 用死循環(huán)+break方式退出
野蠻
簡(jiǎn)單
數(shù)組
什么是
內(nèi)存中連續(xù)存儲(chǔ)多個(gè)數(shù)據(jù)的存儲(chǔ)空間,再起一個(gè)名字
為什么
連續(xù)存儲(chǔ)的一組數(shù)據(jù),可極大提高程序的執(zhí)行效率
便于維護(hù)和查找
何時(shí)
只要存儲(chǔ)多個(gè)數(shù)據(jù)都要用數(shù)組
如何
創(chuàng)建
1. 創(chuàng)建空數(shù)組
何時(shí)
如果創(chuàng)建數(shù)組時(shí),暫時(shí)不知道數(shù)組的內(nèi)容
數(shù)組直接量
var arr=[]
用new
var arr=new Array();
2. 創(chuàng)建數(shù)組同時(shí)初始化數(shù)組元素
何時(shí)
如果創(chuàng)建數(shù)組時(shí),已經(jīng)知道數(shù)組內(nèi)容
數(shù)組直接量
var arr=[值1, 值2,...]
用new
var arr=new Array(值1,值2,...);
3. 創(chuàng)建n個(gè)空元素的數(shù)組
何時(shí)
如果創(chuàng)建數(shù)組時(shí)只知道數(shù)組的元素個(gè)數(shù),暫時(shí)不知道內(nèi)容時(shí)
用new
var arr=new Array(n)
訪問
元素
保存在數(shù)組中的一個(gè)數(shù)據(jù)
下標(biāo)
數(shù)組中唯一標(biāo)識(shí)元素存儲(chǔ)位置的序號(hào)
默認(rèn)從0開始,依次遞增,連續(xù)不重復(fù),到length-1結(jié)束
arr[i]
單個(gè)元素的用法和單個(gè)變量完全一樣
數(shù)組其實(shí)是一組變量的集合,再起一個(gè)統(tǒng)一的變量名
三個(gè)不限制
不限制元素的數(shù)據(jù)類型
不限制下標(biāo)越界
取值
不報(bào)錯(cuò),返回undefined
賦值
不報(bào)錯(cuò),自動(dòng)在指定位置創(chuàng)建新元素
稀疏數(shù)組
下標(biāo)不連續(xù)的數(shù)組
自動(dòng)將length調(diào)整到最大下標(biāo)+1
不限制元素個(gè)數(shù)
可隨時(shí)在任意位置添加新元素
添加新元素后,都會(huì)自動(dòng)改變length屬性為最大數(shù)字下標(biāo)+1
.length屬性
規(guī)定了數(shù)組理論上的元素個(gè)數(shù)
始終等于最大下標(biāo)+1
自動(dòng)維護(hù)
固定套路
獲取最后一個(gè)元素
arr[arr.length-1]
獲取倒數(shù)第n個(gè)元素
arr[arr.length-n]
末尾追加一個(gè)新元素
arr[arr.length]=值
縮容
刪除末尾一個(gè)元素
arr.length--
刪除末尾n個(gè)元素
arr.length-=n
清空數(shù)組
arr.length=0
數(shù)組是引用類型
不直接存儲(chǔ)在變量本地
一個(gè)變量只能存1個(gè)值
實(shí)際存儲(chǔ)在變量之外
每個(gè)數(shù)組都有一個(gè)唯一的地址值
變量中只保存數(shù)組的地址值
也稱變量引用著數(shù)組
按值傳遞
兩變量間賦值或?qū)⒆兞總鹘o函數(shù)參數(shù)時(shí),其實(shí)只是將原變量中的值復(fù)制一個(gè)副本給對(duì)方
原始類型
修改新變量,不影響原變量
引用類型
通過新變量修改數(shù)組,同樣會(huì)影響原變量
為什么
將原變量中的地址值復(fù)制給新變量
結(jié)果
兩個(gè)變量引用同一個(gè)數(shù)組
遍歷
依次取出數(shù)組中每個(gè)元素的值,執(zhí)行相同的操作
何時(shí)
只要對(duì)數(shù)組中每個(gè)元素執(zhí)行相同操作時(shí)
如何
索引數(shù)組
for(var i=0;i<arr.length;i++){ arr[i] //當(dāng)前數(shù)組元素 }
關(guān)聯(lián)數(shù)組
什么是
下標(biāo)都是數(shù)字的數(shù)組,稱為索引數(shù)組
可自定義下標(biāo)名稱的數(shù)組,稱為關(guān)聯(lián)數(shù)組
為什么
索引數(shù)組的數(shù)字下標(biāo)沒有意義,只能通過遍歷查找指定的元素
查找速度受數(shù)組元素個(gè)數(shù)和元素位置的影響
何時(shí)
希望通過下標(biāo)名稱快速查找某個(gè)元素時(shí)
無需遍歷
不受元素個(gè)數(shù)和元素存儲(chǔ)位置的影響
如何
創(chuàng)建
2步
創(chuàng)建空數(shù)組
var hash=[]
向數(shù)組中添加新元素
hash["下標(biāo)名(key)"]=值(value)
訪問
hash["下標(biāo)名(key)"]
用法同訪問索引數(shù)組中的元素
特點(diǎn)
.length屬性始終為0
.length屬性只能統(tǒng)計(jì)數(shù)字下標(biāo)的房間,不能統(tǒng)計(jì)自定義下標(biāo)的房間。
無法使用索引數(shù)組的API
遍歷
for(var key in hash){ key //僅獲取當(dāng)前下標(biāo)名稱 hash[key] //獲取當(dāng)前元素值 }
in 依次取出關(guān)聯(lián)數(shù)組中每個(gè)下標(biāo)名稱保存在變量key
固定套路
僅獲取hash中的所有key
var keys=[]; var i=0; for(keys[i++] in hash); //結(jié)束后: keys中保存了hash的所有key
數(shù)組API
轉(zhuǎn)字符串
String(arr)
將arr中每個(gè)元素轉(zhuǎn)為字符串,用逗號(hào)鏈接
拍照
arr.join("連接符")
將arr中每個(gè)元素轉(zhuǎn)為字符串,可自定義連接符
固定套路
無縫拼接
arr.join("")
判斷空數(shù)組
arr.join("")===""
動(dòng)態(tài)生成頁面元素
var html="<ANY>"+arr.join("</ANY><ANY>")+"</ANY>"
elem.innerHTML=html;
拼接和選取
強(qiáng)調(diào): 都無權(quán)修改原數(shù)組,只能返回新數(shù)組,必須用變量接住返回值
拼接
var newArr=arr1.concat(值1,值2,arr2,....)
將值1,值2,arr2中的每個(gè)元素,拼接到arr1結(jié)尾
強(qiáng)調(diào):
可打散數(shù)組類型參數(shù)
選取
var subArr=arr.slice(starti,endi+1);
選取arr中starti位置到endi位置的元素,組成新數(shù)組返回
強(qiáng)調(diào):
凡是兩個(gè)參數(shù)都是下標(biāo)的API
含頭不含尾
簡(jiǎn)寫
負(fù)數(shù)參數(shù)
倒數(shù)第n個(gè)
本質(zhì):
自動(dòng)執(zhí)行l(wèi)ength-n
省略第二個(gè)參數(shù)
從starti一直選取到結(jié)尾
省略全部?jī)蓚€(gè)參數(shù)
復(fù)制整個(gè)數(shù)組
用途
將類數(shù)組對(duì)象轉(zhuǎn)化為數(shù)組對(duì)象
Array.prototype.slice.call(arguments)
相當(dāng)于
arguments.slice()
固定套路:
獲得i位置開始的n個(gè)元素
arr.slice(i,i+n)
修改數(shù)組
刪除元素
arr.splice(starti,n)
刪除arr中starti位置開始的n個(gè)元素
強(qiáng)調(diào):
直接修改原數(shù)組
不用考慮含頭不含尾
簡(jiǎn)寫:
支持負(fù)數(shù)參數(shù),表示倒數(shù)第n個(gè)
省略第二個(gè)參數(shù)
刪除starti位置后所有元素
其實(shí)有返回值
返回被刪除的元素組成的臨時(shí)數(shù)組
var deletes=arr.splice(starti,n)
插入元素
arr.splice(starti,0,值1,值2,...)
在arr中starti位置插入新值,原starti位置的值及其之后的值被向后順移
強(qiáng)調(diào):
不支持打散數(shù)組類型參數(shù)
如果插入子數(shù)組,會(huì)變成二維數(shù)組
替換
arr.splice(starti,n,值1,值2...)
先刪除arr中starti位置的n個(gè)元素,再在starti位置插入新元素
強(qiáng)調(diào):
刪除的元素個(gè)數(shù)不一定和插入的元素個(gè)數(shù)一致
固定套路
廣告輪播
移除開頭的n個(gè)元素拼到結(jié)尾
imgs=imgs.concat(imgs.splice(0,n))
移除結(jié)尾的n個(gè)元素拼到開頭
imgs=imgs.splice(-n).concat(imgs)
翻轉(zhuǎn)
arr.reverse()
排序
手寫排序:
冒泡,快速,插入
冒泡
依次比較相鄰兩數(shù),如果前數(shù)>后數(shù),就交換兩數(shù)位置
for(var r=1;r<arr.length;r++){ for(var i=0;i<arr.length-r;i++){ if(arr[i]>arr[i+1]){ arr[i]=[arr[i+1],arr[i+1]=arr[i]][0]; } } }
arr.sort()
將arr中每個(gè)元素轉(zhuǎn)為字符串,再按字符串升序排列
問題:
只能按字符串升序排列
解決:
自定義比較器函數(shù)
2步:
1. 定義比較器函數(shù)
專門比較兩個(gè)值大小的函數(shù)
2個(gè)要求
1. 兩個(gè)參數(shù)a,b
2. 返回值
a>b,返回正數(shù)
a<b,返回負(fù)數(shù)
a==b,返回0
比如
最簡(jiǎn)單的數(shù)字升序比較器
function compare(a,b){return a-b;}
2. 將比較器函數(shù)作為對(duì)象傳入sort方法
arr.sort(compare)
強(qiáng)調(diào):
回調(diào)函數(shù)
將一個(gè)函數(shù),作為對(duì)象傳入另一個(gè)函數(shù)內(nèi),被另一個(gè)函數(shù)使用
傳入回調(diào)函數(shù)時(shí),不要加()
因?yàn)椴皇橇⒖陶{(diào)用!也不是只調(diào)用一次!而是交給別人去調(diào)用
其實(shí)都會(huì)簡(jiǎn)寫為:
arr.sort(function(a,b){return a-b;})
ES6
arr.sort((a,b)=>a-b)
降序排列
顛倒比較器函數(shù)返回值的正負(fù)號(hào)
比如:
數(shù)字降序比較器
function compare(a,b){return b-a;}
棧和隊(duì)列
何時(shí)
只要按照順序使用數(shù)組中的元素時(shí)
js中沒有專門的棧和隊(duì)列的類型,都是用普通數(shù)組模擬的
棧stack
一端封閉只能從另一端進(jìn)出的數(shù)組
FILO
何時(shí)
希望始終使用最新進(jìn)入數(shù)組的元素時(shí)
如何
結(jié)尾出入棧
入
arr.push(值)
arr[arr.length]=值
出
var last=arr.pop()
開頭出入棧
入
arr.unshift(值)
arr.splice(0,0,值)
出
var first=arr.shift()
強(qiáng)調(diào):
開頭入棧和結(jié)尾入棧的順序是相反的
隊(duì)列queue
只能從結(jié)尾進(jìn)入,從開頭出的數(shù)組
FIFO
何時(shí)
只要按照先來后到的順序使用數(shù)組元素時(shí)
如何
結(jié)尾入
另見:
開頭出
另見:
二維數(shù)組
什么是
數(shù)組中的元素內(nèi)容,又是一個(gè)子數(shù)組
何時(shí)
1. 保存橫行豎列的二維數(shù)據(jù)
2. 對(duì)大的數(shù)組中的元素,再進(jìn)行細(xì)分類
如何
創(chuàng)建
1. 先創(chuàng)建空數(shù)組,再賦值
var arr=[];
arr[i]=[值1,值2,...]
2. 在創(chuàng)建數(shù)組同時(shí),初始化子數(shù)組
var arr=[ [值1,值2,...], [值1,值2,...], ]
訪問
arr[r][c]
用法和普通數(shù)組元素的用法完全一樣
強(qiáng)調(diào):
任何情況下行下標(biāo)r不能越界
報(bào)錯(cuò)!
遍歷:
for(var r=0;r<arr.length;r++){ for(var c=0;c<arr[r].length;c++){ arr[r][c] //當(dāng)前元素 } }
String
什么是
多個(gè)字符組成的只讀字符數(shù)組
vs 數(shù)組
相同
下標(biāo)
.length
for遍歷
slice()
不同
兩者類型不同
API不通用
API
強(qiáng)調(diào)
所有String API都無權(quán)修改原字符串,只能返回新字符串
大小寫轉(zhuǎn)換
何時(shí)
只要不區(qū)分大小寫時(shí),都要先轉(zhuǎn)換為一致的大小寫,再比較
str.toUpperCase()
str.toLowerCase()
獲取指定位置字符
var char=str.charAt(i)
str[i]
var unicode=str.charCodeAt(i)
簡(jiǎn)寫
省略i, 默認(rèn)為0
var char=String.fromCharCode(unicode)
選取子字符串
var subStr=str.slice(starti,endi+1)
簡(jiǎn)寫:
同數(shù)組的slice
str.substring(starti,endi+1)
簡(jiǎn)寫
另見:
不支持負(fù)數(shù)參數(shù)
變通:
str.length-n
str.substr(starti,n)
不考慮含頭不含尾
str.substring(starti,starti+n)
查找關(guān)鍵詞
1. 查找一個(gè)固定的關(guān)鍵詞出現(xiàn)的位置
var i=str.indexOf("關(guān)鍵詞",fromi)
在str中從fromi位置開始查找下一個(gè)關(guān)鍵詞的位置
返回值
返回找到的關(guān)鍵詞所在的位置
如果找不到返回-1
固定套路
查找所有關(guān)鍵詞出現(xiàn)的位置
var i=-1; while((i=str.indexOf("關(guān)鍵詞",i+1))!=-1){ i //本次找到的關(guān)鍵詞位置 }
簡(jiǎn)寫:
省略fromi,默認(rèn)為0
優(yōu):
可以指定開始位置,可以找所有
缺:
不支持正則,一次只能找一種關(guān)鍵詞
專門查找最后一個(gè)關(guān)鍵詞的位置
var i=str.lastIndexOf("關(guān)鍵詞")
2. 判斷是否包含符合規(guī)則的關(guān)鍵詞
var i=str.search(/正則表達(dá)式/i)
如果返回-1,說明不包含,如果返回不是-1,說明包含
永遠(yuǎn)只找第一個(gè)關(guān)鍵詞的位置
忽略大小寫
/正則表達(dá)式/i
強(qiáng)調(diào)
不支持g
優(yōu):
支持正則
缺:
不能設(shè)置開始查找位置,只能找第一個(gè),不能找所有
只能返回位置,不能返回關(guān)鍵詞內(nèi)容
3. 獲取關(guān)鍵詞的內(nèi)容
只獲得一個(gè)關(guān)鍵詞的內(nèi)容
a. var arr=str.match(/正則/i);
b. 在str中查找第一個(gè)符合正則要求的敏感詞的內(nèi)容和位置
c. 返回值:
1). 如果找到,返回一個(gè)數(shù)組,包含兩個(gè)元素: arr: [ 0: "敏感詞內(nèi)容", "index":"位置" ]
2). 如果沒找到,返回null
獲得所有關(guān)鍵詞的內(nèi)容
var kwards=str.match(/正則表達(dá)式/ig)
強(qiáng)調(diào):
省略g,只找第1個(gè)
加g,才找所有
返回包含所有關(guān)鍵詞的數(shù)組
如果沒找到,返回null
如果一個(gè)函數(shù)可能返回null,都要先驗(yàn)證,再使用結(jié)果
優(yōu):
獲得所有關(guān)鍵詞的內(nèi)容
缺:
無法返回每個(gè)關(guān)鍵詞的位置
4. 即獲得每個(gè)關(guān)鍵詞的內(nèi)容,又獲得每個(gè)關(guān)鍵詞的位置
regexp.exec(str)
替換
簡(jiǎn)單替換:
將所有關(guān)鍵詞都替換為統(tǒng)一的新值
str=str.replace(/正則表達(dá)式/ig,"替換值")
問題: 無法根據(jù)不同的關(guān)鍵詞,選擇不同的值替換
高級(jí)替換:
根據(jù)每個(gè)關(guān)鍵詞的不同,動(dòng)態(tài)返回不同的替換值
str=str.replace(/正則表達(dá)式/ig,function(kw,,,...){ //kw: 會(huì)自動(dòng)獲得本次找到的完整關(guān)鍵詞 //$n: 會(huì)自動(dòng)獲得本次找到的關(guān)鍵詞中第n個(gè)分組的子內(nèi)容 return 根據(jù)不同kw,返回不同替換值 })
衍生
刪除
替換為空字符串
格式化
2步
1. 用正則對(duì)原始字符串分組
var reg=/(\d{4})(\d{2})(\d{2})/
正則中每個(gè)分組都會(huì)自動(dòng)獲得一個(gè)分組序號(hào),從1開始
2. 在replace的替換值中使用$n,重新拼接新的格式
birth.replace(reg,"年月日")
切割
簡(jiǎn)單切割
var subs=str.split("分隔符")
復(fù)雜切割
var subs=str.split(/正則表達(dá)式/)
返回值
多段子字符串組成的數(shù)組
切割后的結(jié)果中不包含分隔符
固定套路
將字符串打散為字符數(shù)組
var chars=str.split("")
正則表達(dá)式
什么是
規(guī)定一個(gè)字符串中字符出現(xiàn)規(guī)律的規(guī)則
何時(shí)
1. 按規(guī)則模糊查找多種關(guān)鍵詞時(shí)
2. 用規(guī)則驗(yàn)證用戶輸入的格式時(shí)
1. 關(guān)鍵詞的原文就是最簡(jiǎn)單的正則表達(dá)式
2. 字符集
何時(shí)
只要一位字符有多個(gè)備選字時(shí)
[備選字符列表]
強(qiáng)調(diào):
一個(gè)字符集只能匹配一位字符
簡(jiǎn)寫
[0-9]
[a-z]
[A-Z]
[A-Za-z]
[A-Za-z0-9]
[\u4e00-\u9fa5]
除了
[^47]
3. 預(yù)定義字符集
\d
[0-9]
\w
[A-Za-z0-9_]
\s
空字符
空格,制表符...
.
通配符
4. 量詞
何時(shí)
只要規(guī)定一個(gè)字符集出現(xiàn)次數(shù)時(shí)
如何
字符集量詞
強(qiáng)調(diào):
默認(rèn)僅修飾相鄰的前一個(gè)字符集
有明確數(shù)量邊界
字符集{n,m}
至少n個(gè),最多m個(gè)
字符集{n,}
n個(gè)以上
字符集{n}
必須n個(gè)
沒有明確邊界
字符集?
{0,1}
字符集*
{0,}
字符集+
{1,}
5. 選擇和分組
分組
(多個(gè)規(guī)則)
何時(shí):
1. 希望一個(gè)量詞同時(shí)修飾多個(gè)字符集時(shí)
身份證號(hào)
\d{15}(\d\d[0-9Xx])?
2. 希望分段獲取或處理字符串中部分子內(nèi)容時(shí)
格式化生日
(\d{4})(\d{2})(\d{2})
選擇
規(guī)則1|規(guī)則2
| 優(yōu)先級(jí)最低
何時(shí)
在兩種規(guī)則間任選其一匹配
微信
(微|w(ei)?)\s*(信|x(in)?)
6. 指定匹配位置
^ 字符串開頭
比如
開頭的空字符
^\s+
$ 字符串結(jié)尾
比如
結(jié)尾的空字符
\s+$
開頭或結(jié)尾的空字符
^\s+|\s+$
\b 單詞邊界
^ $ 空字符 標(biāo)點(diǎn)
比如:
查找每個(gè)單詞首字母
緊挨著單詞邊界之后的一個(gè)字符
\b[a-z]
7. 密碼強(qiáng)度:
6~8位字母,數(shù)字的組合,至少包含一個(gè)大寫字母和一位數(shù)字
^(?![a-z0-9]+$)(?![A-Za-z]+$)[A-Za-z0-9]{6,8}$
RegExp
什么是
封裝一條正則表達(dá)式,并提供用正則表達(dá)式執(zhí)行驗(yàn)證和查找的API
何時(shí)
1. 使用正則表達(dá)式驗(yàn)證字符串格式
2. 即查找關(guān)鍵詞內(nèi)容,又查找關(guān)鍵詞位置
創(chuàng)建
1. 直接量
var reg=/正則表達(dá)式/ig
何時(shí)
如果正則表達(dá)式是固定不變的
字符沖突
/ -> \/
2. 用new
var reg=new RegExp("正則表達(dá)式","ig");
何時(shí)
如果正則表達(dá)式需要?jiǎng)討B(tài)生成
字符沖突:
\->\ \" \'
new RegExp("\d{6}")
API
查找關(guān)鍵詞
var arr=reg.exec(str)
即查找內(nèi)容又查找位置
在str中查找下一個(gè)滿足reg要求的關(guān)鍵詞
返回值:
arr: [0: "完整關(guān)鍵詞", 1: , 2: ,..., index: 本次找到關(guān)鍵詞的位置]
reg.lastIndex
下次開始位置
如果沒找到,返回null
都要先判斷不是null,再使用
exec做三件事
1. 將本次找到的關(guān)鍵詞,放入數(shù)組第0個(gè)元素, 將每個(gè)分組的子內(nèi)容放入后續(xù)元素
2. 修改數(shù)組的index屬性,記錄本次找到關(guān)鍵詞的位置
3. 修改reg.lastIndex屬性=index+關(guān)鍵詞的長(zhǎng)度
固定套路:
找全部
var arr=null; while((arr=reg.exec(str))!=null){ arr[0] //完整關(guān)鍵詞 arr[n] //第n個(gè)分組的子內(nèi)容 arr.index //本次找到關(guān)鍵詞的位置 reg.lastIndex //下次開始查找的位置 }
簡(jiǎn)寫
如果只獲得某個(gè)分組的子內(nèi)容
while(reg.exec(str)!=null){ RegExp.$n //第n個(gè)分組的子內(nèi)容 }
驗(yàn)證
var bool=reg.test(str)
驗(yàn)證str是否符合reg的規(guī)則要求
問題:
test默認(rèn),只要部分匹配就返回true
解決:
只要驗(yàn)證,正則都要前加^,后加$
表示從頭到尾完全匹配
Math
不能new
所有API都用Math.直接調(diào)用
API
1. 取整
上取整
Math.ceil(num)
下取整
Math.floor(num)
只能對(duì)純數(shù)字內(nèi)容下取整
如果傳入的不是數(shù)字,就自動(dòng)調(diào)用Number(x)隱式轉(zhuǎn)換為數(shù)字
vs parseInt(str)
先去掉字符串后非數(shù)字字符,再省略小數(shù)部分
如果傳入的不是字符串,就自動(dòng)調(diào)用String(x),先轉(zhuǎn)為字符串
問題:
多數(shù)情況下,只去單位,還要保留小數(shù)
解決:
如果只是去單位,首選parseFloat
四舍五入取整
Math.round(num)
返回值是number
可直接做算術(shù)計(jì)算
只能取整
不能指定小數(shù)位數(shù)
vs num.toFixed(d)
返回值string
必須先轉(zhuǎn)換,再計(jì)算
可以按任意小數(shù)位數(shù)四舍五入
自定義round
function round(num,d){ num*=Math.pow(10,d); num=Math.round(num); return num/Math.pow(10,d); }
2. 乘方和開平方
乘方
Math.pow(底數(shù),冪)
開平方
Math.sqrt(num)
for(var i=2;i<=Math.sqrt(num);i++){ if(num%i==0){不是質(zhì)數(shù),return} } 是質(zhì)數(shù)
3. 最大值和最小值
Math.max/min(值1,值2,...)
問題:
不支持查找一個(gè)數(shù)組中的最大值/最小值
解決:
Math.max/min.apply(null,arr)
4. 隨機(jī)數(shù)
默認(rèn)
0<=Math.random()<1
在任意min~max之間生成隨機(jī)整數(shù)
parseInt(Math.random()*(max-min+1)+min)
在0~max之間生成隨機(jī)整數(shù)
parseInt(Math.random()*(max+1))
Date
封裝一個(gè)時(shí)間,并提供操作時(shí)間的API
創(chuàng)建
4種
1. 自動(dòng)獲得客戶端當(dāng)前系統(tǒng)時(shí)間
var now=new Date()
2. 創(chuàng)建日期對(duì)象保存自定義時(shí)間
var date=new Date("yyyy/MM/dd hh:mm:ss")
var date=new Date(yyyy,MM-1,dd,hh,mm,ss)
3. 復(fù)制一個(gè)日期對(duì)象
為什么
日期的計(jì)算都是直接修改原日期對(duì)象,計(jì)算后,原日期無法保留
何時(shí)
只要需要同時(shí)保存開始和結(jié)束時(shí)間時(shí),都要先將開始時(shí)間復(fù)制一個(gè)副本,再用副本計(jì)算截止時(shí)間
var date2=new Date(date1)
4. 用毫秒數(shù)
var date=new Date(ms)
何時(shí):
將獲得ms數(shù)轉(zhuǎn)化為當(dāng)?shù)氐臅r(shí)間時(shí)
本質(zhì)
日期對(duì)象中存儲(chǔ)的是1970年1月1日0點(diǎn)至今的毫秒數(shù)
為什么
毫秒數(shù)不受時(shí)區(qū)影響
同一個(gè)毫秒數(shù),在不同時(shí)區(qū)可用new Date()輕松轉(zhuǎn)化為當(dāng)?shù)貢r(shí)間顯示
var ms=date.getTime();
API
單位
FullYear, Month, Date, Day
沒有s結(jié)尾
Hours, Minutes, Seconds Milliseconds
有s結(jié)尾
每個(gè)單位都有一對(duì)兒getXXX/setXXX方法
date.getXXX()負(fù)責(zé)獲取指定單位的數(shù)值
date.setXXX(num)負(fù)責(zé)設(shè)置指定單位的數(shù)值
優(yōu)
自動(dòng)調(diào)整時(shí)間進(jìn)制
特例:
Day沒有setXXX()方法
取值范圍:
只有月中的日Date,從1開始到31結(jié)束
其余都從0~進(jìn)制-1結(jié)束
只有月份Month需要修正
0~11
計(jì)算機(jī)中的月份比現(xiàn)實(shí)中的月份至小1
其余都不需要修正
計(jì)算
兩日期對(duì)象可相減
得到ms差
可計(jì)算倒計(jì)時(shí)
對(duì)任意單位做加減
date.setXXX(date.getXXX()+n)
強(qiáng)調(diào):
1. 直接修改原日期對(duì)象
另見:
2. setXXX() 可自動(dòng)調(diào)整時(shí)間進(jìn)制
格式化
toString()
當(dāng)?shù)貥?biāo)準(zhǔn)時(shí)間的完整格式
toLocaleString()
當(dāng)?shù)貢r(shí)間的簡(jiǎn)化版格式
toLocaleDateString()
當(dāng)?shù)貢r(shí)間格式
僅保留日期部分
有兼容性問題
toLocaleTimeString()
當(dāng)?shù)貢r(shí)間格式
僅保留時(shí)間部分
toGMTString()
國際標(biāo)準(zhǔn)時(shí)間(0時(shí)區(qū))
Error
什么是錯(cuò)誤
代表程序執(zhí)行過程中導(dǎo)致程序無法正常執(zhí)行的原因
錯(cuò)誤處理
什么是
即使程序發(fā)生錯(cuò)誤,也保證不異常退出的機(jī)制
為什么
任何程序只要發(fā)生錯(cuò)誤,就會(huì)立刻中斷退出
何時(shí)
只要希望程序即使出錯(cuò),也不會(huì)中斷退出
如何
try{ 可能出錯(cuò)的代碼 }catch(err){ 只有出錯(cuò)才執(zhí)行的錯(cuò)誤處理代碼 比如: 錯(cuò)誤提示,記錄日志,保存進(jìn)度/數(shù)據(jù) }finally{ 無論是否出錯(cuò)都必須執(zhí)行的代碼 釋放資源 }
Error錯(cuò)誤對(duì)象
什么是
在發(fā)生錯(cuò)誤時(shí)自動(dòng)創(chuàng)建的封裝錯(cuò)誤信息的對(duì)象
name
錯(cuò)誤類型
6種
SyntaxError, ReferenceError, TypeError, RangeError, EvalError, URIError
message
錯(cuò)誤提示信息
String(err)
錯(cuò)誤類型:錯(cuò)誤提示信息
優(yōu)化
如果可以提前預(yù)知錯(cuò)誤的原因
建議用if...else...代替try...catch
拋出自定義錯(cuò)誤
在協(xié)作開發(fā)中,程序的作者用于提醒調(diào)用者錯(cuò)誤的使用了你的程序
throw new Error("自定義錯(cuò)誤信息")
Function
什么是
函數(shù)其實(shí)是一個(gè)封裝一段代碼段的對(duì)象
函數(shù)名其實(shí)僅是引用函數(shù)對(duì)象的一個(gè)普通變量
為什么
代碼重用
何時(shí)
只要一項(xiàng)任務(wù),可能被反復(fù)使用,都要定義為函數(shù),反復(fù)使用函數(shù)
創(chuàng)建
3種
1. 聲明
function 函數(shù)名(參數(shù)列表){函數(shù)體; return 返回值}
會(huì)被聲明提前
hoist
什么是
在開始執(zhí)行程序前
引擎會(huì)首先查找var聲明的變量和function聲明的函數(shù)
將其提前到當(dāng)前作用域的頂部集中創(chuàng)建
賦值留在原地
鄙視時(shí)
凡是看到先使用,后聲明,都是在聲明提前
先改為聲明提前之后的程序,再判斷輸出
解決問題
1. 所有變量和函數(shù)的聲明都放在當(dāng)前作用域的頂部
2. ES6
可用let代替var
要求
當(dāng)前作用域中l(wèi)et a之前不允許出現(xiàn)未聲明的a
3. 函數(shù)
另見:
2. 直接量
var 函數(shù)名=function(參數(shù)列表){函數(shù)體; return 返回值}
不會(huì)被聲明提前
3. 用new
var 函數(shù)名=new Function("參數(shù)名1","參數(shù)名2",...,"函數(shù)體; return 返回值")
參數(shù):
接收函數(shù)執(zhí)行時(shí)必須的數(shù)據(jù)的變量
為什么
可讓函數(shù)更靈活
何時(shí)
只要函數(shù)必須某些數(shù)據(jù)才能正常執(zhí)行
返回值:
函數(shù)的執(zhí)行結(jié)果
何時(shí)
如果調(diào)用者需要獲得函數(shù)的返回值
調(diào)用
讓引擎按照函數(shù)的步驟清單,執(zhí)行任務(wù)
強(qiáng)調(diào):
函數(shù)不調(diào)用不執(zhí)行
只有調(diào)用才執(zhí)行
重載(overload)
什么是
相同函數(shù)名,不同參數(shù)列表的多個(gè)函數(shù)
在調(diào)用時(shí)
根據(jù)傳入?yún)?shù)的不同,自動(dòng)選擇匹配的函數(shù)執(zhí)行
為什么
減少API的數(shù)量,減輕調(diào)用者的負(fù)擔(dān)
何時(shí)
只要一項(xiàng)任務(wù),需要根據(jù)不同的參數(shù),執(zhí)行不同的操作時(shí)
如何
問題
js語法不支持重載
js不允許多個(gè)同名函數(shù)同時(shí)存在
解決
每一個(gè)函數(shù)內(nèi),都有一個(gè)arguments對(duì)象接住所有傳入函數(shù)的參數(shù)值
根據(jù)arguments的元素內(nèi)容或元素個(gè)數(shù),判斷執(zhí)行不同的操作
arguments
函數(shù)調(diào)用時(shí),自動(dòng)創(chuàng)建的
自動(dòng)接收所有傳入函數(shù)的參數(shù)值的
類數(shù)組對(duì)象
長(zhǎng)的像數(shù)組的對(duì)象
vs 數(shù)組
相同
1. 下標(biāo)
2. length
3. for/for of遍歷
不同
類型不同
數(shù)組是Array類型
類數(shù)組對(duì)象是Object類型
類數(shù)組對(duì)象無法使用數(shù)組的API
匿名函數(shù)
什么是
函數(shù)創(chuàng)建時(shí),沒有指定函數(shù)名
使用后自動(dòng)釋放!
為什么
節(jié)約內(nèi)存
劃分臨時(shí)作用域,避免全局變量
何時(shí)
只要一個(gè)函數(shù)只用一次
1. 回調(diào)callback
將一個(gè)函數(shù)作為參數(shù)傳入另一個(gè)函數(shù)內(nèi),被其他函數(shù)調(diào)用
比如:
arr.sort(function(a,b){return a-b;})
ES6
arr.sort((a,b)=>a-b)
str.replace(/reg/g, function(kw,,,...){return 替換值})
ES6
str.replace(/reg/g, (kw,,,...)=>{return 替換值})
2. 自調(diào)
定義函數(shù)后自己調(diào)用自己,調(diào)用后,立刻釋放
何時(shí)
定義一個(gè)臨時(shí)作用域,減少使用全局變量, 避免全局污染
如何
(function(參數(shù)列表){函數(shù)體; return 返回值})(參數(shù)值列表)
+function(參數(shù)列表){函數(shù)體; return 返回值}(參數(shù)值列表)
作用域和作用域鏈
作用域(scope)
變量的可用范圍
其實(shí)是一個(gè)對(duì)象
包括
全局作用域?qū)ο?/p>
window
保存全局變量
優(yōu)
可反復(fù)使用
缺
隨處可用
全局污染
建議:
盡量少用全局變量
函數(shù)作用域?qū)ο?/p>
AO
保存局部變量
包括
2種
參數(shù)變量
函數(shù)內(nèi)var出的變量
優(yōu)
僅函數(shù)內(nèi)可用
不會(huì)被污染
缺
不可重用
函數(shù)的生命周期
開始執(zhí)行程序前
創(chuàng)建ECS
專門保存正在調(diào)用的函數(shù)的執(zhí)行環(huán)境的 數(shù)組
首先在ECS中添加瀏覽器主程序的執(zhí)行環(huán)境main
創(chuàng)建全局作用域?qū)ο體indow
所有全局變量都是保存在window對(duì)象中
main執(zhí)行環(huán)境引用window
定義函數(shù)時(shí)
用函數(shù)名聲明全局變量
創(chuàng)建函數(shù)對(duì)象,封裝函數(shù)定義
函數(shù)對(duì)象的scope屬性,指回函數(shù)創(chuàng)建時(shí)的作用域
函數(shù)名變量引用函數(shù)對(duì)象
調(diào)用函數(shù)時(shí)
向ECS中壓入本次函數(shù)調(diào)用的執(zhí)行環(huán)境元素
創(chuàng)建本次函數(shù)調(diào)用時(shí)使用的函數(shù)作用域?qū)ο?AO)
在AO中創(chuàng)建所有局部變量
包括
形參變量
函數(shù)內(nèi)用var聲明的變量
設(shè)置AO的parent屬性引用函數(shù)的scope屬性指向的父級(jí)作用域?qū)ο?/p>
函數(shù)的執(zhí)行環(huán)境引用AO
變量的使用順序
先在AO中找局部變量
AO中沒有才去父級(jí)作用域中找
調(diào)用后
函數(shù)的執(zhí)行環(huán)境出棧
導(dǎo)致AO釋放
導(dǎo)致AO中的局部變量一同被釋放
作用域鏈(scope chain)
由各級(jí)作用域逐級(jí)引用,形成的鏈?zhǔn)浇Y(jié)構(gòu)
保存著所有的變量
控制著
變量的使用順序
另見:
函數(shù)中,沒有用任何對(duì)象/this就直接訪問的變量,在作用域鏈中找
閉包(closure)
什么是
即重用變量,又保護(hù)變量不被污染的機(jī)制
為什么
全局變量
優(yōu):
可重用
缺:
易被污染
局部變量
缺:
不可重用
優(yōu):
不會(huì)被污染
何時(shí):
即重用變量,又保護(hù)變量不被污染
如何
三特點(diǎn)(3步)
1. 外層函數(shù)
包裹受保護(hù)的變量和操作變量的內(nèi)層函數(shù)
2. 外層函數(shù)要返回內(nèi)層函數(shù)的對(duì)象
3種
1. return function(){...}
2. 直接給全局變量賦值一個(gè)內(nèi)部function
3. 將內(nèi)部函數(shù)保存在一個(gè)對(duì)象的屬性或數(shù)組元素中
return [function,function,function]
return { fun:function(){...} }
3. 調(diào)用外層函數(shù),用外部變量接住返回的內(nèi)層函數(shù)對(duì)象
形成閉包
閉包如何形成
外層函數(shù)調(diào)用后,外層函數(shù)的作用域?qū)ο?AO),無法釋放
被內(nèi)層函數(shù)對(duì)象的scope引用著
缺:
比普通函數(shù)占用更多內(nèi)存
多的是外層函數(shù)的作用域?qū)ο?AO)始終存在
容易造成內(nèi)存泄漏
解決:
如何釋放閉包
將引用內(nèi)層函數(shù)對(duì)象的外部變量置為null
導(dǎo)致內(nèi)層函數(shù)對(duì)象被釋放
導(dǎo)致外層函數(shù)的AO被釋放
鄙視
畫簡(jiǎn)圖
2步
1. 找受保護(hù)的的變量,確定外層函數(shù)調(diào)用后,受保護(hù)變量的最終值
2. 找操作受保護(hù)的變量的內(nèi)層函數(shù)對(duì)象
另見:
結(jié)論:
同一次外層函數(shù)調(diào)用,返回的內(nèi)層函數(shù)對(duì)象,共用同一個(gè)受保護(hù)的變量
OOP
面向?qū)ο笕筇攸c(diǎn):
封裝
創(chuàng)建一個(gè)對(duì)象,集中存儲(chǔ)一個(gè)事物的屬性和功能
繼承
父對(duì)象中的成員,子對(duì)象無需重復(fù)創(chuàng)建,就可直接使用
多態(tài)
同一事物,在不同情況下,表現(xiàn)出不同的狀態(tài)
封裝——?jiǎng)?chuàng)建對(duì)象
也稱為封裝
將一個(gè)事物的屬性和功能集中定義在一個(gè)對(duì)象中
事物的屬性會(huì)成為對(duì)象的屬性
其實(shí)就是保存在對(duì)象中的普通變量
事物的功能會(huì)成為對(duì)象的方法
其實(shí)就是保存在對(duì)象中的普通函數(shù)
對(duì)象的成員
屬性和方法統(tǒng)稱為成員
為什么
便于維護(hù)
何時(shí)
只要使用面向?qū)ο螅家葎?chuàng)建對(duì)象,再按需調(diào)用對(duì)象的方法執(zhí)行操作
如何
3種
創(chuàng)建一個(gè)單獨(dú)的對(duì)象
1. 用對(duì)象直接量
var obj={ 屬性名:屬性值, ... : ... , 方法名: function(){ ... this.屬性名... } }
ES6
var obj={ 屬性名:屬性值, ... : ... , 方法名(){ ... this.屬性名... } }
何時(shí)
在創(chuàng)建對(duì)象時(shí),已知對(duì)象的成員
問題:
對(duì)象自己的方法,訪問自己的屬性,如果不加this,僅會(huì)在作用域鏈中找,不會(huì)在對(duì)象中找
解決:
this.屬性名
今后,只要對(duì)象自己的方法要訪問自己的屬性,必須用this.屬性名
強(qiáng)調(diào):
不帶this.的變量,在作用域鏈中查找
this.屬性在當(dāng)前對(duì)象和當(dāng)前對(duì)象的原型鏈中找
2. 用new
先創(chuàng)建一個(gè)空對(duì)象
var obj=new Object();
{}
簡(jiǎn)寫:
new可省略,()可省略,但不能同時(shí)省略
可推廣到其它內(nèi)置對(duì)象
為新對(duì)象添加新屬性
obj.屬性名=屬性值
obj.方法名=function(){ ... this.屬性名 ... }
何時(shí):
在創(chuàng)建對(duì)象時(shí),暫時(shí)不知道對(duì)象的成員
揭示:
js中一切對(duì)象底層都是關(guān)聯(lián)數(shù)組
相同
obj.屬性名
等效于
obj["屬性名"]
只要訪問對(duì)象的屬性時(shí),屬性名是變化的變量,就只能用[變量]
for in 遍歷每個(gè)成員
for(var key in obj){ key //當(dāng)前屬性名 obj[key] //當(dāng)前屬性值 }
訪問不存在的屬性,不報(bào)錯(cuò),返回undefined
強(qiáng)行給不存在的屬性賦值
不報(bào)錯(cuò)
自動(dòng)添加該屬性
不同
類型不同
API不通用
問題:
反復(fù)創(chuàng)建多個(gè)相同結(jié)構(gòu)的對(duì)象時(shí),會(huì)造成大量重復(fù)的代碼
解決: 用構(gòu)造函數(shù)反復(fù)創(chuàng)建多個(gè)相同結(jié)構(gòu)的對(duì)象
3. 用構(gòu)造函數(shù)
什么是
規(guī)定一類對(duì)象統(tǒng)一結(jié)構(gòu)的函數(shù)
何時(shí)
反復(fù)創(chuàng)建多個(gè)相同結(jié)構(gòu)的對(duì)象
作用:
1. 描述統(tǒng)一的結(jié)構(gòu)
2. 將空對(duì)象構(gòu)建成要求的結(jié)構(gòu)
如何
2步
1. 定義構(gòu)造函數(shù)
function 類型名(屬性參數(shù)){ this.屬性名=屬性參數(shù); /*this.方法名=function(){ ... this.屬性名 ... }*///js中強(qiáng)烈不推薦將方法定義在構(gòu)造函數(shù)中 }
2. 用new調(diào)用構(gòu)造函數(shù)
實(shí)例化一個(gè)對(duì)象
var obj=new 類型名(屬性值)
new
1. 創(chuàng)建一個(gè)新的空對(duì)象
2. 設(shè)置新的子對(duì)象的__proto__繼承構(gòu)造函數(shù)的prototype對(duì)象
3. 調(diào)用構(gòu)造函數(shù),將構(gòu)造函數(shù)中的this自動(dòng)替換為當(dāng)前新對(duì)象
構(gòu)造函數(shù)將規(guī)定的屬性添加到新對(duì)象中,并將傳入的參數(shù)值保存在新對(duì)象的新屬性中
4. 返回新對(duì)象的地址保存到變量中
優(yōu):
代碼重用
問題:
無法節(jié)約內(nèi)存
放在構(gòu)造函數(shù)中的方法定義,每new一次,都會(huì)創(chuàng)建函數(shù)對(duì)象副本
解決:
繼承
this
什么是
自動(dòng)引用正在調(diào)用當(dāng)前方法的.前的對(duì)象
為什么
不用this的普通變量,只能在作用域鏈中查找,無法進(jìn)入對(duì)象中
一般對(duì)象的變量名可能變化,所以不能寫死在對(duì)象的方法內(nèi)部
何時(shí)
只要希望去當(dāng)前對(duì)象中找屬性值時(shí),就必須用this.屬性名
obj.fun()
fun中的this->obj
new Fun()
Fun中的this->正在創(chuàng)建的新對(duì)象
fun()和匿名函數(shù)自調(diào)
this默認(rèn)->window
類型.prototype.fun
fun中的this->將來調(diào)用fun的.前的子對(duì)象
子對(duì)象一定是當(dāng)前類型
如果this不是想要的
fun.call(替換this的對(duì)象)
相當(dāng)于 對(duì)象.fun()
訪問對(duì)象成員
訪問屬性
對(duì)象.屬性名
用法和普通變量完全一樣
特殊:
如果訪問對(duì)象的屬性時(shí),屬性名需要?jiǎng)討B(tài)拼接
只能用obj[屬性名]
調(diào)用方法
對(duì)象.方法名()
用法和普通函數(shù)完全一樣
繼承
什么是
父對(duì)象中的成員,子對(duì)象無需重復(fù)創(chuàng)建即可直接使用
何時(shí):
只要多個(gè)子對(duì)象擁有相同的屬性值或方法時(shí),僅需要集中定義在父對(duì)象中一份,所有子對(duì)象共用即可
為什么
代碼重用,節(jié)約內(nèi)存
如何
js中的繼承都是繼承原型對(duì)象
原型繼承
原型對(duì)象
什么是
集中保存同一類型的所有子對(duì)象共有成員的父對(duì)象
何時(shí)
只要多個(gè)子對(duì)象,擁有相同的成員時(shí),都要將相同的成員集中保存在原型對(duì)象中一份即可
如何
不用創(chuàng)建
買一贈(zèng)一
在定義構(gòu)造函數(shù)同時(shí),已經(jīng)自動(dòng)創(chuàng)建了該類型的原型對(duì)象
構(gòu)造函數(shù).prototype指向原型對(duì)象
原型對(duì)象.constructor指回構(gòu)造函數(shù)
new的第二步
每創(chuàng)建一個(gè)新子對(duì)象,都會(huì)自動(dòng)設(shè)置子對(duì)象的__proto__繼承構(gòu)造函數(shù)的原型對(duì)象
向原型對(duì)象中添加共有成員:
構(gòu)造函數(shù).prototype.成員=值
自有屬性和共有屬性
自有屬性:
直接保存在對(duì)象本地的屬性
共有屬性:
保存在原型對(duì)象中,所有子對(duì)象共有的屬性
操作
讀取
子對(duì)象.屬性
修改
自有屬性
必須用 子對(duì)象.屬性名=值
共有屬性
必須用 構(gòu)造函數(shù).prototype.屬性名=值
判斷
自有
var bool=obj.hasOwnProperty("屬性名")
共有
不是自有
&&
obj.屬性名!==undefined
屬性名 in obj
判斷屬性名是否在obj的原型鏈中
否則,就是沒有
內(nèi)置對(duì)象的原型對(duì)象
內(nèi)置對(duì)象
ES標(biāo)準(zhǔn)中規(guī)定的,瀏覽器廠商已經(jīng)實(shí)現(xiàn)的對(duì)象
11個(gè)
String Number Boolean
包裝類型
專門封裝原始類型的值,并提供操作原始類型值的API
為什么
原始類型的值本身不具有任何功能
何時(shí)
只要試圖對(duì)原始類型的值調(diào)用函數(shù)時(shí),引擎會(huì)自動(dòng)創(chuàng)建對(duì)應(yīng)類型的包裝類型對(duì)象
封裝原始類型的值
調(diào)用包裝類型中的方法操作原始類型的值
比如:
n.toFixed(2)
typeof n
number
new Number(n).toFixed(2)
str.charCodeAt()
typeof str
string
new String(str).charCodeAt();
Array Date Math RegExp
Error
Function Object
Global
全局對(duì)象
在瀏覽器中被window代替
其實(shí)內(nèi)置對(duì)象類型Array,Date...都是構(gòu)造函數(shù)
每種類型都有自己的原型對(duì)象
Array.prototype, Date.prototype, ..
內(nèi)置對(duì)象的原型對(duì)象中保存了所有該類型的子對(duì)象共用的API
固定套路:
解決舊瀏覽器無法使用新API的問題
2步:
判斷
if(typeof 內(nèi)置類型.prototype.API !=="function")
if(!("API" in 內(nèi)置類型.prototype))
添加
內(nèi)置類型.prototype.方法=funciton(參數(shù)){ ... this //獲得將來調(diào)用方法的.前的對(duì)象 }
原型鏈(prototype chain)
由多級(jí)父對(duì)象逐級(jí)繼承形成的鏈?zhǔn)浇Y(jié)構(gòu)
保存了
所有對(duì)象的屬性
控制著
對(duì)象成員的使用順序
先用自有屬性
自己沒有,才延原型鏈向父級(jí)找
原型鏈上沒有,返回undefined
vs 作用域鏈
保存了
局部和全局變量
控制著變量的使用順序
先用局部變量
局部沒有,才去作用域鏈上找
找不到,報(bào)錯(cuò)
總結(jié)
所有不用.的變量都在作用域鏈上找
作用域鏈中的變量不用.,可直接訪問
所有對(duì)象.訪問的屬性都保存在原型鏈上
原型鏈上的屬性必須用"對(duì)象."才能訪問
判斷對(duì)象的類型
0. typeof
只能區(qū)分基礎(chǔ)類型和function
不能進(jìn)一步區(qū)分對(duì)象的類型
1. var bool=類型.prototype.isPrototypeOf(child)
不僅檢查直接父對(duì)象,而且檢查整個(gè)原型鏈
2. var bool=child instanceof 構(gòu)造函數(shù)
不僅檢查直接父對(duì)象,而且檢查整個(gè)原型鏈
問題:
檢查整個(gè)原型鏈
解決: 3. 檢查內(nèi)置class屬性
Object.prototype.toString.call(obj)=="[object 類型名]"
obj.toString()
更嚴(yán)格
class屬性直接保存在對(duì)象本地
只在創(chuàng)建對(duì)象時(shí)確定類型
對(duì)象創(chuàng)建后,不隨繼承關(guān)系的改變而改變
如果檢查數(shù)組類型: 4. var bool=Array.isArray(obj)
ES5
IE9+
原理和Object.prototype.toString.call一樣
鄙視:
方法定義在原型對(duì)象中,還是定義在構(gòu)造函數(shù)對(duì)象上
答
如果方法僅限當(dāng)前類型的子對(duì)象可用,其他類型的對(duì)象不可用,就定義在原型對(duì)象中
必須當(dāng)前類型的子對(duì)象才能調(diào)用
如果方法不確定將來調(diào)用它的對(duì)象類型,就定義在構(gòu)造函數(shù)對(duì)象上
不需要任何對(duì)象實(shí)例,即可用構(gòu)造函數(shù)名直接調(diào)用
多態(tài)
同一個(gè)方法,在不同情況下表現(xiàn)出不同的狀態(tài)
重寫(override)
如果子對(duì)象覺得父對(duì)象的成員不好用,可在本地定義同名自有成員,來覆蓋父對(duì)象中的成員
自定義繼承
1. 僅修改兩個(gè)對(duì)象間的繼承關(guān)系:
獲得子對(duì)象的父對(duì)象
var father=Object.getPrototypeOf(child)
設(shè)置子對(duì)象繼承指定父對(duì)象
Object.setPrototypeOf(child,father)
2. 修改構(gòu)造函數(shù)原型對(duì)象,來修改所有子對(duì)象的父對(duì)象
構(gòu)造函數(shù).prototype=father
時(shí)機(jī):
必須緊跟在構(gòu)造函數(shù)定義之后
開始創(chuàng)建子對(duì)象之前
3. 僅基于現(xiàn)有父對(duì)象,創(chuàng)建子對(duì)象,并擴(kuò)展自有屬性: Object.create()
創(chuàng)建新子對(duì)象,繼承父對(duì)象,擴(kuò)展子對(duì)象自有屬性
var child=Object.create(father,{ 屬性名:{四大特性}, ... : ... })
鄙視: 模擬實(shí)現(xiàn)Object.create()
Object.create=function(father,props){ //var child=new Object(); //Object.setPrototypeOf(child,father); var Fun=function(){}; Fun.prototype=father; var child=new Fun(); //Object.defineProperties(child,props); if(props!==undefined){ for(var key in props){ child[key]=props[key].value; } } return child; }
4. 兩種類型間的繼承
何時(shí):
如果發(fā)現(xiàn)多個(gè)類型擁有部分相同的屬性結(jié)構(gòu)和方法定義
都要抽象父類型
如何
2步:
1. 定義抽象父類型
相同的屬性結(jié)構(gòu)定義在父類型的構(gòu)造函數(shù)中
相同的方法定義在父類型的原型對(duì)象中
2. 讓子類型繼承父類型
1. 在子類型構(gòu)造函數(shù)中借用父類型構(gòu)造
extends
讓父類型構(gòu)造函數(shù)幫助添加相同部分的屬性定義
子類型構(gòu)造函數(shù)僅負(fù)責(zé)添加獨(dú)有的屬性定義即可
錯(cuò)誤:
直接調(diào)用父類型構(gòu)造函數(shù)
this->window
父類型中的屬性都泄露到全局
正確
父類型構(gòu)造.call(this, 參數(shù)1,參數(shù)2,...)
簡(jiǎn)寫:
父類型構(gòu)造.apply(this, arguments)
鄙視:
call vs apply
相同:
都是強(qiáng)行借用一個(gè)本來無法調(diào)用的函數(shù),并臨時(shí)替換函數(shù)中this為指定對(duì)象
不同:
call
傳入借用函數(shù)的參數(shù),必須單獨(dú)傳入,逗號(hào)分隔
apply
傳入借用函數(shù)的參數(shù),放在一個(gè)數(shù)組中整體傳入
可自動(dòng)打散數(shù)組類型參數(shù)
2. 讓子類型原型對(duì)象繼承父類型原型對(duì)象
inherits
Object.setPrototypeOf( 子類型構(gòu)造.prototype, 父類型構(gòu)造.prototype )
ES5
對(duì)對(duì)象的保護(hù)
對(duì)象的屬性
命名屬性
可直接用.訪問到的屬性
分為
數(shù)據(jù)屬性
實(shí)際存儲(chǔ)屬性值的屬性
四大特性
{ value: 屬性值, writable: true/false, //控制是否可修改 enumerable: true/false, //控制是否可被for in遍歷 configurable: true/false, //1. 控制是否可刪除 //2. 控制是否可修改前兩個(gè)特性 }
強(qiáng)調(diào)
configurable一旦改為false,不可逆
訪問器屬性
不實(shí)際存儲(chǔ)數(shù)據(jù),專門提供對(duì)其它數(shù)據(jù)/變量的保護(hù)
何時(shí)
1. 用自定義規(guī)則保護(hù)屬性時(shí)
比如
age屬性必須介于18~65之間
2. 為對(duì)象添加虛擬屬性
fullName
獲取時(shí)
firstName+lastName
賦值時(shí)
將fullName按空格分隔,一半給firstName,另一半給lastName
2步:
1. 定義一個(gè)隱藏的數(shù)據(jù)屬性實(shí)際存儲(chǔ)屬性值
2. 定義訪問器屬性專門讀寫隱藏的數(shù)據(jù)屬性
四大特性
{ get:function(){return this.隱藏屬性;}, set:function(val){ //如果val符合條件 this.隱藏屬性=val //否則 報(bào)錯(cuò) }, enumerable, configurable }
試圖用訪問器屬性讀取受保護(hù)的值時(shí),自動(dòng)調(diào)用get方法
試圖用訪問器屬性修改受保護(hù)的值時(shí),自動(dòng)調(diào)用set方法,參數(shù)val可自動(dòng)獲得要賦的新值
如果省略set方法,則該訪問器屬性相當(dāng)于只讀
獲取一個(gè)屬性的四大特性
var attrs=Object.getOwnPropertyDescriptor(obj,"屬性名")
attrs: {四大特性}
修改四大特性
只改一個(gè)屬性的四大特性
Object.defineProperty(obj,"屬性名",{ 特性名:值, ... : ... })
同時(shí)修改多個(gè)屬性的四大特性
Object.defineProperties(obj,{ 屬性名1: { 特性名:值, ... : ... }, 屬性名2: {四大特性} })
強(qiáng)調(diào):
雙保險(xiǎn)
修改writable或enumerable時(shí),最好同時(shí)定義configurable為false,禁止反向修改
要修改的屬性不存在,會(huì)自動(dòng)添加該屬性
特性的默認(rèn)值
用.添加的新屬性
特性的默認(rèn)值為true
defineProperty/defineProperties添加的新屬性
特性的默認(rèn)值為false
內(nèi)部屬性
不能用.訪問到的屬性
__proto__
Object.getPrototypeOf(obj)
Object.setPrototypeOf(child,father)
class
Object.prototype.toString.call(obj)
extensible:true
var bool=Object.isExtensible(obj)
Object.preventExtensions(obj)
防篡改
1. 防擴(kuò)展
禁止添加新屬性
相當(dāng)于
將對(duì)象的extensible:false
判斷是否已禁止擴(kuò)展
Object.isExtensible(obj)
設(shè)置防擴(kuò)展
Object.preventExtensions(obj)
2. 密封
在防擴(kuò)展同時(shí),禁止刪除現(xiàn)有屬性
相當(dāng)于
將每個(gè)屬性的configurable:false
其他屬性在修改特性時(shí),不必反復(fù)修改configurable:false
判斷是否已密封
Object.isSealed(obj)
密封對(duì)象
Object.seal(obj)
最像Java的構(gòu)造函數(shù)
function Emp(id,ename,salary,age){ this.id=id; this.ename=ename; this.salary=salary; Object.defineProperties(this,{ id:{writable:false}, salary:{enumerable:false}, _age:{ writable:true, enumerable:false }, age:{ get:function(){return this._age;}, set:function(val){ if(val<18||val>65) throw new Error(...) this._age=val; }, enumerable:true } }); this.age=age; //防篡改: Object.seal(this);//密封 }
鄙視:
實(shí)現(xiàn)一個(gè)js類型,包含public屬性和private屬性
3. 凍結(jié)
在密封同時(shí),禁止修改所有屬性的值
相當(dāng)于
將每個(gè)屬性的writable:false
判斷是否被凍結(jié)
Object.isFrozen(obj);
凍結(jié)對(duì)象
Object.freeze(obj)
何時(shí):
如果一個(gè)對(duì)象中所有屬性值都不允許擅自修改
call/apply/bind()
何時(shí):
只要函數(shù)中的this不是想要的,就可用call/apply/bind替換
call和apply
立刻調(diào)用函數(shù)執(zhí)行
同時(shí)臨時(shí)替換函數(shù)中的this
何時(shí)
如果立刻執(zhí)行,且臨時(shí)替換this
如何
fun.call(obj, 參數(shù)值列表)
調(diào)用fun
替換fun中的this為obj
將參數(shù)值列表傳遞給fun
fun.apply(obj, 參數(shù)值數(shù)組)
vs call
apply要求傳入fun的參數(shù)必須放在數(shù)組中整體傳入
apply可自動(dòng)將數(shù)組打散為單個(gè)參數(shù)值分別傳入
bind
基于一個(gè)現(xiàn)有函數(shù),創(chuàng)建一個(gè)新函數(shù),并永久綁定this和部分參數(shù)
何時(shí):
只要替換回調(diào)函數(shù)中的this時(shí)
如何:
var newFun=fun.bind(obj, 參數(shù)值列表 )
創(chuàng)建一個(gè)和fun功能完全一樣的新函數(shù)
永久綁定新函數(shù)中的this為obj
永久綁定新函數(shù)中的部分參數(shù)為參數(shù)值列表中的值
強(qiáng)調(diào):
被bind創(chuàng)建的函數(shù)中的this和綁定的變量,任何情況下不能再被call替換
數(shù)組API
判斷
判斷arr中每個(gè)元素是否都符合條件
arr.every(function(val,i,arr){ //val: 當(dāng)前元素值 //i : 當(dāng)前位置 //arr : 當(dāng)前數(shù)組 return 判斷條件; })
只判斷arr中是否包含符合條件的元素
arr.some(function(val,i,arr){ return 判斷條件; })
遍歷
對(duì)數(shù)組中每個(gè)元素執(zhí)行相同的操作
直接修改原數(shù)組
arr.forEach(function(val,i,arr){ arr[i]=新值; })
IE8
Array.prototype.forEach=function(callback){ for(var i=0;i<this.length;i++){ if(this[i]!==undefined)//防稀疏數(shù)組 callback(this[i],i,this); } }
不修改原數(shù)組,返回新數(shù)組
var newArr=arr.map(function(val,i,arr){ return 修改后的新值 })
IE8
Array.prototype.map=function(callback){ for(var i=0,result=[];i<this.length;i++){ if(this[i]!==undefined)//防稀疏數(shù)組 result[i]=callback(this[i],i,this); } return result; }
其實(shí)map也可像forEach一樣使用
回調(diào)函數(shù)中的this,指window
過濾和匯總
過濾
選取arr中符合條件的元素組成新的子數(shù)組
var subArr=arr.filter(function(val,i,arr){ return 判斷條件 })
匯總
將數(shù)組中每個(gè)元素值匯總出一個(gè)結(jié)果
var r=arr.reduce(function(prev,val,i,arr){ return prev+val; }, 初始值)
嚴(yán)格模式
何時(shí):
1. 新項(xiàng)目,都要啟用嚴(yán)格模式
2. 舊項(xiàng)目,逐個(gè)模塊向嚴(yán)格模式遷移
如何
2個(gè)范圍
整個(gè)代碼塊或js文件啟用嚴(yán)格模式
在代碼塊或js文件的開頭插入: "use strict";
僅在一個(gè)函數(shù)內(nèi)啟用嚴(yán)格模式
在function內(nèi),函數(shù)體頂部插入: "use strict";
要求:
1. 禁止給未聲明的變量賦值
2. 靜默失敗升級(jí)為錯(cuò)誤
3. 普通函數(shù)調(diào)用或匿名函數(shù)自調(diào)中的this不再默認(rèn)指向window,而是undefined
4. arguments, arguments.callee不推薦使用
ES6
模板字符串
簡(jiǎn)化
字符串拼接
反引號(hào)包裹的字符串`xxx`
支持換行
支持動(dòng)態(tài)生成內(nèi)容
在``中使用${js表達(dá)式}
${}中可以放任何有返回值的js表達(dá)式,比如:變量, 運(yùn)算, 三目, 函數(shù)調(diào)用, 創(chuàng)建對(duì)象, 訪問數(shù)組下標(biāo)
${}中不能寫程序結(jié)構(gòu): if else if else for while do while switch case
模板字符串會(huì)自動(dòng)執(zhí)行js表達(dá)式的結(jié)果,并拼接到最終生成的普通字符串中
let
什么是
專門代替var用于聲明變量
var的缺點(diǎn)
1. 聲明提前, 打亂程序正常的執(zhí)行順序
2. 沒有塊級(jí)作用域,導(dǎo)致塊內(nèi)的變量會(huì)被提前到塊外,影響外部的程序
let的優(yōu)點(diǎn)
1. 為js添加了塊級(jí)作用域
避免了塊內(nèi)的變量污染外部
2. 避免了聲明提前
在同一作用域內(nèi),let前不允許提前使用未聲明的同名變量
let的小脾氣
相同作用域內(nèi)不允許同時(shí)let兩個(gè)同名的變量
let 前不允許提前使用該同名變量
即使在全局let的變量,也不會(huì)保存在window中
原理/替代辦法:
let其實(shí)是匿名函數(shù)自調(diào),且修改變量名避免沖突
箭頭函數(shù)
簡(jiǎn)化:
回調(diào)函數(shù)
如何
去function,在()和{}之間加=>
更簡(jiǎn)化:
1. 如果只有一個(gè)參數(shù),可省略()
但是,如果沒有參數(shù),必須保留空()
2. 如果函數(shù)體只有一句話,可省略{}和結(jié)尾的分號(hào)
更簡(jiǎn)化
且一句話還是return
則必須省略return
箭頭函數(shù)特征
內(nèi)外this相同(通)
只要不希望內(nèi)外的this相同時(shí),就不能用箭頭函數(shù)簡(jiǎn)化
比如: 對(duì)象中的方法
何時(shí)
幾乎所有回調(diào)函數(shù)都可用箭頭函數(shù)簡(jiǎn)化
反例
如果回調(diào)函數(shù)中的this和外部的this不相同時(shí),不能簡(jiǎn)化
//this->window elem.addEventListener("click",function(){ //this->當(dāng)前elem })
如果用箭頭函數(shù)簡(jiǎn)化,結(jié)果內(nèi)外this都是window
其實(shí):
//this->window elem.addEventListener("click",e=>{ //this->window //e.target->當(dāng)前elem })
for of
簡(jiǎn)化for循環(huán):
遍歷索引數(shù)組和類數(shù)組對(duì)象
如何
of是依次取出數(shù)組中每個(gè)元素值,保存到of前的變量中
for(var val of arr){ val//當(dāng)前元素值 }
問題:
1. 不支持自定義下標(biāo)或?qū)傩悦年P(guān)聯(lián)數(shù)組和對(duì)象
2. 只能讀取元素值,不能修改元素值, 不能獲得元素的位置
按值傳遞
3. 只能連續(xù)遍歷所有,不能控制遍歷的順序和步調(diào)
Subtopic
參數(shù)增強(qiáng)
默認(rèn)值
什么是
ES6中可給函數(shù)定義中形參列表的最后一個(gè)參數(shù)定義默認(rèn)值
何時(shí)
如果函數(shù)只有最后一個(gè)形參不確定是否給值時(shí)
如何
function 函數(shù)名(形參1,形參2,...,形參n=默認(rèn)值)
如果調(diào)用函數(shù)時(shí),沒有提供最后一個(gè)實(shí)參,就自動(dòng)用默認(rèn)值代替
替代
fromi=fromi||0
如果形參變量fromi的值有意義(可以轉(zhuǎn)為true),就使用fromi的值
如果形參變量fromi沒有傳值,或傳的值沒有意義(會(huì)轉(zhuǎn)為false),就使用||后的0作為備胎
剩余參數(shù)
什么是
代替arguments處理不確定參數(shù)個(gè)數(shù)的情況
為什么
arguments的兩個(gè)問題
1. 不是數(shù)組類型,不能用數(shù)組的API
2. 只能全部獲得實(shí)參值,不能有所選擇
3. 名字太長(zhǎng),且沒有意義
4. 箭頭函數(shù)不支持arguments
何時(shí)
當(dāng)多個(gè)形參不確定給不給值時(shí),但不要求實(shí)參與形參對(duì)應(yīng)時(shí)
如何
定義函數(shù)時(shí)
function 函數(shù)名(形參1,形參2,...數(shù)組名){ //數(shù)組會(huì)接住除之前形參1和形參2之外所有剩余的實(shí)參值 //保存剩余實(shí)參值的結(jié)構(gòu)是一個(gè)純正的數(shù)組,可用數(shù)組家全部API }
優(yōu)點(diǎn)
1. 是純正的數(shù)組類型,可用數(shù)組家全套API
2. 可獲得部分實(shí)參值,不會(huì)與之前的形參變量爭(zhēng)搶
3. 箭頭函數(shù)只支持...rest語法
4. 數(shù)組名可自定義,簡(jiǎn)單
替代
獲得所有實(shí)參值,并轉(zhuǎn)為數(shù)組
var arr=[].slice.call(arguments)
將類數(shù)組對(duì)象轉(zhuǎn)為數(shù)組的通用方法
獲得部分實(shí)現(xiàn)值,并轉(zhuǎn)為數(shù)組
var arr=[].slice.call(arguments, 開始截取的位置)
打散數(shù)組
什么是
代替apply,將數(shù)組打散為單個(gè)值,傳入函數(shù)中
為什么
apply的本職工作是替換this,順便打散數(shù)組
如果用apply單純打散數(shù)組,必須給定一個(gè)無意義的對(duì)象作為第一個(gè)參數(shù)
比如: 查找數(shù)組中的最大值:
Math.max.apply(null,arr)
何時(shí)
當(dāng)一個(gè)函數(shù)需要多個(gè)參數(shù)值,但是給定的實(shí)參值卻是放在一個(gè)數(shù)組中
如何
調(diào)用函數(shù)時(shí)
函數(shù)名(...數(shù)組)
先將數(shù)組打散為單個(gè)值,再將單個(gè)值傳入函數(shù)中
替代
函數(shù).apply(任意對(duì)象, 數(shù)組)
如果該函數(shù)和this無關(guān),則任意對(duì)象可隨便寫任何符合語法的值或?qū)ο蠖夹校瑳]有任何影響
固定套路
合并多個(gè)數(shù)組
var arr3=[].concat(arr1, arr2)
var arr3=[...arr1, ...arr2]
其實(shí)...不但可以打散數(shù)組,還可以打散對(duì)象
克隆一個(gè)對(duì)象
var obj2={...obj1}
合并多個(gè)對(duì)象
var obj3={...obj1, ...obj2}
數(shù)組降維
二維數(shù)組降維
arr=[].concat(...arr)
多維數(shù)組降維
function fun(arr){ //先降一次維 arr=[].concat(...arr); //再檢查降維后的數(shù)組中是否還包含子數(shù)組 var hasArray=arr.some(function(elem){ return Array.isArray(elem); }) if(hasArray){ //如果包含子數(shù)組 arr=fun(arr);//就只能再降維一次,直到檢查不再包含子數(shù)組為止 } return arr }
解構(gòu)
什么是:
提取出一個(gè)大對(duì)象或數(shù)組中的個(gè)別成員,單獨(dú)使用
何時(shí)
只要僅使用大對(duì)象或數(shù)組中的少數(shù)部分成員時(shí),都用解構(gòu)
如何
3種
1. 數(shù)組解構(gòu)
下標(biāo)對(duì)下標(biāo)
什么是
提取出索引數(shù)組中指定位置的元素,保存在單獨(dú)的變量中獨(dú)立使用
如何
1. 在等號(hào)左邊先將要接受數(shù)據(jù)的變量,裝扮成和等號(hào)右邊數(shù)組相同的結(jié)構(gòu)
2. 執(zhí)行時(shí),等號(hào)右邊的數(shù)組會(huì)將相同位置的元素值,賦值給等號(hào)左邊相同位置的變量
var date=[2019,5,6]; 0 1 2 0 1 2 var [y,m,d]=date; y=2019, m=5, d=6
固定套路
不聲明第三個(gè)變量,交換兩個(gè)變量中的值
共有幾種
a+=b, b=a-b, a-=b
只能交換數(shù)字值
a^=b, b^=a, a^=b
只能交換數(shù)字值
a=[b,b=a][0]
Subtopic
因?yàn)闆]有算術(shù)計(jì)算,所以可以交換任意類型的值
[a,b]=[b,a]
因?yàn)闆]有算術(shù)計(jì)算,所以可以交換任意類型的值
2. 對(duì)象解構(gòu)
屬性名對(duì)屬性名
什么是
僅提取出大對(duì)象中的個(gè)別成員屬性或方法,保存在變量中,單獨(dú)使用
如何
1. 先將等號(hào)左邊的變量裝扮成和要解構(gòu)的對(duì)象相同的屬性結(jié)構(gòu)
2. 在賦值時(shí),等號(hào)右邊的對(duì)象就會(huì)將相同屬性名的屬性值,賦值給等號(hào)右邊的相同屬性名對(duì)應(yīng)的變量
var lilei={ sname: "Li Lei", sage: 11, signin:function(){ 登錄... }, signout:function(){ 注銷... } }
var { sname:sname, signin:signin }=lilei
結(jié)果:
變量sname="Li Lei"
變量signin=function(){ 登錄... }
簡(jiǎn)寫
如果屬性名和變量名相同,只寫一個(gè)即可
var {sname, signin}=lilei
一個(gè)名稱,即當(dāng)屬性名配對(duì)用,又當(dāng)變量名將來單獨(dú)反復(fù)使用
3. 參數(shù)解構(gòu)
什么是
將所有參數(shù)都定義在一個(gè)對(duì)象結(jié)構(gòu)中,調(diào)用函數(shù)傳參時(shí),也是講所有實(shí)參值都放在一個(gè)對(duì)象中傳入
何時(shí)
多個(gè)形參不確定給不給時(shí),且要求實(shí)參值必須傳給指定的形參變量
如何
2步
1. 在定義函數(shù)時(shí),將所有形參放在一個(gè)對(duì)象結(jié)構(gòu)中
function ajax({ url:url, type:type, data:data, dataType:dataType }){ ... ... }
簡(jiǎn)寫為
function ajax({url,type,data,dataType}){ ... ... }
結(jié)果:
函數(shù)中
形參url獲得實(shí)參url的值"http://localhost:3000/users/signup"
形參type獲得實(shí)參type的值"post"
形參data獲得實(shí)參data的值"uname=dingding&upwd=123456"
形參dataType獲得實(shí)參dataType的值"json"
2. 在調(diào)用函數(shù)時(shí),所有實(shí)參也必須放在相同結(jié)構(gòu)的對(duì)象中
ajax({ url:"http://localhost:3000/users/signup", type:"post", data:"uname=dingding&upwd=123456", dataType:"json" })
另見:
優(yōu)點(diǎn)
1. 無論有多少參數(shù)不確定,都不會(huì)報(bào)語法錯(cuò)誤
如果解構(gòu)時(shí),實(shí)參列表中缺少參數(shù),則函數(shù)中對(duì)應(yīng)的形參變量獲得undefined
2. 實(shí)參列表和形參列表只要名稱對(duì)應(yīng)即可,沒有必然順序
參數(shù)解構(gòu)其實(shí)就是對(duì)象解構(gòu)在函數(shù)調(diào)用時(shí)的應(yīng)用而已
class
簡(jiǎn)化:
面向?qū)ο?/p>
封裝,繼承,多態(tài)
如何
封裝
對(duì)象{}
如果一個(gè)屬性的值來自于外部的變量,且屬性名剛好等于變量名,則{}中只寫一個(gè)名字即可
對(duì)象中的方法不能簡(jiǎn)寫為箭頭函數(shù),但可去掉":function"
如果連屬性名都需要?jiǎng)討B(tài)拼接生成
應(yīng)該將拼接字符串的表達(dá)式,放在[]中
class
如何
1. 用"class 類型名{}"包裹原來的構(gòu)造函數(shù)和原型對(duì)象方法
2. 構(gòu)造函數(shù)名提升為類型名,放在class關(guān)鍵詞之后,修改構(gòu)造函數(shù)的"function 函數(shù)名"為"constructor"
3. 原型對(duì)象方法:
可省略開頭的"類型.prototype"
可省略方法名后的"=function"
所有直接保存在class中的方法,默認(rèn)就是保存在原型對(duì)象中
比如:
舊
function Student(sname,sage){ this.sname=sname; this.sage=sage; } Student.prototype.intr=function(){ console.log(`I'm ${this.sname},I'm ${this.sage}`); }
新
class Student{ constructor(sname,sage){ this.sname=sname; this.sage=sage; } intr(){ console.log(`I'm ${this.sname},I'm ${this.sage}`); } }
4. 添加訪問器屬性:
在構(gòu)造函數(shù)的平級(jí)
get 訪問器屬性名(){ return this.受保護(hù)的其他屬性 }
set 訪問器屬性名(val){ if(條件) this.受保護(hù)的屬性=val else 報(bào)錯(cuò) }
繼承
兩種類型間的繼承
一個(gè)class繼承另一個(gè)class的屬性結(jié)構(gòu)和方法定義
何時(shí)
只要發(fā)現(xiàn)多個(gè)子class之間有部分相同的屬性結(jié)構(gòu)和方法定義時(shí)
如何
1. 定義父類型,集中存儲(chǔ)相同部分的屬性結(jié)構(gòu)和方法
父類型的構(gòu)造函數(shù)中包含相同部分的屬性結(jié)構(gòu)
父類型class內(nèi)包含相同的方法定義
2. 讓子class,繼承父class
1. 在"class 類型名"后添加" extends 父類型"
0. 不用再設(shè)置Object.setPrototypeOf
2. 在構(gòu)造函數(shù)中借用父類型構(gòu)造函數(shù)
不用call/apply,不用加this
super(屬性值列表)
沒有繼承的兩個(gè)class
Subtopic
使用繼承優(yōu)化后,變成3個(gè)class
Subtopic
Promise
為什么
避免callback hell
何時(shí)
只要要求多個(gè)原本異步執(zhí)行的函數(shù),必須連續(xù)順序執(zhí)行
舊js中
用回調(diào)函數(shù)
1. 在前一個(gè)任務(wù)的形參列表結(jié)尾添加一個(gè)callback參數(shù)
在前一個(gè)任務(wù)內(nèi)部,之后一句話執(zhí)行完之后,自動(dòng)調(diào)用傳入的回調(diào)函數(shù)callback
function liang(next){ console.log("亮起跑...") setTimeout(function(){ console.log("亮到達(dá)終點(diǎn)!"); //亮到達(dá)終點(diǎn)后,自動(dòng)調(diào)用下一項(xiàng)任務(wù)開始執(zhí)行 next(); },6000); }
2. 在調(diào)用前一個(gè)任務(wù)時(shí),就要將下一個(gè)任務(wù)函數(shù)對(duì)象,以參數(shù)形式提前交給前一個(gè)任務(wù)的callback形參保存起來備用
liang( //liang的next function(){ ran() } )
結(jié)果:
function(){ ran() }給了liang的形參next
當(dāng)liang執(zhí)行完,在內(nèi)部自動(dòng)調(diào)用next(),等效于調(diào)用下一個(gè)function
問題: 如果需要連續(xù)執(zhí)行的異步任務(wù)很多,就會(huì)造成很深的嵌套
稱為回調(diào)地獄
如何
2步
定義時(shí)
前一個(gè)函數(shù)
不用在參數(shù)中添加callback
在函數(shù)內(nèi)返回Promise對(duì)象
function 前一項(xiàng)任務(wù)(){ return new Promise((resolve, reject)=>{ 如果正確,就繼續(xù)調(diào)用resolve() 否則,就調(diào)用reject("錯(cuò)誤提示") }) }
Promise的參數(shù)中是一個(gè)函數(shù)
函數(shù)封裝了原函數(shù)正常的邏輯
函數(shù)由兩個(gè)回調(diào)函數(shù)參數(shù)
resolve
在正常執(zhí)行后,調(diào)用resolve()
就會(huì)自動(dòng)執(zhí)行.then()
reject
在出錯(cuò)時(shí),調(diào)用reject()
就會(huì)自動(dòng)執(zhí)行.catch()
連續(xù)調(diào)用兩個(gè)函數(shù)時(shí)
前一個(gè)函數(shù)() //return new Promise() .then(下一個(gè)函數(shù)) .catch((err)=>{錯(cuò)誤處理函數(shù)})
下一個(gè)函數(shù)后不要加()
ES7
(async function(){ try{ var result=await 異步調(diào)用1() await 異步調(diào)用2() 其它主程序代碼 }catch(err){ 錯(cuò)誤處理... } })()
必需整個(gè)過程包裹在一個(gè) (async function(){ })() 內(nèi)
await是等待
所有promise中寫then的地方,都可以用await代替
await會(huì)掛起當(dāng)前程序,等待當(dāng)前異步任務(wù)執(zhí)行完,再開始執(zhí)行當(dāng)前程序之后的代碼
Promise的三個(gè)狀態(tài)
當(dāng)首次調(diào)用Promise的函數(shù)時(shí),整個(gè)Promise對(duì)象處于pending狀態(tài)
當(dāng)前一個(gè)任務(wù)執(zhí)行完,自動(dòng)調(diào)用resolve()函數(shù)后,整個(gè)Promise的狀態(tài)就變成resolved
就會(huì)自動(dòng)觸發(fā)then()執(zhí)行
當(dāng)任務(wù)執(zhí)行過程中出錯(cuò),自動(dòng)調(diào)用reject()函數(shù)后,整個(gè)Promise的狀態(tài)就變?yōu)閞ejected
就會(huì)自動(dòng)執(zhí)行catch()
Subtopic
多個(gè)函數(shù)需要連續(xù)調(diào)用,每個(gè)函數(shù),都要返回Promise對(duì)象,才能連續(xù)用then調(diào)用
如果需要等待多個(gè)函數(shù)執(zhí)行完,才執(zhí)行:
Promise.all([ 函數(shù)1(), 函數(shù)2(), ... ... , ]).then(function(result){ ... })
result
.then中下一項(xiàng)任務(wù)參數(shù)result會(huì)接住一個(gè)數(shù)組,其中保存
[ 函數(shù)1的返回值, 函數(shù)2的返回值, ...]
和調(diào)用結(jié)束的先后順序無關(guān),而始終和調(diào)用的順序一致!
能每一個(gè)前端工程師都想要理解瀏覽器的工作原理。
我們希望知道從在瀏覽器地址欄中輸入 url 到頁面展現(xiàn)的短短幾秒內(nèi)瀏覽器究竟做了什么;
我們希望了解平時(shí)常常聽說的各種代碼優(yōu)化方案是究竟為什么能起到優(yōu)化的作用;
我們希望更細(xì)化的了解瀏覽器的渲染流程。
瀏覽器的多進(jìn)程架構(gòu)
一個(gè)好的程序常常被劃分為幾個(gè)相互獨(dú)立又彼此配合的模塊,瀏覽器也是如此,以 Chrome 為例,它由多個(gè)進(jìn)程組成,每個(gè)進(jìn)程都有自己核心的職責(zé),它們相互配合完成瀏覽器的整體功能,每個(gè)進(jìn)程中又包含多個(gè)線程,一個(gè)進(jìn)程內(nèi)的多個(gè)線程也會(huì)協(xié)同工作,配合完成所在進(jìn)程的職責(zé)。
對(duì)一些前端開發(fā)同學(xué)來說,進(jìn)程和線程的概念可能會(huì)有些模糊,為了更好的理解瀏覽器的多進(jìn)程架構(gòu),這里我們簡(jiǎn)單討論一下進(jìn)程和線程。
進(jìn)程(process)和線程(thread)
進(jìn)程就像是一個(gè)有邊界的生產(chǎn)廠間,而線程就像是廠間內(nèi)的一個(gè)個(gè)員工,可以自己做自己的事情,也可以相互配合做同一件事情。
當(dāng)我們啟動(dòng)一個(gè)應(yīng)用,計(jì)算機(jī)會(huì)創(chuàng)建一個(gè)進(jìn)程,操作系統(tǒng)會(huì)為進(jìn)程分配一部分內(nèi)存,應(yīng)用的所有狀態(tài)都會(huì)保存在這塊內(nèi)存中,應(yīng)用也許還會(huì)創(chuàng)建多個(gè)線程來輔助工作,這些線程可以共享這部分內(nèi)存中的數(shù)據(jù)。如果應(yīng)用關(guān)閉,進(jìn)程會(huì)被終結(jié),操作系統(tǒng)會(huì)釋放相關(guān)內(nèi)存。更生動(dòng)的示意圖如下:
一個(gè)進(jìn)程還可以要求操作系統(tǒng)生成另一個(gè)進(jìn)程來執(zhí)行不同的任務(wù),系統(tǒng)會(huì)為新的進(jìn)程分配獨(dú)立的內(nèi)存,兩個(gè)進(jìn)程之間可以使用 IPC (Inter Process Communication)進(jìn)行通信。很多應(yīng)用都會(huì)采用這樣的設(shè)計(jì),如果一個(gè)工作進(jìn)程反應(yīng)遲鈍,重啟這個(gè)進(jìn)程不會(huì)影響應(yīng)用其它進(jìn)程的工作。
如果對(duì)進(jìn)程及線程的理解還存在疑惑,可以參考下述文章:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
瀏覽器的架構(gòu)
有了上面的知識(shí)做鋪墊,我們可以更合理的討論瀏覽器的架構(gòu)了,其實(shí)如果要開發(fā)一個(gè)瀏覽器,它可以是單進(jìn)程多線程的應(yīng)用,也可以是使用 IPC 通信的多進(jìn)程應(yīng)用。
不同瀏覽器的架構(gòu)模型
不同瀏覽器采用了不同的架構(gòu)模式,這里并不存在標(biāo)準(zhǔn),本文以 Chrome 為例進(jìn)行說明 :
Chrome 采用多進(jìn)程架構(gòu),其頂層存在一個(gè) Browser process 用以協(xié)調(diào)瀏覽器的其它進(jìn)程。
Chrome 的不同進(jìn)程
具體說來,Chrome 的主要進(jìn)程及其職責(zé)如下:
Browser Process:
Renderer Process:
Plugin Process:
不同進(jìn)程負(fù)責(zé)的瀏覽器區(qū)域示意圖
Chrome 還為我們提供了「任務(wù)管理器」,供我們方便的查看當(dāng)前瀏覽器中運(yùn)行的所有進(jìn)程及每個(gè)進(jìn)程占用的系統(tǒng)資源,右鍵單擊還可以查看更多類別信息。
通過「頁面右上角的三個(gè)點(diǎn)點(diǎn)點(diǎn) — 更多工具 — 任務(wù)管理器」即可打開相關(guān)面板。
Chrome 多進(jìn)程架構(gòu)的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
某一渲染進(jìn)程出問題不會(huì)影響其他進(jìn)程
更為安全,在系統(tǒng)層面上限定了不同進(jìn)程的權(quán)限
缺點(diǎn)
由于不同進(jìn)程間的內(nèi)存不共享,不同進(jìn)程的內(nèi)存常常需要包含相同的內(nèi)容。
為了節(jié)省內(nèi)存,Chrome 限制了最多的進(jìn)程數(shù),最大進(jìn)程數(shù)量由設(shè)備的內(nèi)存和 CPU 能力決定,當(dāng)達(dá)到這一限制時(shí),新打開的 Tab 會(huì)共用之前同一個(gè)站點(diǎn)的渲染進(jìn)程。
測(cè)試了一下在 Chrome 中打開不斷打開知乎首頁,在 Mac i5 8g 上可以啟動(dòng)四十多個(gè)渲染進(jìn)程,之后新打開 tab 會(huì)合并到已有的渲染進(jìn)程中。
Chrome 把瀏覽器不同程序的功能看做服務(wù),這些服務(wù)可以方便的分割為不同的進(jìn)程或者合并為一個(gè)進(jìn)程。以 Broswer Process 為例,如果 Chrome 運(yùn)行在強(qiáng)大的硬件上,它會(huì)分割不同的服務(wù)到不同的進(jìn)程,這樣 Chrome 整體的運(yùn)行會(huì)更加穩(wěn)定,但是如果 Chrome 運(yùn)行在資源貧瘠的設(shè)備上,這些服務(wù)又會(huì)合并到同一個(gè)進(jìn)程中運(yùn)行,這樣可以節(jié)省內(nèi)存,示意圖如下。
iframe 的渲染 – Site Isolation
在上面的進(jìn)程圖中我們還可以看到一些進(jìn)程下還存在著 Subframe,這就是 Site Isolation 機(jī)制作用的結(jié)果。
Site Isolation 機(jī)制從 Chrome 67 開始默認(rèn)啟用。這種機(jī)制允許在同一個(gè) Tab 下的跨站 iframe 使用單獨(dú)的進(jìn)程來渲染,這樣會(huì)更為安全。
iframe 會(huì)采用不同的渲染進(jìn)程
Site Isolation 被大家看做里程碑式的功能, 其成功實(shí)現(xiàn)是多年工程努力的結(jié)果。Site Isolation 不是簡(jiǎn)單的疊加多個(gè)進(jìn)程。這種機(jī)制在底層改變了 iframe 之間通信的方法,Chrome 的其它功能都需要做對(duì)應(yīng)的調(diào)整,比如說 devtools 需要相應(yīng)的支持,甚至 Ctrl + F 也需要支持。關(guān)于 Site Isolation 的更多內(nèi)容可參考下述鏈接:
https://developers.google.com/web/updates/2018/07/site-isolation
介紹完了瀏覽器的基本架構(gòu)模式,接下來我們看看一個(gè)常見的導(dǎo)航過程對(duì)瀏覽器來說究竟發(fā)生了什么。
導(dǎo)航過程發(fā)生了什么
也許大多數(shù)人使用 Chrome 最多的場(chǎng)景就是在地址欄輸入關(guān)鍵字進(jìn)行搜索或者輸入地址導(dǎo)航到某個(gè)網(wǎng)站,我們來看看瀏覽器是怎么看待這個(gè)過程的。
我們知道瀏覽器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又對(duì)這些工作進(jìn)一步劃分,使用不同線程進(jìn)行處理:
瀏覽器主進(jìn)程中的不同線程
回到我們的問題,當(dāng)我們?cè)跒g覽器地址欄中輸入文字,并點(diǎn)擊回車獲得頁面內(nèi)容的過程在瀏覽器看來可以分為以下幾步:
1. 處理輸入
UI thread 需要判斷用戶輸入的是 URL 還是 query;
2. 開始導(dǎo)航
當(dāng)用戶點(diǎn)擊回車鍵,UI thread 通知 network thread 獲取網(wǎng)頁內(nèi)容,并控制 tab 上的 spinner 展現(xiàn),表示正在加載中。
network thread 會(huì)執(zhí)行 DNS 查詢,隨后為請(qǐng)求建立 TLS 連接。
UI thread 通知 Network thread 加載相關(guān)信息
如果 network thread 接收到了重定向請(qǐng)求頭如 301,network thread 會(huì)通知 UI thread 服務(wù)器要求重定向,之后,另外一個(gè) URL 請(qǐng)求會(huì)被觸發(fā)。
3. 讀取響應(yīng)
當(dāng)請(qǐng)求響應(yīng)返回的時(shí)候,network thread 會(huì)依據(jù) Content-Type 及 MIME Type sniffing 判斷響應(yīng)內(nèi)容的格式。
判斷響應(yīng)內(nèi)容的格式
如果響應(yīng)內(nèi)容的格式是 HTML ,下一步將會(huì)把這些數(shù)據(jù)傳遞給 renderer process,如果是 zip 文件或者其它文件,會(huì)把相關(guān)數(shù)據(jù)傳輸給下載管理器。
Safe Browsing 檢查也會(huì)在此時(shí)觸發(fā),如果域名或者請(qǐng)求內(nèi)容匹配到已知的惡意站點(diǎn),network thread 會(huì)展示一個(gè)警告頁。此外 CORB 檢測(cè)也會(huì)觸發(fā)確保敏感數(shù)據(jù)不會(huì)被傳遞給渲染進(jìn)程。
4. 查找渲染進(jìn)程
當(dāng)上述所有檢查完成,network thread 確信瀏覽器可以導(dǎo)航到請(qǐng)求網(wǎng)頁,network thread 會(huì)通知 UI thread 數(shù)據(jù)已經(jīng)準(zhǔn)備好,UI thread 會(huì)查找到一個(gè) renderer process 進(jìn)行網(wǎng)頁的渲染。
收到 Network thread 返回的數(shù)據(jù)后,UI thread 查找相關(guān)的渲染進(jìn)程
由于網(wǎng)絡(luò)請(qǐng)求獲取響應(yīng)需要時(shí)間,這里其實(shí)還存在著一個(gè)加速方案。當(dāng) UI thread 發(fā)送 URL 請(qǐng)求給 network thread 時(shí),瀏覽器其實(shí)已經(jīng)知道了將要導(dǎo)航到那個(gè)站點(diǎn)。UI thread 會(huì)并行的預(yù)先查找和啟動(dòng)一個(gè)渲染進(jìn)程,如果一切正常,當(dāng) network thread 接收到數(shù)據(jù)時(shí),渲染進(jìn)程已經(jīng)準(zhǔn)備就緒了,但是如果遇到重定向,準(zhǔn)備好的渲染進(jìn)程也許就不可用了,這時(shí)候就需要重啟一個(gè)新的渲染進(jìn)程。
5. 確認(rèn)導(dǎo)航
進(jìn)過了上述過程,數(shù)據(jù)以及渲染進(jìn)程都可用了, Browser Process 會(huì)給 renderer process 發(fā)送 IPC 消息來確認(rèn)導(dǎo)航,一旦 Browser Process 收到 renderer process 的渲染確認(rèn)消息,導(dǎo)航過程結(jié)束,頁面加載過程開始。
此時(shí),地址欄會(huì)更新,展示出新頁面的網(wǎng)頁信息。history tab 會(huì)更新,可通過返回鍵返回導(dǎo)航來的頁面,為了讓關(guān)閉 tab 或者窗口后便于恢復(fù),這些信息會(huì)存放在硬盤中。
6. 額外的步驟
一旦導(dǎo)航被確認(rèn),renderer process 會(huì)使用相關(guān)的資源渲染頁面,下文中我們將重點(diǎn)介紹渲染流程。當(dāng) renderer process 渲染結(jié)束(渲染結(jié)束意味著該頁面內(nèi)的所有的頁面,包括所有 iframe 都觸發(fā)了 onload 時(shí)),會(huì)發(fā)送 IPC 信號(hào)到 Browser process, UI thread 會(huì)停止展示 tab 中的 spinner。
Renderer Process 發(fā)送 IPC 消息通知 browser process 頁面已經(jīng)加載完成。
當(dāng)然上面的流程只是網(wǎng)頁首幀渲染完成,在此之后,客戶端依舊可下載額外的資源渲染出新的視圖。
在這里我們可以明確一點(diǎn),所有的 JS 代碼其實(shí)都由 renderer Process 控制的,所以在你瀏覽網(wǎng)頁內(nèi)容的過程大部分時(shí)候不會(huì)涉及到其它的進(jìn)程。不過也許你也曾經(jīng)監(jiān)聽過 beforeunload 事件,這個(gè)事件再次涉及到 Browser Process 和 renderer Process 的交互,當(dāng)當(dāng)前頁面關(guān)閉時(shí)(關(guān)閉 Tab ,刷新等等),Browser Process 需要通知 renderer Process 進(jìn)行相關(guān)的檢查,對(duì)相關(guān)事件進(jìn)行處理。
瀏覽器進(jìn)程發(fā)送 IPC 消息給渲染進(jìn)程,通知要離開當(dāng)前網(wǎng)站了
如果導(dǎo)航由 renderer process 觸發(fā)(比如在用戶點(diǎn)擊某鏈接,或者 JS 執(zhí)行 window.location="http://newsite.com" ) renderer process 會(huì)首先檢查是否有 beforeunload 事件處理器,導(dǎo)航請(qǐng)求由 renderer process 傳遞給 Browser process。
如果導(dǎo)航到新的網(wǎng)站,會(huì)啟用一個(gè)新的 render process 來處理新頁面的渲染,老的進(jìn)程會(huì)留下來處理類似 unload 等事件。
關(guān)于頁面的生命周期,更多內(nèi)容可參考 Page Lifecycle API 。
瀏覽器進(jìn)程發(fā)送 IPC 消息到新的渲染進(jìn)程通知渲染新的頁面,同時(shí)通知舊的渲染進(jìn)程卸載。
除了上述流程,有些頁面還擁有 Service Worker (服務(wù)工作線程),Service Worker 讓開發(fā)者對(duì)本地緩存及判斷何時(shí)從網(wǎng)絡(luò)上獲取信息有了更多的控制權(quán),如果 Service Worker 被設(shè)置為從本地 cache 中加載數(shù)據(jù),那么就沒有必要從網(wǎng)上獲取更多數(shù)據(jù)了。
值得注意的是 service worker 也是運(yùn)行在渲染進(jìn)程中的 JS 代碼,因此對(duì)于擁有 Service Worker 的頁面,上述流程有些許的不同。
當(dāng)有 Service Worker 被注冊(cè)時(shí),其作用域會(huì)被保存,當(dāng)有導(dǎo)航時(shí),network thread 會(huì)在注冊(cè)過的 Service Worker 的作用域中檢查相關(guān)域名,如果存在對(duì)應(yīng)的 Service worker,UI thread 會(huì)找到一個(gè) renderer process 來處理相關(guān)代碼,Service Worker 可能會(huì)從 cache 中加載數(shù)據(jù),從而終止對(duì)網(wǎng)絡(luò)的請(qǐng)求,也可能從網(wǎng)上請(qǐng)求新的數(shù)據(jù)。
Service Worker 依據(jù)具體情形做處理。
關(guān)于 Service Worker 的更多內(nèi)容可參考:
https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
如果 Service Worker 最終決定通過網(wǎng)上獲取數(shù)據(jù),Browser 進(jìn)程 和 renderer 進(jìn)程的交互其實(shí)會(huì)延后數(shù)據(jù)的請(qǐng)求時(shí)間 。Navigation Preload 是一種與 Service Worker 并行的加速加載資源的機(jī)制,服務(wù)端通過請(qǐng)求頭可以識(shí)別這類請(qǐng)求,而做出相應(yīng)的處理。
更多內(nèi)容可參考:
https://developers.google.com/web/updates/2017/02/navigation-preload
渲染進(jìn)程是如何工作的?
渲染進(jìn)程幾乎負(fù)責(zé) Tab 內(nèi)的所有事情,渲染進(jìn)程的核心目的在于轉(zhuǎn)換 HTML CSS JS 為用戶可交互的 web 頁面。渲染進(jìn)程中主要包含以下線程:
渲染進(jìn)程包含的線程
1. 主線程 Main thread
2. 工作線程 Worker thread
3. 排版線程 Compositor thread
4. 光柵線程 Raster thread
后文我們將逐步介紹不同線程的職責(zé),在此之前我們先看看渲染的流程。
1. 構(gòu)建 DOM
當(dāng)渲染進(jìn)程接收到導(dǎo)航的確認(rèn)信息,開始接受 HTML 數(shù)據(jù)時(shí),主線程會(huì)解析文本字符串為 DOM。
渲染 html 為 DOM 的方法由 HTML Standard 定義。
2. 加載次級(jí)的資源
網(wǎng)頁中常常包含諸如圖片,CSS,JS 等額外的資源,這些資源需要從網(wǎng)絡(luò)上或者 cache 中獲取。主進(jìn)程可以在構(gòu)建 DOM 的過程中會(huì)逐一請(qǐng)求它們,為了加速 preload scanner 會(huì)同時(shí)運(yùn)行,如果在 html 中存在 <img><link> 等標(biāo)簽,preload scanner 會(huì)把這些請(qǐng)求傳遞給 Browser process 中的 network thread 進(jìn)行相關(guān)資源的下載。
3.JS 的下載與執(zhí)行
當(dāng)遇到 <script> 標(biāo)簽時(shí),渲染進(jìn)程會(huì)停止解析 HTML,而去加載,解析和執(zhí)行 JS 代碼,停止解析 html 的原因在于 JS 可能會(huì)改變 DOM 的結(jié)構(gòu)(使用諸如 documwnt.write()等 API)。
不過開發(fā)者其實(shí)也有多種方式來告知瀏覽器應(yīng)對(duì)如何應(yīng)對(duì)某個(gè)資源,比如說如果在<script> 標(biāo)簽上添加了 async 或 defer 等屬性,瀏覽器會(huì)異步的加載和執(zhí)行 JS 代碼,而不會(huì)阻塞渲染。更多的方法可參考 Resource Prioritization – Getting the Browser to Help You。
4. 樣式計(jì)算
僅僅渲染 DOM 還不足以獲知頁面的具體樣式,主進(jìn)程還會(huì)基于 CSS 選擇器解析 CSS 獲取每一個(gè)節(jié)點(diǎn)的最終的計(jì)算樣式值。即使不提供任何 CSS,瀏覽器對(duì)每個(gè)元素也會(huì)有一個(gè)默認(rèn)的樣式。
渲染進(jìn)程主線程計(jì)算每一個(gè)元素節(jié)點(diǎn)的最終樣式值
5. 獲取布局
想要渲染一個(gè)完整的頁面,除了獲知每個(gè)節(jié)點(diǎn)的具體樣式,還需要獲知每一個(gè)節(jié)點(diǎn)在頁面上的位置,布局其實(shí)是找到所有元素的幾何關(guān)系的過程。其具體過程如下:
通過遍歷 DOM 及相關(guān)元素的計(jì)算樣式,主線程會(huì)構(gòu)建出包含每個(gè)元素的坐標(biāo)信息及盒子大小的布局樹。布局樹和 DOM 樹類似,但是其中只包含頁面可見的元素,如果一個(gè)元素設(shè)置了 display:none ,這個(gè)元素不會(huì)出現(xiàn)在布局樹上,偽元素雖然在 DOM 樹上不可見,但是在布局樹上是可見的。
6. 繪制各元素
即使知道了不同元素的位置及樣式信息,我們還需要知道不同元素的繪制先后順序才能正確繪制出整個(gè)頁面。在繪制階段,主線程會(huì)遍歷布局樹以創(chuàng)建繪制記錄。繪制記錄可以看做是記錄各元素繪制先后順序的筆記。
主線程依據(jù)布局樹構(gòu)建繪制記錄
7. 合成幀
熟悉 PS 等繪圖軟件的童鞋肯定對(duì)圖層這一概念不陌生,現(xiàn)代 Chrome 其實(shí)利用了這一概念來組合不同的層。
復(fù)合是一種分割頁面為不同的層,并單獨(dú)柵格化,隨后組合為幀的技術(shù)。不同層的組合由 compositor 線程(合成器線程)完成。
主線程會(huì)遍歷布局樹來創(chuàng)建層樹(layer tree),添加了 will-change CSS 屬性的元素,會(huì)被看做單獨(dú)的一層。
主線程遍歷布局樹生成層樹
你可能會(huì)想給每一個(gè)元素都添加上 will-change,不過組合過多的層也許會(huì)比在每一幀都柵格化頁面中的某些小部分更慢。為了更合理的使用層,可參考 堅(jiān)持僅合成器的屬性和管理層計(jì)數(shù) 。
一旦層樹被創(chuàng)建,渲染順序被確定,主線程會(huì)把這些信息通知給合成器線程,合成器線程會(huì)柵格化每一層。有的層的可以達(dá)到整個(gè)頁面的大小,因此,合成器線程將它們分成多個(gè)磁貼,并將每個(gè)磁貼發(fā)送到柵格線程,柵格線程會(huì)柵格化每一個(gè)磁貼并存儲(chǔ)在 GPU 顯存中。
柵格線程會(huì)柵格化每一個(gè)磁貼并存儲(chǔ)在 GPU 顯存中
一旦磁貼被光柵化,合成器線程會(huì)收集稱為繪制四邊形的磁貼信息以創(chuàng)建合成幀。
合成幀隨后會(huì)通過 IPC 消息傳遞給瀏覽器進(jìn)程,由于瀏覽器的 UI 改變或者其它拓展的渲染進(jìn)程也可以添加合成幀,這些合成幀會(huì)被傳遞給 GPU 用以展示在屏幕上,如果滾動(dòng)發(fā)生,合成器線程會(huì)創(chuàng)建另一個(gè)合成幀發(fā)送給 GPU。
合成器線程會(huì)發(fā)送合成幀給 GPU 渲染
合成器的優(yōu)點(diǎn)在于,其工作無關(guān)主線程,合成器線程不需要等待樣式計(jì)算或者 JS 執(zhí)行,這就是為什么合成器相關(guān)的動(dòng)畫 最流暢,如果某個(gè)動(dòng)畫涉及到布局或者繪制的調(diào)整,就會(huì)涉及到主線程的重新計(jì)算,自然會(huì)慢很多。
瀏覽器對(duì)事件的處理
瀏覽器通過對(duì)不同事件的處理來滿足各種交互需求,這一部分我們一起看看從瀏覽器的視角,事件是什么,在此我們先主要考慮鼠標(biāo)事件。
在瀏覽器的看來,用戶的所有手勢(shì)都是輸入,鼠標(biāo)滾動(dòng),懸置,點(diǎn)擊等等都是。
當(dāng)用戶在屏幕上觸發(fā)諸如 touch 等手勢(shì)時(shí),首先收到手勢(shì)信息的是 Browser process, 不過 Browser process 只會(huì)感知到在哪里發(fā)生了手勢(shì),對(duì) tab 內(nèi)內(nèi)容的處理是還是由渲染進(jìn)程控制的。
事件發(fā)生時(shí),瀏覽器進(jìn)程會(huì)發(fā)送事件類型及相應(yīng)的坐標(biāo)給渲染進(jìn)程,渲染進(jìn)程隨后找到事件對(duì)象并執(zhí)行所有綁定在其上的相關(guān)事件處理函數(shù)。
事件從瀏覽器進(jìn)程傳送給渲染進(jìn)程
前文中,我們提到過合成器可以獨(dú)立于主線程之外通過合成柵格化層平滑的處理滾動(dòng)。如果頁面中沒有綁定相關(guān)事件,組合器線程可以獨(dú)立于主線程創(chuàng)建組合幀。如果頁面綁定了相關(guān)事件處理器,主線程就不得不出來工作了。這時(shí)候合成器線程會(huì)怎么處理呢?
這里涉及到一個(gè)專業(yè)名詞「理解非快速滾動(dòng)區(qū)域(non-fast scrollable region)」由于執(zhí)行 JS 是主線程的工作,當(dāng)頁面合成時(shí),合成器線程會(huì)標(biāo)記頁面中綁定有事件處理器的區(qū)域?yàn)?non-fast scrollable region ,如果存在這個(gè)標(biāo)注,合成器線程會(huì)把發(fā)生在此處的事件發(fā)送給主線程,如果事件不是發(fā)生在這些區(qū)域,合成器線程則會(huì)直接合成新的幀而不用等到主線程的響應(yīng)。
涉及 non-fast scrollable region 的事件,合成器線程會(huì)通知主線程進(jìn)行相關(guān)處理。
web 開發(fā)中常用的事件處理模式是事件委托,基于事件冒泡,我們常常在最頂層綁定事件:
復(fù)制代碼
document.body.addEventListener('touchstart', event=> { if (event.target===area) { event.preventDefault(); } } );
上述做法很常見,但是如果從瀏覽器的角度看,整個(gè)頁面都成了 non-fast scrollable region 了。
這意味著即使操作的是頁面無綁定事件處理器的區(qū)域,每次輸入時(shí),合成器線程也需要和主線程通信并等待反饋,流暢的合成器獨(dú)立處理合成幀的模式就失效了。
由于事件綁定在最頂部,整個(gè)頁面都成為了 non-fast scrollable region。
為了防止這種情況,我們可以為事件處理器傳遞 passive: true 做為參數(shù),這樣寫就能讓瀏覽器即監(jiān)聽相關(guān)事件,又讓組合器線程在等等主線程響應(yīng)前構(gòu)建新的組合幀。
復(fù)制代碼
document.body.addEventListener('touchstart', event=> { if (event.target===area) { event.preventDefault() } }, {passive: true} );
不過上述寫法可能又會(huì)帶來另外一個(gè)問題,假設(shè)某個(gè)區(qū)域你只想要水平滾動(dòng),使用 passive: true 可以實(shí)現(xiàn)平滑滾動(dòng),但是垂直方向的滾動(dòng)可能會(huì)先于event.preventDefault()發(fā)生,此時(shí)可以通過 event.cancelable 來防止這種情況。
復(fù)制代碼
document.body.addEventListener('pointermove', event=> { if (event.cancelable) { event.preventDefault(); // block the native scroll /* * do what you want the application to do here */ } }, {passive: true});
也可以使用 css 屬性 touch-action 來完全消除事件處理器的影響,如:
復(fù)制代碼
#area { touch-action: pan-x; }
查找到事件對(duì)象
當(dāng)組合器線程發(fā)送輸入事件給主線程時(shí),主線程首先會(huì)進(jìn)行命中測(cè)試(hit test)來查找對(duì)應(yīng)的事件目標(biāo),命中測(cè)試會(huì)基于渲染過程中生成的繪制記錄( paint records )查找事件發(fā)生坐標(biāo)下存在的元素。
主線程依據(jù)繪制記錄查找事件相關(guān)元素。
事件的優(yōu)化
一般我們屏幕的刷新速率為 60fps,但是某些事件的觸發(fā)量會(huì)不止這個(gè)值,出于優(yōu)化的目的,Chrome 會(huì)合并連續(xù)的事件 (如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延遲到下一幀渲染時(shí)候執(zhí)行 。
而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非連續(xù)性事件則會(huì)立即被觸發(fā)。
Chrome 會(huì)合并連續(xù)事件到下一幀觸發(fā)。
合并事件雖然能提示性能,但是如果你的應(yīng)用是繪畫等,則很難繪制一條平滑的曲線了,此時(shí)可以使用 getCoalescedEvents API 來獲取組合的事件。示例代碼如下:
復(fù)制代碼
window.addEventListener('pointermove', event=> { const events=event.getCoalescedEvents(); for (let event of events) { const x=event.pageX; const y=event.pageY; // draw a line using x and y coordinates. } });
花了好久來整理上面的內(nèi)容,整理的過程收獲還挺大的,也希望這篇筆記能對(duì)你有所啟發(fā),如果有任何疑問,歡迎一起來討論。
本文經(jīng)作者授權(quán)轉(zhuǎn)載,原文鏈接為:
https://zhuanlan.zhihu.com/p/47407398
參考鏈接
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。