最近用團隊的帳號發了一篇文章專有釘釘前端面試指南,初衷是但願給你們傳遞一些可能沒有接觸過的知識,其中某些知識可能也超出了前端的範疇,本質是但願給你們提供一些掃盲的思路。可是文章的評論使我意識到你們對於這個文章的抵觸心情很是大。我有很認真的看你們的每一條評論,而後可能過多的解釋也沒有什麼用。我本身也反思可能文章就不該該以面試爲標題進行傳播,由於面試的話它就意味着跟職位以及工做息息相關,更況且我仍是以團隊的名義去發這個文章。在這裏,先跟這些讀完文章體驗不是很好的同窗道個歉。javascript
之前寫文章感受都很開心,寫完發完感受都能給你們帶來一些新的輸入。可是這一次,我感受挺難受的,也確實反思了不少,感受本身以這樣的方式傳播可能有些問題,主要以下:html
這裏再也不過多解釋和糾結面試題的問題了,由於我感受無論在評論中作什麼解釋,不承認的同窗仍是會一如既往的懟上來(挺好的,若是懟完感受本身還能釋放一些小壓力,或許還能適當的給子弈增長一些蒼白解釋的動力)。固然我也很開心不少同窗在評論中求答案,接下來我會好好認真作一期答案,但願可以給你們帶來一些新的輸入,固然答案不可能一會兒作完,也不必定全面或者讓你們感受滿意,或許你們此次的評論又能給我帶來一些學習的機會。前端
舒適提示:這裏儘可能多給出一些知識點,因此不會針對問題進行機械式的回答,可能更多的須要你們自行理解和抽象。其中大部分面試題可能會已文章連接的形式出現,或許是我本身之前寫過的文章,或者是我以爲別人寫的不錯的文章。java
基礎知識主要包含如下幾個方面:node
現代計算機以存儲器爲中心,主要由 CPU、I / O 設備以及主存儲器三大部分組成。各個部分之間經過總線進行鏈接通訊,具體以下圖所示: 上圖是一種多總線結構的示意圖,CPU、主存以及 I / O 設備之間的全部數據都是經過總線進行並行傳輸,使用局部總線是爲了提升 CPU 的吞吐量(CPU 不須要直接跟 I / O 設備通訊),而使用高速總線(更貼近 CPU)和 DMA 總線則是爲了提高高速 I / O 設備(外設存儲器、局域網以及多媒體等)的執行效率。ios
主存包括隨機存儲器 RAM 和只讀存儲器 ROM,其中 ROM 又能夠分爲 MROM(一次性)、PROM、EPROM、EEPROM 。ROM 中存儲的程序(例如啓動程序、固化程序)和數據(例如常量數據)在斷電後不會丟失。RAM 主要分爲靜態 RAM(SRAM) 和動態 RAM(DRAM) 兩種類型(DRAM 種類不少,包括 SDRAM、RDRAM、CDRAM 等),斷電後數據會丟失,主要用於存儲臨時程序或者臨時變量數據。 DRAM 通常訪問速度相對較慢。因爲現代 CPU 讀取速度要求相對較高,所以在 CPU 內核中都會設計 L一、L2 以及 L3 級別的多級高速緩存,這些緩存基本是由 SRAM 構成,通常訪問速度較快。git
高級程序設計語言不能直接被計算機理解並執行,須要經過翻譯程序將其轉換成特定處理器上可執行的指令,計算機 CPU 的簡單工做原理以下所示: CPU 主要由控制單元、運算單元和存儲單元組成(注意忽略了中斷系統),各自的做用以下:程序員
除此以外,計算機系統執行程序指令時須要花費時間,其中取出一條指令並執行這條指令的時間叫指令週期。指令週期能夠分爲若干個階段(取指週期、間址週期、執行週期和中斷週期),每一個階段主要完成一項基本操做,完成基本操做的時間叫機器週期。機器週期是時鐘週期的分頻,例如最經典的 8051 單片機的機器週期爲 12 個時鐘週期。時鐘週期是 CPU 工做的基本時間單位,也能夠稱爲節拍脈衝或 T 週期(CPU 主頻的倒數) 。假設 CPU 的主頻是 1 GHz(1 Hz 表示每秒運行 1 次),那麼表示時鐘週期爲 1 / 109 s。理論上 CPU 的主頻越高,程序指令執行的速度越快。es6
上圖右側主存中的指令是 CPU 能夠支持的處理命令,通常包含算術指令(加和減)、邏輯指令(與、或和非)、數據指令(移動、輸入、刪除、加載和存儲)、流程控制指令以及程序結束指令等,因爲 CPU 只能識別二進制碼,所以指令是由二進制碼組成。除此以外,指令的集合稱爲指令集(例如彙編語言就是指令集的一種表現形式),常見的指令集有精簡指令集(ARM)和複雜指令集(Inter X86)。通常指令集決定了 CPU 處理器的硬件架構,規定了處理器的相應操做。github
早期的計算機只有機器語言時,程序設計必須用二進制數(0 和 1)來編寫程序,而且要求程序員對計算機硬件和指令集很是瞭解,編程的難度較大,操做極易出錯。爲了解決機器語言的編程問題,慢慢開始出現了符號式的彙編語言(採用 ADD、SUB、MUL、DIV 等符號表明加減乘除)。爲了使得計算機能夠識別彙編語言,須要將彙編語言翻譯成機器可以識別的機器語言(處理器的指令集): 因爲每一種機器的指令系統不一樣,須要不一樣的彙編語言程序與之匹配,所以程序員每每須要針對不一樣的機器瞭解其硬件結構和指令系統。爲了能夠抹平不一樣機器的指令系統,使得程序員能夠更加關注程序設計自己,前後出現了各類面向問題的高級程序設計語言,例如 BASIC 和 C,具體過程以下圖所示:
高級程序語言會先翻譯成彙編語言或者其餘中間語言,而後再根據不一樣的機器翻譯成機器語言進行執行。除此以外,彙編語言虛擬機和機器語言機器之間還存在一層操做系統虛擬機,主要用於控制和管理操做系統的所有硬件和軟件資源(隨着超大規模集成電路技術的不斷髮展,一些操做系統的軟件功能逐步由硬件來替換,例如目前的操做系統已經實現了部分程序的固化,簡稱固件,將程序永久性的存儲在 ROM 中)。機器語言機器還能夠繼續分解成微程序機器,將每一條機器指令翻譯成一組微指令(微程序)進行執行。
上述虛擬機所提供的語言轉換程序被稱爲編譯器,主要做用是將某種語言編寫的源程序轉換成一個等價的機器語言程序,編譯器的做用以下圖所示: 例如 C 語言,能夠先經過 gcc 編譯器生成 Linux 和 Windows 下的目標 .o 和 .obj 文件(object 文件,即目標文件),而後將目標文件與底層系統庫文件、應用程序庫文件以及啓動文件連接成可執行文件在目標機器上執行。
舒適提示:感興趣的同窗能夠了解一下 ARM 芯片的程序運行原理,包括使用 IDE 進行程序的編譯(IDE 內置編譯器,主流編譯器包含 ARMCC、IAR 以及 GCC FOR ARM 等,其中一些編譯器僅僅隨着 IDE 進行捆綁發佈,不提供獨立使用的能力,而一些編譯器則隨着 IDE 進行發佈的同時,還提供命令行接口的獨立使用方式)、經過串口進行程序下載(下載到芯片的代碼區初始啓動地址映射的存儲空間地址)、啓動的存儲空間地址映射(包括系統存儲器、閃存 FLASH、內置 SRAM 等)、芯片的程序啓動模式引腳 BOOT 的設置(例如調試代碼時經常選擇內置 SRAM、真正程序運行的時候選擇閃存 FLASH)等。
若是某種高級語言或者應用語言(例如用於人工智能的計算機設計語言)轉換的目標語言不是特定計算機的彙編語言,而是面向另外一種高級程序語言(不少研究性的編譯器將 C 做爲目標語言),那麼還須要將目標高級程序語言再進行一次額外的編譯才能獲得最終的目標程序,這種編譯器可稱爲源到源的轉換器。
除此以外,有些程序設計語言將編譯的過程和最終轉換成目標程序進行執行的過程混合在一塊兒,這種語言轉換程序一般被稱爲解釋器,主要做用是將某種語言編寫的源程序做爲輸入,將該源程序執行的結果做爲輸出,解釋器的做用以下圖所示:
解釋器和編譯器有不少類似之處,都須要對源程序進行分析,並轉換成目標機器可識別的機器語言進行執行。只是解釋器是在轉換源程序的同時立馬執行對應的機器語言(轉換和執行的過程不分離),而編譯器得先把源程序所有轉換成機器語言併產生目標文件,而後將目標文件寫入相應的程序存儲器進行執行(轉換和執行的過程分離)。例如 Perl、Scheme、APL 使用解釋器進行轉換, C、C++ 則使用編譯器進行轉換,而 Java 和 JavaScript 的轉換既包含了編譯過程,也包含了解釋過程。
JavaScript 中的數組存儲大體須要分爲兩種狀況:
舒適提示:能夠想象一下連續的內存空間只須要根據索引(指針)直接計算存儲位置便可。若是是哈希映射那麼首先須要計算索引值,而後若是索引值有衝突的場景下還須要進行二次查找(須要知道哈希的存儲方式)。
閱讀連接:面試分享:兩年工做經驗成功面試阿里P6總結 - 瞭解 Event Loop 嗎?
編譯器的設計是一個很是龐大和複雜的軟件系統設計,在真正設計的時候須要解決兩個相對重要的問題:
爲了解決上述兩項問題,編譯器的設計最終被分解成前端(注意這裏所說的不是 Web 前端)和後端兩個編譯階段,前端用於解決第一個問題,然後端用於解決第二個問題,具體以下圖所示: 上圖中的中間表示(Intermediate Representation,IR)是程序結構的一種表現方式,它會比 AST(後續講解)更加接近彙編語言或者指令集,同時也會保留源程序中的一些高級信息,除此以外 ,它的種類不少,包括三地址碼(Three Address Code, TAC)、靜態單賦值形式(Static Single Assignment Form, SSA)以及基於棧的 IR 等,具體做用包括:
因爲 IR 能夠進行多趟迭代進行程序優化,所以在編譯器中可插入一個新的優化階段,以下圖所示:
優化器能夠對 IR 處理一遍或者多遍,從而生成更快執行速度(例如找到循環中不變的計算並對其進行優化從而減小運算次數)或者更小體積的目標程序,也可能用於產生更少異常或者更低功耗的目標程序。除此以外,前端和後端內部還能夠細分爲多個處理步驟,具體以下圖所示:
優化器中的每一遍優化處理均可以使用一個或多個優化技術來改進代碼,每一趟處理最終都是讀寫 IR 的操做,這樣不只僅可使得優化能夠更加高效,同時也能夠下降優化的複雜度,還提升了優化的靈活性,可使得編譯器配置不一樣的優化選項,達到組合優化的效果。
閱讀連接:基於Vue實現一個簡易MVVM - 觀察者模式和發佈/訂閱模式
編程範式(Programming paradigm)是指計算機編程的基本風格或者典型模式,能夠簡單理解爲編程學科中實踐出來的具備哲學和理論依據的一些經典原型。常見的編程範式有:
閱讀連接::若是你對於編程範式的定義相對模糊,能夠繼續閱讀 What is the precise definition of programming paradigm? 瞭解更多。
不一樣的語言能夠支持多種不一樣的編程範式,例如 C 語言支持 POP 範式,C++ 和 Java 語言支持 OOP 範式,Swift 語言則能夠支持 FP 範式,而 Web 前端中的 JavaScript 能夠支持上述列出的全部編程範式。
顧名思義,函數式編程是使用函數來進行高效處理數據或數據流的一種編程方式。在數學中,函數的三要素是定義域、值域和**對應關係。假設 A、B 是非空數集,對於集合 A 中的任意一個數 x,在集合 B 中都有惟一肯定的數 f(x) 和它對應,那麼能夠將 f 稱爲從 A 到 B 的一個函數,記做:y = f(x)。在函數式編程中函數的概念和數學函數的概念相似,主要是描述形參 x 和返回值 y 之間的對應關係,**以下圖所示:
舒適提示:圖片來自於簡明 JavaScript 函數式編程——入門篇。
在實際的編程中,能夠將各類明確對應關係的函數進行傳遞、組合從而達處處理數據的最終目的。在此過程當中,咱們的關注點不在於如何去實現**對應關係,**而在於如何將各類已有的對應關係進行高效聯動,從而可快速進行數據轉換,達到最終的數據處理目的,提供開發效率。
簡單示例
儘管你對函數式編程的概念有所瞭解,可是你仍然不知道函數式編程到底有什麼特色。這裏咱們仍然拿 OOP 編程範式來舉例,假設但願經過 OOP 編程來解決數學的加減乘除問題:
class MathObject {
constructor(private value: number) {}
public add(num: number): MathObject {
this.value += num;
return this;
}
public multiply(num: number): MathObject {
this.value *= num;
return this;
}
public getValue(): number {
return this.value;
}
}
const a = new MathObject(1);
a.add(1).multiply(2).add(a.multiply(2).getValue());
複製代碼
咱們但願經過上述程序來解決 (1 + 2) * 2 + 1 * 2 的問題,但實際上計算出來的結果是 24,由於在代碼內部有一個 this.value
的狀態值須要跟蹤,這會使得結果不符合預期。 接下來咱們採用函數式編程的方式:
function add(a: number, b: number): number {
return a + b;
}
function multiply(a: number, b: number): number {
return a * b;
}
const a: number = 1;
const b: number = 2;
add(multiply(add(a, b), b), multiply(a, b));
複製代碼
以上程序計算的結果是 8,徹底符合預期。咱們知道了 add
和 multiply
兩個函數的實際對應關係,經過將對應關係進行有效的組合和傳遞,達到了最終的計算結果。除此以外,這兩個函數還能夠根據數學定律得出更優雅的組合方式:
add(multiply(add(a, b), b), multiply(a, b));
// 根據數學定律分配律:a * b + a * c = a * (b + c),得出:
// (a + b) * b + a * b = (2a + b) * b
// 簡化上述函數的組合方式
multiply(add(add(a, a), b), b);
複製代碼
咱們徹底不須要追蹤相似於 OOP 編程範式中可能存在的內部狀態數據,事實上對於數學定律中的結合律、交換律、同一概以及分配律,上述的函數式編程代碼足能夠勝任。
原則
經過上述簡單的例子能夠發現,要實現高可複用的函數**(對應關係)**,必定要遵循某些特定的原則,不然在使用的時候可能沒法進行高效的傳遞和組合,例如
若是你以前常常進行無原則性的代碼設計,那麼在設計過程當中可能會出現各類出乎意料的問題(這是爲何新手總是出現一些稀奇古怪問題的主要緣由)。函數式編程能夠有效的經過一些原則性的約束使你設計出更加健壯和優雅的代碼,而且在不斷的實踐過程當中進行經驗式疊加,從而提升開發效率。
特色
雖然咱們在使用函數的過程當中更多的再也不關注函數如何實現(對應關係),可是真正在使用和設計函數的時候須要注意如下一些特色:
聲明式
咱們之前設計的代碼一般是命令式編程方式,這種編程方式每每注重具體的實現的過程(對應關係),而函數式編程則採用聲明式的編程方式,每每注重如何去組合已有的**對應關係。**簡單舉個例子:
// 命令式
const array = [0.8, 1.7, 2.5, 3.4];
const filterArray = [];
for (let i = 0; i < array.length; i++) {
const integer = Math.floor(array[i]);
if (integer < 2) {
continue;
}
filterArray.push(integer);
}
// 聲明式
// map 和 filter 不會修改原有數組,而是產生新的數組返回
[0.8, 1.7, 2.5, 3.4].map((item) => Math.floor(item)).filter((item) => item > 1);
複製代碼
命令式代碼一步一步的告訴計算機須要執行哪些語句,須要關心變量的實例化狀況、循環的具體過程以及跟蹤變量狀態的變化過程。聲明式代碼更多的再也不關心代碼的具體執行過程,而是採用表達式的組合變換去處理問題,再也不強調怎麼作,而是指明**作什麼。**聲明式編程方式能夠將咱們設計代碼的關注點完全從過程式解放出來,從而提升開發效率。
一等公民
在 JavaScript 中,函數的使用很是靈活,例如能夠對函數進行如下操做:
interface IHello {
(name: string): string;
key?: string;
arr?: number[];
fn?(name: string): string;
}
// 函數聲明提高
console.log(hello instanceof Object); // true
// 函數聲明提高
// hello 和其餘引用類型的對象同樣,都有屬性和方法
hello.key = 'key';
hello.arr = [1, 2];
hello.fn = function (name: string) {
return `hello.fn, ${name}`;
};
// 函數聲明提高
// 注意函數表達式不能在聲明前執行,例如不能在這裏使用 helloCopy('world')
hello('world');
// 函數
// 建立新的函數對象,將函數的引用指向變量 hello
// hello 僅僅是變量的名稱
function hello(name: string): string {
return `hello, ${name}`;
}
console.log(hello.key); // key
console.log(hello.arr); // [1,2]
console.log(hello.name); // hello
// 函數表達式
const helloCopy: IHello = hello;
helloCopy('world');
function transferHello(name: string, hello: Hello) {
return hello('world');
}
// 把函數對象看成實參傳遞
transferHello('world', helloCopy);
// 把匿名函數看成實參傳遞
transferHello('world', function (name: string) {
return `hello, ${name}`;
});
複製代碼
經過以上示例能夠看出,函數繼承至對象並擁有對象的特性。在 JavaScript 中能夠對函數進行參數傳遞、變量賦值或數組操做等等,所以把函數稱爲一等公民。函數式編程的核心就是對函數進行組合或傳遞,JavaScript 中函數這種靈活的特性是知足函數式編程的重要條件。
純函數
純函數是是指在相同的參數調用下,函數的返回值惟一不變。這跟數學中函數的映射關係相似,一樣的 x 不可能映射多個不一樣的 y。使用函數式編程會使得函數的調用很是穩定,從而下降 Bug 產生的機率。固然要實現純函數的這種特性,須要函數不能包含如下一些反作用:
從以上常見的一些反作用能夠看出,純函數的實現須要遵循最小意外原則,爲了確保函數的穩定惟一的輸入和輸出,儘可能應該避免與函數外部的環境進行任何交互行爲,從而防止外部環境對函數內部產生沒法預料的影響。純函數的實現應該自給自足,舉幾個例子:
// 若是使用 const 聲明 min 變量(基本數據類型),則能夠保證如下函數的純粹性
let min: number = 1;
// 非純函數
// 依賴外部環境變量 min,一旦 min 發生變化則輸入和返回不惟一
function isEqual(num: number): boolean {
return num === min;
}
// 純函數
function isEqual(num: number): boolean {
return num === 1;
}
// 非純函數
function request<T, S>(url: string, params: T): Promise<S> {
// 會產生請求成功和請求失敗兩種結果,返回的結果可能不惟一
return $.getJson(url, params);
}
// 純函數
function request<T, S>(url: string, params: T) : () => Promise<S> {
return function() {
return $.getJson(url, params);
}
}
複製代碼
純函數的特性使得函數式編程具有如下特性:
可緩存性和可測試性基於純函數輸入輸出惟一不變的特性,可移植性則主要基於純函數不依賴外部環境的特性。這裏舉一個可緩存的例子:
interface ICache<T> {
[arg: string]: T;
}
interface ISquare<T> {
(x: T): T;
}
// 簡單的緩存函數(忽略通用性和健壯性)
function memoize<T>(fn: ISquare<T>): ISquare<T> {
const cache: ICache<T> = {};
return function (x: T) {
const arg: string = JSON.stringify(x);
cache[arg] = cache[arg] || fn.call(fn, x);
return cache[arg];
};
}
// 純函數
function square(x: number): number {
return x * x;
}
const memoSquare = memoize<number>(square);
memoSquare(4);
// 不會再次調用純函數 square,而是直接從緩存中獲取值
// 因爲輸入和輸出的惟一性,獲取緩存結果可靠穩定
// 提高代碼的運行效率
memoSquare(4);
複製代碼
無狀態和數據不可變
在函數式編程的簡單示例中已經能夠清晰的感覺到函數式編程絕對不能依賴內部狀態,而在純函數中則說明了函數式編程不能依賴外部的環境或狀態,由於一旦依賴的狀態變化,不能保證函數根據對應關係所計算的返回值由於狀態的變化仍然保持不變。
這裏單獨講解一下數據不可變,在 JavaScript 中有不少數組操做的方法,舉個例子:
const arr = [1, 2, 3];
console.log(arr.slice(0, 2)); // [1, 2]
console.log(arr); // [1, 2, 3]
console.log(arr.slice(0, 2)); // [1, 2]
console.log(arr); // [1, 2, 3]
console.log(arr.splice(0, 1)); // [1]
console.log(arr); // [2, 3]
console.log(arr.splice(0, 1)); // [2]
console.log(arr); // [3]
複製代碼
這裏的 slice
方法屢次調用都不會改變原有數組,且會產生相同的輸出。而 splice
每次調用都在修改原數組,且產生的輸出也不相同。 在函數式編程中,這種會改變原有數據的函數已經再也不是純函數,應該儘可能避免使用。
閱讀連接:若是想要了解更深刻的函數式編程知識點,能夠額外閱讀函數式編程指北。
響應式編程是一種基於觀察者(發佈 / 訂閱)模式而且面向異步(Asynchronous)數據流(Data Stream)和變化傳播的聲明式編程範式。響應式編程主要適用的場景包含:
The direction
CSS property sets the direction of text, table columns, and horizontal overflow. Use rtl
for languages written from right to left (like Hebrew or Arabic), and ltr
for those written from left to right (like English and most other languages).
具體查看:developer.mozilla.org/en-US/docs/…
The console
object provides access to the browser's debugging console (e.g. the Web console in Firefox). The specifics of how it works varies from browser to browser, but there is a de facto set of features that are typically provided.
這裏列出一些我經常使用的 API:
具體查看:developer.mozilla.org/en-US/docs/…
在 JavaScript 中利用事件循環機制(Event Loop)能夠在單線程中實現非阻塞式、異步的操做。例如
咱們重點來看一下經常使用的幾種編程方式(Callback、Promise、Generator、Async)在語法糖上帶來的優劣對比。
Callback
Callback(回調函數)是在 Web 前端開發中常常會使用的編程方式。這裏舉一個經常使用的定時器示例:
export interface IObj {
value: string;
deferExec(): void;
deferExecAnonymous(): void;
console(): void;
}
export const obj: IObj = {
value: 'hello',
deferExecBind() {
// 使用箭頭函數可達到同樣的效果
setTimeout(this.console.bind(this), 1000);
},
deferExec() {
setTimeout(this.console, 1000);
},
console() {
console.log(this.value);
},
};
obj.deferExecBind(); // hello
obj.deferExec(); // undefined
複製代碼
回調函數常常會由於調用環境的變化而致使 this
的指向性變化。除此以外,使用回調函數來處理多個繼發的異步任務時容易致使回調地獄(Callback Hell):
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
fs.readFile(fileC, 'utf-8', function (err, data) {
fs.readFile(fileD, 'utf-8', function (err, data) {
// 假設在業務中 fileD 的讀寫依次依賴 fileA、fileB 和 fileC
// 或者常常也能夠在業務中看到多個 HTTP 請求的操做有先後依賴(繼發 HTTP 請求)
// 這些異步任務之間縱向嵌套強耦合,沒法進行橫向複用
// 若是某個異步發生變化,那它的全部上層或下層回調可能都須要跟着變化(好比 fileA 和 fileB 的依賴關係倒置)
// 所以稱這種現象爲 回調地獄
// ....
});
});
});
});
複製代碼
回調函數不能經過 return
返回數據,好比咱們但願調用帶有回調參數的函數並返回異步執行的結果時,只能經過再次回調的方式進行參數傳遞:
// 但願延遲 3s 後執行並拿到結果
function getAsyncResult(result: number) {
setTimeout(() => {
return result * 3;
}, 1000);
}
// 儘管這是常規的編程思惟方式
const result = getAsyncResult(3000);
// 可是打印 undefined
console.log('result: ', result);
function getAsyncResultWithCb(result: number, cb: (result: number) => void) {
setTimeout(() => {
cb(result * 3);
}, 1000);
}
// 經過回調的形式獲取結果
getAsyncResultWithCb(3000, (result) => {
console.log('result: ', result); // 9000
});
複製代碼
對於 JavaScript 中標準的異步 API 可能沒法經過在外部進行 try...catch...
的方式進行錯誤捕獲:
try {
setTimeout(() => {
// 下述是異常代碼
// 你能夠在回調函數的內部進行 try...catch...
console.log(a.b.c)
}, 1000)
} catch(err) {
// 這裏不會執行
// 進程會被終止
console.error(err)
}
複製代碼
上述示例講述的都是 JavaScript 中標準的異步 API ,若是使用一些三方的異步 API 而且提供了回調能力時,這些 API 多是非受信的,在真正使用的時候會由於執行反轉(回調函數的執行權在三方庫中)致使如下一些問題:
舉個簡單的例子:
interface ILib<T> {
params: T;
emit(params: T): void;
on(callback: (params: T) => void): void;
}
// 假設如下是一個三方庫,併發布成了npm 包
export const lib: ILib<string> = {
params: '',
emit(params) {
this.params = params;
},
on(callback) {
try {
// callback 回調執行權在 lib 上
// lib 庫能夠決定回調執行屢次
callback(this.params);
callback(this.params);
callback(this.params);
// lib 庫甚至能夠決定回調延遲執行
// 異步執行回調函數
setTimeout(() => {
callback(this.params);
}, 3000);
} catch (err) {
// 假設 lib 庫的捕獲沒有拋出任何異常信息
}
},
};
// 開發者引入 lib 庫開始使用
lib.emit('hello');
lib.on((value) => {
// 使用者但願 on 裏的回調只執行一次
// 這裏的回調函數的執行時機是由三方庫 lib 決定
// 實際上打印四次,而且其中一次是異步執行
console.log(value);
});
lib.on((value) => {
// 下述是異常代碼
// 可是執行下述代碼不會拋出任何異常信息
// 開發者沒法感知本身的代碼設計錯誤
console.log(value.a.b.c)
});
複製代碼
Promise
Callback 的異步操做形式除了會形成回調地獄,還會形成難以測試的問題。ES6 中的 Promise (基於 Promise A + 規範的異步編程解決方案)利用有限狀態機的原理來解決異步的處理問題,Promise 對象提供了統一的異步編程 API,它的特色以下:
pending
(進行中)、 fulfilled
(已成功)和 rejected
(已失敗) ,只有 Promise 對象自己的異步操做結果能夠決定當前的執行狀態,任何其餘的操做沒法改變狀態的結果pending
(進行中)變爲 fulfilled
(已成功)或從 pending
(進行中)變爲 rejected
(已失敗)舒適提示:有限狀態機提供了一種優雅的解決方式,異步的處理自己能夠經過異步狀態的變化來觸發相應的操做,這會比回調函數在邏輯上的處理更加合理,也能夠下降代碼的複雜度。
Promise 對象的執行狀態不可變示例以下:
const promise = new Promise<number>((resolve, reject) => {
// 狀態變動爲 fulfilled 並返回結果 1 後不會再變動狀態
resolve(1);
// 不會變動狀態
reject(4);
});
promise
.then((result) => {
// 在 ES 6 中 Promise 的 then 回調執行是異步執行(微任務)
// 在當前 then 被調用的那輪事件循環(Event Loop)的末尾執行
console.log('result: ', result);
})
.catch((error) => {
// 不執行
console.error('error: ', error);
});
複製代碼
假設要實現兩個繼發的 HTTP 請求,第一個請求接口返回的數據是第二個請求接口的參數,使用回調函數的實現方式以下所示(這裏使用 setTimeout
來指代異步請求):
// 回調地獄
const doubble = (result: number, callback: (finallResult: number) => void) => {
// Mock 第一個異步請求
setTimeout(() => {
// Mock 第二個異步請求(假設第二個請求的參數依賴第一個請求的返回結果)
setTimeout(() => {
callback(result * 2);
}, 2000);
}, 1000);
};
doubble(1000, (result) => {
console.log('result: ', result);
});
複製代碼
舒適提示:繼發請求的依賴關係很是常見,例如人員基本信息管理系統的開發中,常常須要先展現組織樹結構,並默認加載第一個組織下的人員列表信息。
若是採用 Promise 的處理方式則能夠規避上述常見的回調地獄問題:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Mock 異步請求
// 將 resolve 改爲 reject 會被 catch 捕獲
setTimeout(() => resolve(result), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Mock 異步請求
// 將 resolve 改爲 reject 會被 catch 捕獲
setTimeout(() => resolve(result * 2), 1000);
});
};
firstPromise(1000)
.then((result) => {
return nextPromise(result);
})
.then((result) => {
// 2s 後打印 2000
console.log('result: ', result);
})
// 任何一個 Promise 到達 rejected 狀態都能被 catch 捕獲
.catch((err) => {
console.error('err: ', err);
});
複製代碼
Promise 的錯誤回調能夠同時捕獲 firstPromise
和 nextPromise
兩個函數的 rejected
狀態。接下來考慮如下調用場景:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Mock 異步請求
setTimeout(() => resolve(result), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Mock 異步請求
setTimeout(() => resolve(result * 2), 1000);
});
};
firstPromise(1000)
.then((result) => {
nextPromise(result).then((result) => {
// 後打印
console.log('nextPromise result: ', result);
});
})
.then((result) => {
// 先打印
// 因爲上一個 then 沒有返回值,這裏打印 undefined
console.log('firstPromise result: ', result);
})
.catch((err) => {
console.error('err: ', err);
});
複製代碼
首先 Promise 能夠註冊多個 then
(放在一個執行隊列裏),而且這些 then
會根據上一次返回值的結果依次執行。除此以外,各個 Promise 的 then
執行互不干擾。 咱們將示例進行簡單的變換:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Mock 異步請求
setTimeout(() => resolve(result), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Mock 異步請求
setTimeout(() => resolve(result * 2), 1000);
});
};
firstPromise(1000)
.then((result) => {
// 返回了 nextPromise 的 then 執行後的結果
return nextPromise(result).then((result) => {
return result;
});
})
// 接着 nextPromise 的 then 執行的返回結果繼續執行
.then((result) => {
// 2s 後打印 2000
console.log('nextPromise result: ', result);
})
.catch((err) => {
console.error('err: ', err);
});
複製代碼
上述例子中的執行結果是由於 then
的執行會返回一個新的 Promise 對象,而且若是 then
執行後返回的仍然是 Promise 對象,那麼下一個 then
的鏈式調用會等待該 Promise 對象的狀態發生變化後纔會調用(能獲得這個 Promise 處理的結果)。接下來重點看下 Promise 的錯誤處理:
const promise = new Promise<string>((resolve, reject) => {
// 下述是異常代碼
console.log(a.b.c);
resolve('hello');
});
promise
.then((result) => {
console.log('result: ', result);
})
// 去掉 catch 仍然會拋出錯誤,但不會退出進程終止腳本執行
.catch((err) => {
// 執行
// ReferenceError: a is not defined
console.error(err);
});
setTimeout(() => {
// 繼續執行
console.log('hello world!');
}, 2000);
複製代碼
從上述示例能夠看出 Promise 的錯誤不會影響其餘代碼的執行,只會影響 Promise 內部的代碼自己,由於Promise 會在內部對錯誤進行異常捕獲,從而保證總體代碼執行的穩定性。Promise 還提供了其餘的一些 API 方便多任務的執行,包括
Promise.all
:適合多個異步任務併發執行但不容許其中任何一個任務失敗Promise.race
:適合多個異步任務搶佔式執行Promise.allSettled
:適合多個異步任務併發執行但容許某些任務失敗Promise 相對於 Callback 對於異步的處理更加優雅,而且能力也更增強大, 可是也存在一些自身的缺點:
try...catch...
的形式進行錯誤捕獲(Promise 內部捕獲了錯誤)舒適提示:手寫 Promise 是面試官很是喜歡的一道筆試題,本質是但願面試者可以經過底層的設計正確瞭解 Promise 的使用方式,若是你對 Promise 的設計原理不熟悉,能夠深刻了解一下或者手動設計一個。
Generator
Promise 解決了 Callback 的回調地獄問題,但也形成了代碼冗餘,若是一些異步任務不支持 Promise 語法,就須要進行一層 Promise 封裝。Generator 將 JavaScript 的異步編程帶入了一個全新的階段,它使得異步代碼的設計和執行看起來和同步代碼一致。Generator 使用的簡單示例以下:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
// 在 Generator 函數裏執行的異步代碼看起來和同步代碼一致
function* gen(result: number): Generator<Promise<number>, Promise<number>, number> {
// 異步代碼
const firstResult = yield firstPromise(result)
console.log('firstResult: ', firstResult) // 2
// 異步代碼
const nextResult = yield nextPromise(firstResult)
console.log('nextResult: ', nextResult) // 6
return nextPromise(firstResult)
}
const g = gen(1)
// 手動執行 Generator 函數
g.next().value.then((res: number) => {
// 將 firstPromise 的返回值傳遞給第一個 yield 表單式對應的 firstResult
return g.next(res).value
}).then((res: number) => {
// 將 nextPromise 的返回值傳遞給第二個 yield 表單式對應的 nextResult
return g.next(res).value
})
複製代碼
經過上述代碼,能夠看出 Generator 相對於 Promise 具備如下優點:
next
能夠產生不一樣的狀態信息,也能夠經過 return
結束函數的執行狀態,相對於 Promise 的 resolve
不可變狀態更加豐富 next
能夠不停的改變狀態使得 yield
得以繼續執行的代碼能夠變得很是有規律,例如從上述的手動執行 Generator 函數能夠看出,徹底能夠將其封裝成一個自動執行的執行器,具體以下所示:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
type Gen = Generator<Promise<number>, Promise<number>, number>
function* gen(): Gen {
const firstResult = yield firstPromise(1)
console.log('firstResult: ', firstResult) // 2
const nextResult = yield nextPromise(firstResult)
console.log('nextResult: ', nextResult) // 6
return nextPromise(firstResult)
}
// Generator 自動執行器
function co(gen: () => Gen) {
const g = gen()
function next(data: number) {
const result = g.next(data)
if(result.done) {
return result.value
}
result.value.then(data => {
// 經過遞歸的方式處理相同的邏輯
next(data)
})
}
// 第一次調用 next 主要用於啓動 Generator 函數
// 內部指針會從函數頭部開始執行,直到遇到第一個 yield 表達式
// 所以第一次 next 傳遞的參數沒有任何含義(這裏傳遞只是爲了防止 TS 報錯)
next(0)
}
co(gen)
複製代碼
舒適提示:TJ Holowaychuk 設計了一個 Generator 自動執行器 Co,使用 Co 的前提是
yield
命令後必須是 Promise 對象或者 Thunk 函數。Co 還能夠支持併發的異步處理,具體可查看官方的 API 文檔。
須要注意的是 Generator 函數的返回值是一個 Iterator 遍歷器對象,具體以下所示:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
type Gen = Generator<Promise<number>>;
function* gen(): Gen {
yield firstPromise(1);
yield nextPromise(2);
}
// 注意使用 next 是繼發執行,而這裏是併發執行
Promise.all([...gen()]).then((res) => {
console.log('res: ', res);
});
for (const promise of gen()) {
promise.then((res) => {
console.log('res: ', res);
});
}
複製代碼
Generator 函數的錯誤處理相對複雜一些,極端狀況下須要對執行和 Generator 函數進行雙重錯誤捕獲,具體以下所示:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// 須要注意這裏的reject 沒有被捕獲
setTimeout(() => reject(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
type Gen = Generator<Promise<number>>;
function* gen(): Gen {
try {
yield firstPromise(1);
yield nextPromise(2);
} catch (err) {
console.error('Generator 函數錯誤捕獲: ', err);
}
}
try {
const g = gen();
g.next();
// 返回 Promise 後還須要經過 Promise.prototype.catch 進行錯誤捕獲
g.next();
// Generator 函數錯誤捕獲
g.throw('err');
// 執行器錯誤捕獲
g.throw('err');
} catch (err) {
console.error('執行錯誤捕獲: ', err);
}
複製代碼
在使用 g.throw
的時候還須要注意如下一些事項:
g.next
,則 g.throw
不會在 Gererator 函數中被捕獲(由於執行指針沒有啓動 Generator 函數的執行),此時能夠在執行處進行執行錯誤捕獲Async
Async 是 Generator 函數的語法糖,相對於 Generator 而言 Async 的特性以下:
yield
命令無約束:在 Generator 中使用 Co 執行器時 yield
後必須是 Promise 對象或者 Thunk 函數,而 Async 語法中的 await
後能夠是 Promise 對象或者原始數據類型對象、數字、字符串、布爾值等(此時會對其進行 Promise.resolve()
包裝處理) async
函數的返回值是 Promise 對象(返回原始數據類型會被 Promise 進行封裝), 所以還能夠做爲 await
的命令參數,相對於 Generator 返回 Iterator 遍歷器更加簡潔實用舉個簡單的示例:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
const firstResult = await firstPromise(1);
// 1s 後打印 2
console.log('firstResult: ', firstResult);
// 等待 firstPromise 的狀態發生變化後執行
const nextResult = await nextPromise(firstResult);
// 2s 後打印 6
console.log('nextResult: ', nextResult);
return nextResult;
}
co();
co().then((res) => {
console.log('res: ', res); // 6
});
複製代碼
經過上述示例能夠看出,async
函數的特性以下:
async
函數後返回的是一個 Promise 對象,經過 then
回調能夠拿到 async 函數內部 return
語句的返回值 async
函數後返回的 Promise 對象必須等待內部全部 await
對應的 Promise 執行完(這使得 async
函數多是阻塞式執行)後纔會發生狀態變化,除非中途遇到了 return
語句await
命令後若是是 Promise 對象,則返回 Promise 對象處理後的結果,若是是原始數據類型,則直接返回原始數據類型上述代碼是阻塞式執行,nextPromise
須要等待 firstPromise
執行完成後才能繼續執行,若是但願二者可以併發執行,則能夠進行下述設計:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
return await Promise.all([firstPromise(1), nextPromise(1)]);
}
co().then((res) => {
console.log('res: ', res); // [2,3]
});
複製代碼
除了使用 Promise 自帶的併發執行 API,也能夠經過讓全部的 Promise 提早併發執行來處理:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
console.log('firstPromise');
setTimeout(() => resolve(result * 2), 10000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
console.log('nextPromise');
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
// 執行 firstPromise
const first = firstPromise(1);
// 和 firstPromise 同時執行 nextPromise
const next = nextPromise(1);
// 等待 firstPromise 結果回來
const firstResult = await first;
console.log('firstResult: ', firstResult);
// 等待 nextPromise 結果回來
const nextResult = await next;
console.log('nextResult: ', nextResult);
return nextResult;
}
co().then((res) => {
console.log('res: ', res); // 3
});
複製代碼
Async 的錯誤處理相對於 Generator 會更加簡單,具體示例以下所示:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Promise 決斷錯誤
setTimeout(() => reject(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
const firstResult = await firstPromise(1);
console.log('firstResult: ', firstResult);
const nextResult = await nextPromise(1);
console.log('nextResult: ', nextResult);
return nextResult;
}
co()
.then((res) => {
console.log('res: ', res);
})
.catch((err) => {
console.error('err: ', err); // err: 2
});
複製代碼
async
函數內部拋出的錯誤,會致使函數返回的 Promise 對象變爲 rejected
狀態,從而能夠經過 catch
捕獲, 上述代碼只是一個粗粒度的容錯處理,若是但願 firstPromise
錯誤後能夠繼續執行 nextPromise
,則能夠經過 try...catch...
在 async
函數裏進行局部錯誤捕獲:
const firstPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
// Promise 決斷錯誤
setTimeout(() => reject(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
try {
await firstPromise(1);
} catch (err) {
console.error('err: ', err); // err: 2
}
// nextPromise 繼續執行
const nextResult = await nextPromise(1);
return nextResult;
}
co()
.then((res) => {
console.log('res: ', res); // res: 3
})
.catch((err) => {
console.error('err: ', err);
});
複製代碼
舒適提示:Callback 是 Node.js 中常用的編程方式,Node.js 中不少原生的 API 都是採用 Callback 的形式進行異步設計,早期的 Node.js 常常會有 Callback 和 Promise 混用的狀況,而且在很長一段時間裏都沒有很好的支持 Async 語法。若是你對 Node.js 和它的替代品 Deno 感興趣,能夠觀看 Ryan Dahl 在 TS Conf 2019 中的經典演講 Deno is a New Way to JavaScript。
業務思考更多的是結合基礎知識的廣度和深度進行的具體業務實踐,主要包含如下幾個方面:
筆試更多的是考驗應聘者的邏輯思惟能力和代碼書寫風格,主要包含如下幾個方面:
// 扁平數據
[{
name: '文本1',
parent: null,
id: 1,
}, {
name: '文本2',
id: 2,
parent: 1
}, {
name: '文本3',
parent: 2,
id: 3,
}]
// 樹狀數據
[{
name: '文本1',
id: 1,
children: [{
name: '文本2',
id: 2,
children: [{
name: '文本3',
id: 3
}]
}]
}]
複製代碼
const template = '嗨,{{ info.name.value }}您好,今天是星期 {{ day.value }}';
const data = {
info: {
name: {
value: '張三'
}
},
day: {
value: '三'
}
};
render(template, data); // 嗨,張三您好,今天是星期三
複製代碼