『1W7字中高級前端面試必知必會』終極版

Chrome 瀏覽器進程

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

瀏覽器主進程

  • 負責瀏覽器界面顯示html

  • 各個頁面的管理,建立以及銷燬前端

  • 將渲染進程的結果繪製到用戶界面上html5

  • 網絡資源管理java

GPU 進程

  • 用於 3D 渲染繪製

網絡進程

  • 發起網絡請求

插件進程

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

渲染進程

  • 頁面渲染node

  • 腳本執行android

  • 事件處理nginx

網絡傳輸流程

生成 HTTP 請求消息

  1. 輸入網址git

  2. 瀏覽瀏覽器解析 URLgithub

  3. 生成 HTTP 請求信息

  4. 收到響應

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

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

  1. Socket 庫提供查詢 IP 地址的功能

  2. 經過解析器向 DNS 服務器發出查詢

全世界 DNS 服務器的大接力

  1. 尋找相應的 DNS 服務器並獲取 IP 地址

  2. 經過緩存加快 DNS 服務器的響應

委託協議棧發送消息

協議棧經過 TCP 協議收發數據的操做。

  1. 建立套接字

  • 瀏覽器,郵件等通常的應用程序收發數據時用 TCP
  • DNS 查詢等收發較短的控制數據時用 UDP
  1. 鏈接服務器

瀏覽器調用 Socket.connect

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

  1. 收發數據

瀏覽器調用 Socket.write

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

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

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

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

  • 使用窗口有效管理 ACK 號

  • ACK 與窗口的合併

  • 接收 HTTP 響應消息

  1. 斷開管道並刪除套接字

瀏覽器調用 Socket.close

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

  • 刪除套接字

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

跨域

同源策略

同源策略是一個重要的安全策略,它用於限制一個origin的文檔或者它加載的腳本如何能與另外一個源的資源進行交互。它能幫助阻隔惡意文檔,減小可能被攻擊的媒介。

若是兩個 URL 的 protocolport (若是有指定的話)和 host 都相同的話,則這兩個 URL 是同源。

例如:

URL 結果 緣由
http://store.company.com/dir2/other.html 同源 只有路徑不一樣
http://store.company.com/dir/inner/another.html 同源 只有路徑不一樣
https://store.company.com/secure.html 失敗 協議不一樣
http://store.company.com:81/dir/etc.html 失敗 端口不一樣 ( http:// 默認端口是80)
http://news.company.com/dir/other.html 失敗 主機不一樣

主要的跨域處理

JSONP

JSONP的原理是:靜態資源請求不受同源策略影響。實現以下:

const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://www.domain.com/a?data=1&callback=cb'
const cb = res => {
    console.log(JSON.stringify(res))
}
複製代碼

CORS

CORS:跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不一樣源服務器上的指定的資源。

在各類服務端代碼實現以下:

// 根據不一樣語言規則,具體語法有所不一樣,此處以NodeJs的express爲例
//設置跨域訪問 
app.all('*', function(req, res, next) {  
    res.header("Access-Control-Allow-Origin", "*");  
    res.header("Access-Control-Allow-Headers", "X-Requested-With");  
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    next();  
});   
複製代碼

Nginx實現以下:

server {
    ...
    
    add_header Access-Control-Allow-Credentials true;
    add_header Access-Control-Allow-Origin $http_origin;
    
        
    location /file {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin $http_origin;
            add_header Access-Control-Allow-Methods $http_access_control_request_method;
            add_header Access-Control-Allow-Credentials true;
            add_header Access-Control-Allow-Headers $http_access_control_request_headers;
            add_header Access-Control-Max-Age 1728000;
            return 204;
        }         
    }
	
    ...
}
複製代碼

網絡協議

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 執行機制

代碼提高(爲了編譯)

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

編譯代碼

V8 編譯 JS 代碼的過程

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

  2. 第一階段是分詞(tokenize),又稱爲詞法分析

  3. 第二階段是解析(parse),又稱爲語法分析

  4. 生成字節碼

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

  5. 執行代碼

高級語言編譯器步驟:

  1. 輸入源程序字符流
  2. 詞法分析
  3. 語法分析
  4. 語義分析
  5. 中間代碼生成
  6. 機器無關代碼優化
  7. 代碼生成
  8. 機器相關代碼優化
  9. 目標代碼生成

執行代碼

  • 執行全局代碼時,建立全局上下文
  • 調用函數時,建立函數上下文
  • 使用 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(); // 4
getNum(); // 5
複製代碼

做用域

全局做用域

對象在代碼中的任何地方都能訪問,其生命週期伴隨着頁面的生命週期。

函數做用域

函數內部定義的變量或者函數,而且定義的變量或者函數只能在函數內部被訪問。函數執行結束以後,函數內部定義的變量會被銷燬。

局部做用域

