『中高級前端面試』之終極知識點

Chrome瀏覽器進程

在資源不足的設備上,將服務合併到瀏覽器進程中

瀏覽器主進程

  • 負責瀏覽器界面顯示
  • 各個頁面的管理,創建以及銷燬
  • 將渲染進程的結果繪製到用戶界面上
  • 網絡資源管理

GPU進程

  • 用於3D渲染繪製

網絡進程

  • 發起網絡請求

插件進程

  • 第三方插件處理,運行在沙箱中

渲染進程

  • 頁面渲染
  • 腳本執行
  • 事件處理

網絡傳輸流程

生成HTTP請求消息

  1. 輸入網址

  2. 瀏覽瀏覽器解析URL

  3. 生成HTTP請求信息

    • https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/http-request.png

    • https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/http-response.png

  4. 收到響應

    狀態碼 含義
    1xx 告知請求的處理進度和情況
    2xx 成功
    3xx 表示需要進一步操作
    4xx 客戶端錯誤
    5xx 服務端錯誤

向DNS服務器查詢Web服務器的IP地址

  1. Socket庫提供查詢IP地址的功能
  2. 通過解析器向DNS服務器發出查詢

全世界DNS服務器的大接力

  1. 尋找相應的DNS服務器並獲取IP地址
  2. 通過緩存加快DNS服務器的響應

委託協議棧發送消息

協議棧通過TCP協議收發數據的操作。

  1. 創建套接字

    https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/create.png

    • 瀏覽器,郵件等一般的應用程序收發數據時用TCP
    • DNS查詢等收發較短的控制數據時用UDP
  2. 連接服務器

    瀏覽器調用Socket.connect

    • 在TCP模塊處創建表示連接控制信息的頭部
    • 通過TCP頭部中的發送方和接收方端口號找到要連接的套接字

    https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/connect.png

  3. 收發數據

    瀏覽器調用Socket.write

    • 將HTTP請求消息交給協議棧

    • 對較大的數據進行拆分,拆分的每一塊數據加上TCP頭,由IP模塊來發送

    • 使用ACK號確認網絡包已收到

    • 根據網絡包平均往返時間調整ACK號等待時間

    • 使用窗口有效管理ACK號

      https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/ack_window.png

      • ACK與窗口的合併
      • 接收HTTP響應消息
  4. 斷開管道並刪除套接字

    • 數據發送完畢後斷開連接

      https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/disconnect.png

    • 刪除套接字

      1. 客戶端發送FIN
      2. 服務端返回ACK號
      3. 服務端發送FIN
      4. 客戶端返回ACK號

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/Socket.png

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/whole.png

