- 作者:陳大魚頭
- github: KRISACHAN
Chrome瀏覽器進程
在資源不足的設備上,將服務合併到瀏覽器進程中
瀏覽器主進程
- 負責瀏覽器界面顯示
- 各個頁面的管理,創建以及銷燬
- 將渲染進程的結果繪製到用戶界面上
- 網絡資源管理
GPU進程
- 用於3D渲染繪製
網絡進程
- 發起網絡請求
插件進程
- 第三方插件處理,運行在沙箱中
渲染進程
- 頁面渲染
- 腳本執行
- 事件處理
網絡傳輸流程
生成HTTP請求消息
-
輸入網址
-
瀏覽瀏覽器解析URL
-
生成HTTP請求信息
-
收到響應
狀態碼 含義 1xx 告知請求的處理進度和情況 2xx 成功 3xx 表示需要進一步操作 4xx 客戶端錯誤 5xx 服務端錯誤
向DNS服務器查詢Web服務器的IP地址
- Socket庫提供查詢IP地址的功能
- 通過解析器向DNS服務器發出查詢
全世界DNS服務器的大接力
- 尋找相應的DNS服務器並獲取IP地址
- 通過緩存加快DNS服務器的響應
委託協議棧發送消息
協議棧通過TCP協議收發數據的操作。
-
創建套接字
- 瀏覽器,郵件等一般的應用程序收發數據時用TCP
- DNS查詢等收發較短的控制數據時用UDP
-
連接服務器
瀏覽器調用Socket.connect
- 在TCP模塊處創建表示連接控制信息的頭部
- 通過TCP頭部中的發送方和接收方端口號找到要連接的套接字
-
收發數據
瀏覽器調用Socket.write
-
將HTTP請求消息交給協議棧
-
對較大的數據進行拆分,拆分的每一塊數據加上TCP頭,由IP模塊來發送
-
使用ACK號確認網絡包已收到
-
根據網絡包平均往返時間調整ACK號等待時間
-
使用窗口有效管理ACK號
- ACK與窗口的合併
- 接收HTTP響應消息
-
-
斷開管道並刪除套接字
-
數據發送完畢後斷開連接
-
刪除套接字
- 客戶端發送FIN
- 服務端返回ACK號
- 服務端發送FIN
- 客戶端返回ACK號
-
網絡協議
TCP
傳輸控制協議(TCP,Transmission Control Protocol)是一種面向連接的、可靠的、基於字節流的傳輸層通信協議,由IETF的RFC 793 定義。
- 基於流的方式
- 面向連接
- 丟包重傳
- 保證數據順序
UDP
Internet 協議集支持一個無連接的傳輸協議,該協議稱爲用戶數據報協議(UDP,User Datagram Protocol)。UDP 爲應用程序提供了一種無需建立連接就可以發送封裝的 IP 數據包的方法。RFC 768 描述了 UDP。
- UDP是非連接的協議,也就是不會跟終端建立連接
- UDP包信息只有8個字節
- UDP是面向報文的。既不拆分,也不合並,而是保留這些報文的邊界
- UDP可能丟包
- UDP不保證數據順序
HTTP
-
HTTP/0.9:GET,無狀態的特點形成
-
HTTP/1.0:支持POST,HEAD,添加了請求頭和響應頭,支持任何格式的文件發送,添加了狀態碼、多字符集支持、多部分發送、權限、緩存、內容編碼等
-
HTTP/1.1:默認長連接,同時6 個 TCP連接,CDN 域名分片
-
HTTPS:HTTP + TLS(非對稱加密 與 對稱加密)
- 客戶端發出https請求,請求服務端建立SSL連接
- 服務端收到https請求,申請或自制數字證書,得到公鑰和服務端私鑰,並將公鑰發送給客戶端
- 戶端驗證公鑰,不通過驗證則發出警告,通過驗證則產生一個隨機的客戶端私鑰
- 客戶端將公鑰與客戶端私鑰進行對稱加密後傳給服務端
- 服務端收到加密內容後,通過服務端私鑰進行非對稱解密,得到客戶端私鑰
- 服務端將客戶端私鑰和內容進行對稱加密,並將加密內容發送給客戶端
- 客戶端收到加密內容後,通過客戶端私鑰進行對稱解密,得到內容
-
HTTP/2.0:多路複用(一次TCP連接可以處理多個請求),服務器主動推送,stream傳輸。
-
HTTP/3:基於 UDP 實現了QUIC 協議
- 建立好HTTP2連接
- 發送HTTP2擴展幀
- 使用QUIC建立連接
- 如果成功就斷開HTTP2連接
- 升級爲HTTP3連接
注:RTT = Round-trip time
頁面渲染流程
構建 DOM 樹、樣式計算、佈局階段、分層、繪製、分塊、光柵化和合成
- 創建DOM tree
- 遍歷 DOM 樹中的所有可見節點,並把這些節點加到佈局樹中。
- 不可見的節點會被佈局樹忽略掉。
- 樣式計算
- 創建CSSOM tree
- 轉換樣式表中的屬性值
- 計算出DOM節點樣式
- 生成layout tree
- 分層
- 生成圖層樹(LayerTree)
- 擁有層疊上下文屬性的元素會被提升爲單獨的一層
- 需要剪裁(clip)的地方也會被創建爲圖層
- 圖層繪製
- 將圖層轉換爲位圖
- 合成位圖並顯示在頁面中
頁面更新機制
- 更新了元素的幾何屬性(重排)
- 更新元素的繪製屬性(重繪)
- 直接合成
- CSS3的屬性可以直接跳到這一步
JS執行機制
代碼提升(爲了編譯)
- 變量提升
- 函數提升(優先級最高)
編譯代碼
-
生成抽象語法樹(AST)和執行上下文
- 第一階段是分詞(tokenize),又稱爲詞法分析
- 第二階段是解析(parse),又稱爲語法分析
-
生成字節碼
字節碼就是介於 AST 和機器碼之間的一種代碼。但是與特定類型的機器碼無關,字節碼需要通過解釋器將其轉換爲機器碼後才能執行。
-
執行代碼
執行代碼
- 執行全局代碼時,創建全局上下文
- 調用函數時,創建函數上下文
- 使用eval函數時,創建eval上下文
- 執行局部代碼時,創建局部上下文
類型
基本類型
- Undefined
- Null
- Boolean
- String
- Symbol
- Number
- Object
- BigInt
複雜類型
- Object
隱式轉換規則
基本情況
- 轉換爲布爾值
- 轉換爲數字
- 轉換爲字符串
轉換爲原始類型
對象在轉換類型的時候,會執行原生方法ToPrimitive。
其算法如下:
- 如果已經是 原始類型,則返回當前值;
- 如果需要轉 字符串 則先調用
toSting
方法,如果此時是 原始類型 則直接返回,否則再調用valueOf
方法並返回結果; - 如果不是 字符串,則先調用
valueOf
方法,如果此時是 原始類型 則直接返回,否則再調用toString
方法並返回結果; - 如果都沒有 原始類型 返回,則拋出 TypeError類型錯誤。
當然,我們可以通過重寫Symbol.toPrimitive
來制定轉換規則,此方法在轉原始類型時調用優先級最高。
const data = {
valueOf () {
return 1;
},
toString () {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
}
};
data + 1 // 3
轉換爲布爾值
對象轉換爲布爾值的規則如下表:
參數類型 | 結果 |
---|---|
Undefined | 返回 false 。 |
Null | 返回 false 。 |
Boolean | 返回 當前參數。 |
Number | 如果參數爲+0 、-0 或NaN ,則返回 false ;其他情況則返回 true 。 |
String | 如果參數爲空字符串,則返回 false ;否則返回 true 。 |
Symbol | 返回 true 。 |
Object | 返回 true 。 |
轉換爲數字
對象轉換爲數字的規則如下表:
參數類型 | 結果 |
---|---|
Undefined | 返回 NaN 。 |
Null | Return +0. |
Boolean | 如果參數爲 true ,則返回 1 ;false 則返回 +0 。 |
Number | 返回當前參數。 |
String | 先調用 ToPrimitive,再調用 ToNumber,然後返回結果。 |
Symbol | 拋出 TypeError 錯誤。 |
Object | 先調用 ToPrimitive,再調用 ToNumber,然後返回結果。 |
轉換爲字符串
對象轉換爲字符串的規則如下表:
參數類型 | 結果 |
---|---|
Undefined | 返回 "undefined" 。 |
Null | 返回 "null" 。 |
Boolean | 如果參數爲 true ,則返回 "true" ;否則返回 "false" 。 |
Number | 調用 NumberToString,然後返回結果。 |
String | 返回 當前參數。 |
Symbol | 拋出 TypeError 錯誤。 |
Object | 先調用 ToPrimitive,再調用 ToString,然後返回結果。 |
this
this 是和執行上下文綁定的。
執行上下文:
- 全局執行上下文:全局執行上下文中的 this 也是指向 window 對象。
- 函數執行上下文:使用對象來調用其內部的一個方法,該方法的 this 是指向對象本身的。
- eval 執行上下文:執行eval環境內部的上兩個情況。
根據優先級最高的來決定 this
最終指向哪裏。
首先,new
的方式優先級最高,接下來是 bind
這些函數,然後是 obj.foo()
這種調用方式,最後是 foo
這種調用方式,同時,箭頭函數的 this
一旦被綁定,就不會再被任何方式所改變。
三點注意:
- 當函數作爲對象的方法調用時,函數中的 this 就是該對象;
- 當函數被正常調用時,在嚴格模式下,this 值是 undefined,非嚴格模式下 this 指向的是全局對象 window;
- 嵌套函數中的 this 不會繼承外層函數的 this 值。
- 我們還提了一下箭頭函數,因爲箭頭函數沒有自己的執行上下文,所以箭頭函數的 this 就是它外層函數的 this。
閉包
沒有被引用的閉包會被自動回收,但還存在全局變量中,則依然會內存泄漏。
在 JavaScript 中,根據詞法作用域的規則,內部函數總是可以訪問其外部函數中聲明的變量,當通過調用一個外部函數返回一個內部函數後,即使該外部函數已經執行結束了,但是內部函數引用外部函數的變量依然保存在內存中,我們就把這些變量的集合稱爲閉包。比如外部函數是 foo,那麼這些變量的集合就稱爲 foo 函數的閉包。
var getNum
function getCounter() {
var n = 1
var inner = function() {
n++
}
return inner
}
getNum = getCounter()
getNum() // 2
getNum() // 3
getNum() // 5
getNum() // 5
作用域
全局作用域
對象在代碼中的任何地方都能訪問,其生命週期伴隨着頁面的生命週期。
函數作用域
函數內部定義的變量或者函數,並且定義的變量或者函數只能在函數內部被訪問。函數執行結束之後,函數內部定義的變量會被銷燬。
局部作用域
使用一對大括號包裹的一段代碼,比如函數、判斷語句、循環語句,甚至單獨的一個{}都可以被看作是一個塊級作用域。
作用域鏈
詞法作用域
詞法作用域就是指作用域是由代碼中函數聲明的位置來決定的,所以詞法作用域是靜態的作用域,通過它就能夠預測代碼在執行過程中如何查找標識符。
詞法作用域是代碼階段就決定好的,和函數是怎麼調用的沒有關係。
原型&原型鏈
其實每個 JS 對象都有 __proto__
屬性,這個屬性指向了原型。
原型也是一個對象,並且這個對象中包含了很多函數,對於 obj
來說,可以通過 __proto__
找到一個原型對象,在該對象中定義了很多函數讓我們來使用。
原型鏈:
Object
是所有對象的爸爸,所有對象都可以通過__proto__
找到它Function
是所有函數的爸爸,所有函數都可以通過__proto__
找到它- 函數的
prototype
是一個對象 - 對象的
__proto__
屬性指向原型,__proto__
將對象和原型連接起來組成了原型鏈
V8工作原理
數據存儲
- 棧空間:調用棧,存儲執行上下文,以及存儲原始類型的數據
- 堆空間:存儲引用類型
原始類型的賦值會完整複製變量值,而引用類型的賦值是複製引用地址。
垃圾回收
-
回收調用棧內的數據:執行上下文結束且沒有被引用時,則會通過向下移動 記錄當前執行狀態的指針(稱爲 ESP) 來銷燬該函數保存在棧中的執行上下文。
-
回收堆裏的數據:
V8 中會把堆分爲新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。
- 副垃圾回收器,主要負責新生代的垃圾回收。
- 主垃圾回收器,主要負責老生代的垃圾回收。
垃圾回收重要術語:
- 代際假說
- 分代收集
工作流程:
- 標記空間中活動對象和非活動對象
- 回收非活動對象所佔據的內存
- 內存整理
一旦執行垃圾回收算法,會導致 全停頓(Stop-The-World) 。但是V8有 增量標記算法。V8 將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成。
事件循環
微任務(microtask)
- process.nextTick
- promise
- Object.observe (已廢棄)
- MutationObserver
宏任務(macrotask)
- script
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
執行順序
- 執行同步代碼,這屬於宏任務
- 執行棧爲空,查詢是否有微任務需要執行
- 必要的話渲染 UI
- 然後開始下一輪 Event loop,執行宏任務中的異步代碼
瀏覽器安全
攻擊方式
-
xss:將代碼注入到網頁
- 持久型:寫入數據庫
- 非持久型:修改用戶代碼
-
csrf:跨站請求僞造。
- Get 請求不對數據進行修改
- 不讓第三方網站訪問到用戶 Cookie
- 阻止第三方網站請求接口
- 請求時附帶驗證信息,比如驗證碼或者 Token
-
中間人攻擊:中間人攻擊是攻擊方同時與服務端和客戶端建立起了連接,並讓對方認爲連接是安全的,但是實際上整個通信過程都被攻擊者控制了。攻擊者不僅能獲得雙方的通信信息,還能修改通信信息。
當然防禦中間人攻擊其實並不難,只需要增加一個安全通道來傳輸信息。
CSP
建立白名單
- HTTP Header 中的
Content-Security-Policy
<meta http-equiv="Content-Security-Policy">
瀏覽器性能
DNS預解析
<link rel="dns-prefetch" href="" />
- Chrome 和 Firefox 3.5+ 能自動進行預解析
- 關閉DNS預解析:
<meta http-equiv="x-dns-prefetch-control" content="off|on">
強緩存
-
Expires
- 緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。
- Expires 是 HTTP/1 的產物,受限於本地時間,如果修改了本地時間,可能會造成緩存失效。
-
Cache-Control
協商緩存
協商緩存就是強制緩存失效後,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程。
- 服務器響應頭:Last-Modified,Etag
- 瀏覽器請求頭:If-Modified-Since,If-None-Match
**Last-Modified ** 與 If-Modified-Since 配對。Last-Modified
把Web應用最後修改時間告訴客戶端,客戶端下次請求之時會把 If-Modified-Since
的值發生給服務器,服務器由此判斷是否需要重新發送資源,如果不需要則返回304,如果有則返回200。這對組合的缺點是隻能精確到秒,而且是根據本地打開時間來記錄的,所以會不準確。
**Etag ** 與 If-None-Match 配對。它們沒有使用時間作爲判斷標準,而是使用了一組特徵串。Etag
把此特徵串發生給客戶端,客戶端在下次請求之時會把此特徵串作爲If-None-Match
的值發送給服務端,服務器由此判斷是否需要重新發送資源,如果不需要則返回304,如果有則返回200。
NodeJs
單線程
基礎概念:
- 進程:進程(英語:process),是指計算機中已運行的程序。進程曾經是分時系統的基本運作單位。
- 線程:線程(英語:thread)是操作系統能夠進行運算調度的最小單位。大部分情況下,它被包含在進程之中,是進程中的實際運作單位。
- 協程:協程(英語:coroutine)是計算機程序的一類組件,推廣了協作式多任務的子程序,允許執行被掛起與被恢復。
Node 中最核心的是 v8 引擎,在 Node 啓動後,會創建 v8 的實例,這個實例是多線程的,各個線程如下:
- 主線程:編譯、執行代碼。
- 編譯/優化線程:在主線程執行的時候,可以優化代碼。
- 分析器線程:記錄分析代碼運行時間,爲 Crankshaft 優化代碼執行提供依據。
- 垃圾回收的幾個線程。
非阻塞I/O
阻塞 是指在 Node.js 程序中,其它 JavaScript 語句的執行,必須等待一個非 JavaScript 操作完成。這是因爲當 阻塞 發生時,事件循環無法繼續運行 JavaScript。
在 Node.js 中,JavaScript 由於執行 CPU 密集型操作,而不是等待一個非 JavaScript 操作(例如 I/O)而表現不佳,通常不被稱爲 阻塞。在 Node.js 標準庫中使用 libuv 的同步方法是最常用的 阻塞 操作。原生模塊中也有 阻塞 方法。
事件循環
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
注意:每個框被稱爲事件循環機制的一個階段。
在 Windows 和 Unix/Linux 實現之間存在細微的差異,但這對演示來說並不重要。
階段概述:
- 定時器:本階段執行已經被
setTimeout()
和setInterval()
的調度回調函數。 - 待定回調:執行延遲到下一個循環迭代的 I/O 回調。
- idle, prepare:僅系統內部使用。
- 輪詢:檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數,那些由計時器和
setImmediate()
調度的之外),其餘情況 node 將在適當的時候在此阻塞。 - 檢測:
setImmediate()
回調函數在這裏執行。 - 關閉的回調函數:一些關閉的回調函數,如:
socket.on('close', ...)
。
在每次運行的事件循環之間,Node.js 檢查它是否在等待任何異步 I/O 或計時器,如果沒有的話,則完全關閉。
process.nextTick()
:它是異步 API 的一部分。從技術上講不是事件循環的一部分。不管事件循環的當前階段如何,都將在當前操作完成後處理 nextTickQueue
。這裏的一個操作被視作爲一個從底層 C/C++ 處理器開始過渡,並且處理需要執行的 JavaScript 代碼。
Libuv
Libuv 是一個跨平臺的異步 IO 庫,它結合了 UNIX 下的 libev 和 Windows 下的 IOCP 的特性,最早由 Node.js 的作者開發,專門爲 Node.js 提供多平臺下的異步IO支持。Libuv 本身是由 C++ 語言實現的,Node.js 中的非阻塞 IO 以及事件循環的底層機制都是由 libuv 實現的。
在 Windows 環境下,libuv 直接使用Windows的 IOCP 來實現異步IO。在 非Windows 環境下,libuv使用多線程(線程池Thread Pool)來模擬異步IO,這裏僅簡要提一下 libuv 中有線程池的概念,之後的文章會介紹 libuv 如何實現進程間通信。
手寫代碼
new操作符
var New = function (Fn) {
var obj = {} // 創建空對象
var arg = Array.prototype.slice.call(arguments, 1)
obj.__proto__ = Fn.prototype // 將obj的原型鏈__proto__指向構造函數的原型prototype
obj.__proto__.constructor = Fn // 在原型鏈 __proto__上設置構造函數的構造器constructor,爲了實例化Fn
Fn.apply(obj, arg) // 執行Fn,並將構造函數Fn執行obj
return obj // 返回結果
}
深拷貝
const getType = (data) => { // 獲取數據類型
const baseType = Object.prototype.toString.call(data).replace(/^\[object\s(.+)\]$/g, '$1').toLowerCase();
const type = data instanceof Element ? 'element' : baseType;
return type;
};
const isPrimitive = (data) => { // 判斷是否是基本數據類型
const primitiveType = 'undefined,null,boolean,string,symbol,number,bigint,map,set,weakmap,weakset'.split(','); // 其實還有很多類型
return primitiveType.includes(getType(data));
};
const isObject = data => (getType(data) === 'object');
const isArray = data => (getType(data) === 'array');
const deepClone = data => {
let cache = {}; // 緩存值,防止循環引用
const baseClone = _data => {
let res;
if (isPrimitive(_data)) {
return data;
} else if (isObject(_data)) {
res = { ..._data }
} else if (isArray(_data)) {
res = [..._data]
};
// 判斷是否有複雜類型的數據,有就遞歸
Reflect.ownKeys(res).forEach(key => {
if (res[key] && getType(res[key]) === 'object') {
// 用cache來記錄已經被複制過的引用地址。用來解決循環引用的問題
if (cache[res[key]]) {
res[key] = cache[res[key]];
} else {
cache[res[key]] = res[key];
res[key] = baseClone(res[key]);
};
};
});
return res;
};
return baseClone(data);
};
手寫bind
Function.prototype.bind2 = function (context) {
if (typeof this !== 'function') {
throw new Error('...');
};
var that = this;
var args1 = Array.prototype.slice.call(arguments,1);
var bindFn = function () {
var args2 = Array.prototype.slice.call(arguments);
var that2 = this instanceof bindFn ? this : context; // 如果當前函數的this指向的是構造函數中的this 則判定爲new 操作。如果this是構造函數bindFn new出來的實例,那麼此處的this一定是該實例本身。
return that.apply(
that2,
args1.concat(args2)
);
}
var Fn = function () {}; // 連接原型鏈用Fn
// 原型賦值
Fn.prototype = this.prototype; // bindFn的prototype指向和this的prototype一樣,指向同一個原型對象
bindFn.prototype = new Fn();
return bindFn;
}
手寫函數柯里化
const curry = fn => {
if (typeof fn !== 'function') {
throw Error('No function provided')
}
return function curriedFn(...args){
if (args.length < fn.length) {
return function () {
return curriedFn.apply(null, args.concat([].slice.call(arguments)))
}
}
return fn.apply(null, args)
}
}
手寫Promise
// 來源於 https://github.com/bailnl/promise/blob/master/src/promise.js
const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;
const isFunction = fn => (typeof fn === 'function');
const isObject = obj => (obj !== null && typeof obj === 'object');
const noop = () => {};
const nextTick = fn => setTimeout(fn, 0);
const resolve = (promise, x) => {
if (promise === x) {
reject(promise, new TypeError('You cannot resolve a promise with itself'));
} else if (x && x.constructor === Promise) {
if (x._stauts === PENDING) {
const handler = statusHandler => value => statusHandler(promise, value) ;
x.then(handler(resolve), handler(reject));
} else if (x._stauts === FULFILLED) {
fulfill(promise, x._value);
} else if (x._stauts === REJECTED) {
reject(promise, x._value);
};
} else if (isFunction(x) || isObject(x)) {
let isCalled = false;
try {
const then = x.then;
if (isFunction(then)) {
const handler = statusHandler => value => {
if (!isCalled) {
statusHandler(promise, value);
}
isCalled = true;
};
then.call(x, handler(resolve), handler(reject));
} else {
fulfill(promise, x);
};
} catch (e) {
if (!isCalled) {
reject(promise, e);
};
};
} else {
fulfill(promise, x);
};
};
const reject = (promise, reason) => {
if (promise._stauts !== PENDING) {
return;
}
promise._stauts = REJECTED;
promise._value = reason;
invokeCallback(promise);
};
const fulfill = (promise, value) => {
if (promise._stauts !== PENDING) {
return;
};
promise._stauts = FULFILLED;
promise._value = value;
invokeCallback(promise);
};
const invokeCallback = (promise) => {
if (promise._stauts === PENDING) {
return;
};
nextTick(() => {
while (promise._callbacks.length) {
const {
onFulfilled = (value => value),
onRejected = (reason => { throw reason }),
thenPromise,
} = promise._callbacks.shift();
let value;
try {
value = (promise._stauts === FULFILLED ? onFulfilled : onRejected)(promise._value);
} catch (e) {
reject(thenPromise, e);
continue;
}
resolve(thenPromise, value);
};
});
};
class Promise {
static resolve(value) {
return new Promise((resolve, reject) => resolve(value))
}
static reject(reason) {
return new Promise((resolve, reject) => reject(reason))
}
constructor(resolver) {
if (!(this instanceof Promise)) {
throw new TypeError(`Class constructor Promise cannot be invoked without 'new'`);
};
if (!isFunction(resolver)) {
throw new TypeError(`Promise resolver ${resolver} is not a function`);
};
this._stauts = PENDING;
this._value = undefined;
this._callbacks = [];
try {
resolver(value => resolve(this, value), reason => reject(this, reason));
} catch (e) {
reject(this, e);
};
};
then(onFulfilled, onRejected) {
const thenPromise = new this.constructor(noop);
this._callbacks = this._callbacks.concat([{
onFulfilled: isFunction(onFulfilled) ? onFulfilled : void 0,
onRejected: isFunction(onRejected) ? onRejected : void 0,
thenPromise,
}]);
invokeCallback(this);
return thenPromise;
};
catch(onRejected) {
return this.then(void 0, onRejected);
};
};
手寫防抖函數
const debounce = (fn = {}, wait=50, immediate) => {
let timer;
return function () {
if (immediate) {
fn.apply(this, arguments)
};
if (timer) {
clearTimeout(timer)
timer = null;
};
timer = setTimeout(()=> {
fn.apply(this,arguments)
}, wait);
};
};
手寫節流函數
var throttle = (fn = {}, wait = 0) => {
let prev = new Date();
return function () {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
};
}
}
手寫instanceOf
const instanceOf = (left, right) => {
let proto = left.__proto__;
let prototype = right.prototype
while (true) {
if (proto === null) {
return false;
} else if (proto === prototype) {
return true;
};
proto = proto.__proto__;
};
}
其它知識
typeof vs instanceof
instanceof
運算符用來檢測 constructor.prototype
是否存在於參數 object
的原型鏈上。
typeof
操作符返回一個字符串,表示未經計算的操作數的類型。
在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示類型的標籤和實際數據值表示的。對象的類型標籤是 0。由於 null
代表的是空指針(大多數平臺下值爲 0x00),因此,null 的類型標籤是 0,typeof null
也因此返回 "object"
。
參考資料
- 瀏覽器工作原理與實踐
- 瀏覽器的運行機制—2.瀏覽器都包含哪些進程?
- 「中高級前端面試」JavaScript手寫代碼無敵祕籍
- JavaScript 深拷貝
- bailnl/promise
- 網絡是怎樣連接的?
- 瀏覽器工作原理與實踐
- 瀏覽器的工作原理:新式網絡瀏覽器幕後揭祕
- 前端面試之道
- HTTP各版本的區別
- 你覺得Node.js是單線程這個結論對嗎?
- Node指南
- 深入理解瀏覽器的緩存機制
- 公司要求會使用框架vue,面試題會被問及哪些?
- 「面試題」20+Vue面試題整理
後記
如果你喜歡探討技術,或者對本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。
魚頭的微信號是:krisChans95
也可以掃碼添加好友,備註“csdn”就行(加好友送200M前端面試資料)