使用一對大括號包裹的一段代碼,好比函數、判斷語句、循環語句,甚至單獨的一個{}均可以被看做是一個塊級做用域。

做用域鏈

詞法做用域

詞法做用域就是指做用域是由代碼中函數聲明的位置來決定的,因此詞法做用域是靜態的做用域,經過它就可以預測代碼在執行過程當中如何查找標識符。

詞法做用域是代碼階段就決定好的,和函數是怎麼調用的沒有關係。

原型&原型鏈

其實每一個 JS 對象都有 __proto__ 屬性,這個屬性指向了原型。

原型也是一個對象,而且這個對象中包含了不少函數,對於 obj 來講,能夠經過 __proto__ 找到一個原型對象,在該對象中定義了不少函數讓咱們來使用。

原型鏈:

  • Object 是全部對象的爸爸,全部對象均可以經過 __proto__ 找到它
  • Function 是全部函數的爸爸,全部函數均可以經過 __proto__ 找到它
  • 函數的 prototype 是一個對象
  • 對象的 __proto__ 屬性指向原型, __proto__ 將對象和原型鏈接起來組成了原型鏈

V8 工做原理

數據存儲

  • 棧空間:先進後出的數據結構,調用棧,存儲執行上下文,以及存儲原始類型的數據。
  • 堆空間:用數組實現的二叉樹,存儲引用類型。堆空間很大,能存放不少大的數據。存放在堆內存中的對象,變量實際保存的是一個指針,這個指針指向另外一個位置。

原始類型的賦值會完整複製變量值,而引用類型的賦值是複製引用地址。

垃圾回收

  • 回收調用棧內的數據:執行上下文結束且沒有被引用時,則會經過向下移動 記錄當前執行狀態的指針(稱爲 ESP) 來銷燬該函數保存在棧中的執行上下文。

  • 回收堆裏的數據:

    V8 中會把堆分爲新生代和老生代兩個區域,

    新生代中存放的是生存時間短的對象,

    老生代中存放的生存時間久的對象。

    垃圾回收重要術語:

    • 代際假說
      • 大部分對象在內存中存在的時間很短
      • 不死的對象,會活得更久
    • 分代收集

副垃圾回收器:

主要負責新生代的垃圾回收。

這個區域不大,可是垃圾回收比較頻繁。

新生代的垃圾回收算法是 Scavenge 算法。

主要把新生代空間對半劃分爲兩個區域:對象區域,空閒區域。

當對象區域快被寫滿時,則會進行一次垃圾清理。

流程以下:

  1. 對對象區域中的垃圾作標記
  2. 把存活的對象複製到空閒區域中
  3. 把這些對象有序地排列起來
  4. 清理完以後,對象區域會與空閒區域互換

主垃圾回收器:

主垃圾回收器主要負責老生區中的垃圾回收。

除了新生區中晉升的對象,一些大的對象會直接被分配到老生區。

所以老生區中的對象有兩個特色,一個是對象佔用空間大,另外一個是對象存活時間長。

流程以下:

  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:跨站請求僞造。攻擊者會虛構一個後端請求地址,誘導用戶經過某些途徑發送請求。

  • 中間人攻擊:中間人攻擊是攻擊方同時與服務端和客戶端創建起了鏈接,並讓對方認爲鏈接是安全的,可是實際上整個通訊過程都被攻擊者控制了。攻擊者不只能得到雙方的通訊信息,還能修改通訊信息。

    • DNS 欺騙:入侵 DNS 來將用戶訪問目標改成入侵者指定機器
    • 會話劫持:在一次正常的通訊過程當中,攻擊者做爲第三方參與到其中,或者是在數據里加入其餘信息,甚至將雙方的通訊模式暗中改變,即從直接聯繫變成有攻擊者參與的聯繫。