網絡協議

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/js/%20interview2/osl.png

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(非對稱加密對稱加密

    1. 客戶端發出https請求,請求服務端建立SSL連接
    2. 服務端收到https請求,申請或自制數字證書,得到公鑰和服務端私鑰,並將公鑰發送給客戶端
    3. 戶端驗證公鑰,不通過驗證則發出警告,通過驗證則產生一個隨機的客戶端私鑰
    4. 客戶端將公鑰與客戶端私鑰進行對稱加密後傳給服務端
    5. 服務端收到加密內容後,通過服務端私鑰進行非對稱解密,得到客戶端私鑰
    6. 服務端將客戶端私鑰和內容進行對稱加密,並將加密內容發送給客戶端
    7. 客戶端收到加密內容後,通過客戶端私鑰進行對稱解密,得到內容
  • HTTP/2.0:多路複用(一次TCP連接可以處理多個請求),服務器主動推送,stream傳輸。

  • HTTP/3:基於 UDP 實現了QUIC 協議

    • 建立好HTTP2連接
    • 發送HTTP2擴展幀
    • 使用QUIC建立連接
    • 如果成功就斷開HTTP2連接
    • 升級爲HTTP3連接

注:RTT = Round-trip time

頁面渲染流程

構建 DOM 樹、樣式計算、佈局階段、分層、繪製、分塊、光柵化和合成

  1. 創建DOM tree
    • 遍歷 DOM 樹中的所有可見節點,並把這些節點加到佈局樹中。
    • 不可見的節點會被佈局樹忽略掉。
  2. 樣式計算
    • 創建CSSOM tree
    • 轉換樣式表中的屬性值
    • 計算出DOM節點樣式
  3. 生成layout tree
  4. 分層
    • 生成圖層樹(LayerTree)
    • 擁有層疊上下文屬性的元素會被提升爲單獨的一層
    • 需要剪裁(clip)的地方也會被創建爲圖層
    • 圖層繪製
  5. 將圖層轉換爲位圖
  6. 合成位圖並顯示在頁面中

頁面更新機制

  • 更新了元素的幾何屬性(重排)
  • 更新元素的繪製屬性(重繪)
  • 直接合成
    • CSS3的屬性可以直接跳到這一步

JS執行機制

代碼提升(爲了編譯)

  • 變量提升
  • 函數提升(優先級最高)

編譯代碼

  1. 生成抽象語法樹(AST)和執行上下文

    1. 第一階段是分詞(tokenize),又稱爲詞法分析
    2. 第二階段是解析(parse),又稱爲語法分析
  2. 生成字節碼

    字節碼就是介於 AST 和機器碼之間的一種代碼。但是與特定類型的機器碼無關,字節碼需要通過解釋器將其轉換爲機器碼後才能執行。

  3. 執行代碼

執行代碼

  • 執行全局代碼時,創建全局上下文
  • 調用函數時,創建函數上下文
  • 使用eval函數時,創建eval上下文
  • 執行局部代碼時,創建局部上下文

類型

基本類型

  • Undefined
  • Null
  • Boolean
  • String
  • Symbol
  • Number
  • Object
  • BigInt

複雜類型

  • Object

隱式轉換規則

基本情況

  • 轉換爲布爾值
  • 轉換爲數字
  • 轉換爲字符串

轉換爲原始類型

對象在轉換類型的時候,會執行原生方法ToPrimitive

其算法如下:

  1. 如果已經是 原始類型,則返回當前值;
  2. 如果需要轉 字符串 則先調用toSting方法,如果此時是 原始類型 則直接返回,否則再調用valueOf方法並返回結果;
  3. 如果不是 字符串,則先調用valueOf方法,如果此時是 原始類型 則直接返回,否則再調用toString方法並返回結果;
  4. 如果都沒有 原始類型 返回,則拋出 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-0NaN,則返回 false;其他情況則返回 true
String 如果參數爲空字符串,則返回 false;否則返回 true
Symbol 返回 true
Object 返回 true

轉換爲數字

對象轉換爲數字的規則如下表:

參數類型 結果
Undefined 返回 NaN
Null Return +0.
Boolean 如果參數爲 true,則返回 1false則返回 +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 一旦被綁定,就不會再被任何方式所改變。

三點注意:

  1. 當函數作爲對象的方法調用時,函數中的 this 就是該對象;
  2. 當函數被正常調用時,在嚴格模式下,this 值是 undefined,非嚴格模式下 this 指向的是全局對象 window;
  3. 嵌套函數中的 this 不會繼承外層函數的 this 值。
  4. 我們還提了一下箭頭函數,因爲箭頭函數沒有自己的執行上下文,所以箭頭函數的 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 中會把堆分爲新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。

    • 副垃圾回收器,主要負責新生代的垃圾回收。
    • 主垃圾回收器,主要負責老生代的垃圾回收。

    垃圾回收重要術語:

    • 代際假說
    • 分代收集

    工作流程:

    1. 標記空間中活動對象和非活動對象
    2. 回收非活動對象所佔據的內存
    3. 內存整理

    一旦執行垃圾回收算法,會導致 全停頓(Stop-The-World) 。但是V8有 增量標記算法。V8 將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成。

事件循環

微任務(microtask)

  • process.nextTick
  • promise
  • Object.observe (已廢棄)
  • MutationObserver

宏任務(macrotask)

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

執行順序

  1. 執行同步代碼,這屬於宏任務
  2. 執行棧爲空,查詢是否有微任務需要執行
  3. 必要的話渲染 UI
  4. 然後開始下一輪 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">

https://camo.githubusercontent.com/4e1a2fff1565062e3c71363f91dd1fba6a5e0d8b/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f312f352f313638316332316530343535326637373f773d3232313326683d39343826663d706e6726733d333131313733

強緩存

  1. Expires

    • 緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。
    • Expires 是 HTTP/1 的產物,受限於本地時間,如果修改了本地時間,可能會造成緩存失效。
  2. Cache-Control

    https://camo.githubusercontent.com/ba9d2cbebd6daefbf1050a73726cf972e9a4fbc5/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f312f332f313638313436376438323562326562653f773d35363226683d33343326663d706e6726733d313031373538

    https://camo.githubusercontent.com/91cdfdc6e71e175718bf085f05c7ba40aa0da084/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031382f352f32302f313633376430626264366461313134663f773d38323026683d37333926663d706e6726733d313033333139

協商緩存

協商緩存就是強制緩存失效後,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程。

  • 服務器響應頭: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"

參考資料

  1. 瀏覽器工作原理與實踐
  2. 瀏覽器的運行機制—2.瀏覽器都包含哪些進程?
  3. 「中高級前端面試」JavaScript手寫代碼無敵祕籍
  4. JavaScript 深拷貝
  5. bailnl/promise
  6. 網絡是怎樣連接的?
  7. 瀏覽器工作原理與實踐
  8. 瀏覽器的工作原理:新式網絡瀏覽器幕後揭祕
  9. 前端面試之道
  10. HTTP各版本的區別
  11. 你覺得Node.js是單線程這個結論對嗎?
  12. Node指南
  13. 深入理解瀏覽器的緩存機制
  14. 公司要求會使用框架vue,面試題會被問及哪些?
  15. 「面試題」20+Vue面試題整理

後記

如果你喜歡探討技術,或者對本文有任何的意見或建議,非常歡迎加魚頭微信好友一起探討,當然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。
魚頭的微信號是:krisChans95
也可以掃碼添加好友,備註“csdn”就行(加好友送200M前端面試資料)

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/base/wx-qrcode1.jpg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章