防護措施

  1. 預防 XSS
  • 使用轉義字符過濾 html 代碼

    const escapeHTML = value => {
      if (!value || !value.length) {
        return value;
      }
      return value
        .replace(/&/g, "&")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#39;");
    };
    複製代碼
  • 過濾 SQL 代碼

    const replaceSql = value => {
      if (!value || !value.length) {
        return value;
      }
      return value.replace(/select|update|delete|exec|count|'|"|=|;|>|<|%/gi, "");
    };
    複製代碼
  1. 預防 CSRF

    • 驗證 HTTP Referer 字段
    • 在請求地址中添加 token 並驗證
    • 在 HTTP 頭中自定義屬性並驗證
    • Get 請求不對數據進行修改
    • 接口防跨域處理
    • 不讓第三方網站訪問用戶 cookie
  2. 預防中間人攻擊

  • 對於 DNS 欺騙:檢查本機的 HOSTS 文件
  • 對於會話劫持:使用交換式網絡代替共享式網絡,還必須使用靜態 ARP、捆綁 MAC+IP 等方法來限制欺騙,以及採用認證方式的鏈接等。
  1. 內容安全策略(CSP)

內容安全策略 (CSP) 是一個額外的安全層,用於檢測並削弱某些特定類型的攻擊,包括跨站腳本 (XSS) 和數據注入攻擊等。不管是數據盜取、網站內容污染仍是散發惡意軟件,這些攻擊都是主要的手段。

措施以下:

  • 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">

強緩存

  1. Expires

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

協商緩存

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

  • 服務器響應頭:Last-Modified,Etag
  • 瀏覽器請求頭:If-Modified-Since,If-None-Match

Last-ModifiedIf-Modified-Since 配對。Last-Modified 把 Web 應用最後修改時間告訴客戶端,客戶端下次請求之時會把 If-Modified-Since 的值發生給服務器,服務器由此判斷是否須要從新發送資源,若是不須要則返回 304,若是有則返回 200。這對組合的缺點是隻能精確到秒,並且是根據本地打開時間來記錄的,因此會不許確。

EtagIf-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"

遞歸

遞歸(英語:Recursion),又譯爲遞迴,在數學與計算機科學中,是指在函數的定義中使用函數自身的方法。

例如:

大雄在房裏,用時光電視看着將來的狀況。電視畫面中的那個時候,他正在房裏,用時光電視,看着將來的狀況。電視畫面中的電視畫面的那個時候,他正在房裏,用時光電視,看着將來的狀況……

簡單來講,就是 無限套娃

咱們以斐波那契數列(Fibonacci sequence)爲例,看看輸入結果會爲正無窮的值的狀況下,各類遞歸的狀況。

首先是普通版

const fib1 = n => {
  if (typeof n !== "number") {
    throw new Error("..");
  }
  if (n < 2) {
    return n;
  }
  return fib1(n - 1) + fib1(n - 2);
};
複製代碼

從上面的代碼分析,咱們不難發現,在fib1裏,JS 會不停建立執行上下文,壓入棧內,並且在得出結果前不會銷燬,因此數大了以後容易爆棧。

因此咱們能夠對其進行優化,就是利用 尾調用 進行優化。

尾調用是指函數的最後一步只返回一個純函數的調用,而沒有別的數據佔用引用。代碼以下:

const fib2 = (n, a = 0, b = 1) => {
  if (typeof n !== "number") {
    throw new Error("..");
  }
  if (n === 0) {
    return a;
  }
  return fib2(n - 1, b, a + b);
};
複製代碼

不過很遺憾,在 Chrome 83.0.4103.61 裏仍是會爆。

而後咱們還有備忘錄遞歸法,就是另外申請空間去存儲每次遞歸的值,是個自頂向下的算法。

惋惜,仍是掛了。

不過在一些遞歸問題上,咱們還能夠利用動態規劃(Dynamic programming,簡稱 DP)來解決。

動態規劃是算法裏比較難掌握的一個概念之一,可是基本能用遞歸來解決的問題,都能用動態規劃來解決。

動態規劃背後的基本思想很是簡單。大體上,若要解一個給定問題,咱們須要解其不一樣部分(即子問題),再根據子問題的解以得出原問題的解。

跟備忘錄遞歸恰好相反,是自底向上的算法。具體代碼以下:

const fib3 = n => {
  if (typeof n !== "number") {
    throw new Error("..");
  }
  if (n < 2) {
    return n;
  }
  let a = 0;
  let b = 1;
  while (n--) {
    [a, b] = [b, a + b];
  }
  return a;
};
複製代碼

效果很好,正確輸出了正無窮~

參考資料

  1. 瀏覽器工做原理與實踐
  2. 瀏覽器的運行機制—2.瀏覽器都包含哪些進程?
  3. 「中高級前端面試」JavaScript 手寫代碼無敵祕籍
  4. JavaScript 深拷貝
  5. bailnl/promise
  6. 網絡是怎樣鏈接的?
  7. 瀏覽器工做原理與實踐
  8. 瀏覽器的工做原理:新式網絡瀏覽器幕後揭祕
  9. 內容安全策略( CSP )
  10. 前端面試之道
  11. HTTP 各版本的區別
  12. CORS解決跨域問題(Nginx跨域配置)
  13. 你以爲 Node.js 是單線程這個結論對嗎?
  14. Node 指南
  15. 深刻理解瀏覽器的緩存機制

後記

若是你喜歡探討技術,或者對本文有任何的意見或建議,很是歡迎加魚頭微信好友一塊兒探討,固然,魚頭也很是但願能跟你一塊兒聊生活,聊愛好,談天說地。 魚頭的微信號是:krisChans95 掃碼公衆號,回覆『面試資料』能夠獲取約200M前端高質量面試資料,千萬不要錯過哦。

https://user-gold-cdn.xitu.io/2020/5/23/17240ccfa92ee33d?w=1000&h=480&f=png&s=311000
相關文章
相關標籤/搜索