腳撕專有釘釘前端面試題(標題,拿來吧)

前言

最近用團隊的帳號發了一篇文章專有釘釘前端面試指南,初衷是但願給你們傳遞一些可能沒有接觸過的知識,其中某些知識可能也超出了前端的範疇,本質是但願給你們提供一些掃盲的思路。可是文章的評論使我意識到你們對於這個文章的抵觸心情很是大。我有很認真的看你們的每一條評論,而後可能過多的解釋也沒有什麼用。我本身也反思可能文章就不該該以面試爲標題進行傳播,由於面試的話它就意味着跟職位以及工做息息相關,更況且我仍是以團隊的名義去發這個文章。在這裏,先跟這些讀完文章體驗不是很好的同窗道個歉。javascript

之前寫文章感受都很開心,寫完發完感受都能給你們帶來一些新的輸入。可是這一次,我感受挺難受的,也確實反思了不少,感受本身以這樣的方式傳播可能有些問題,主要以下:html

  • 題目取的不對,不該該拿面試做爲標題,題目就應該是「腳撕專有釘釘前端面試題」
  • 若是做爲面試題,其中某些問題問的太大,範圍太廣,確實不適合面試者進行回答
  • 若是做爲面試題,其中某些問題問的不夠專業,甚至是有歧義
  • 給出了面試題,就應該給出面試題的答案,這樣纔是真正幫助到你們掃盲
  • ...

這裏再也不過多解釋和糾結面試題的問題了,由於我感受無論在評論中作什麼解釋,不承認的同窗仍是會一如既往的懟上來(挺好的,若是懟完感受本身還能釋放一些小壓力,或許還能適當的給子弈增長一些蒼白解釋的動力)。固然我也很開心不少同窗在評論中求答案,接下來我會好好認真作一期答案,但願可以給你們帶來一些新的輸入,固然答案不可能一會兒作完,也不必定全面或者讓你們感受滿意,或許你們此次的評論又能給我帶來一些學習的機會。前端

舒適提示:這裏儘可能多給出一些知識點,因此不會針對問題進行機械式的回答,可能更多的須要你們自行理解和抽象。其中大部分面試題可能會已文章連接的形式出現,或許是我本身之前寫過的文章,或者是我以爲別人寫的不錯的文章。java

基礎知識

基礎知識主要包含如下幾個方面:node

  • 基礎:計算機原理、編譯原理、數據結構、算法、設計模式、編程範式等基本知識瞭解
  • 語法:JavaScript、ECMAScript、CSS、TypeScript、HTML、Node.js 等語法的瞭解和使用
  • 框架:React、Vue、Egg、Koa、Express、Webpack 等原理的瞭解和使用
  • 工程:編譯工具、格式工具、Git、NPM、單元測試、Nginx、PM二、CI / CD 瞭解和使用
  • 網絡:HTTP、TCP、UDP、WebSocket、Cookie、Session、跨域、緩存、協議的瞭解
  • 性能:編譯性能、監控、白屏檢測、SEO、Service Worker 等了解
  • 插件:Chrome 、Vue CLI 、Webpack 等插件設計思路的理解
  • 系統:Mac、Windows、Linux 系統配置的實踐
  • 後端:Redis 緩存、數據庫、Graphql、SSR、模板引擎等了解和使用

基礎

一、列舉你所瞭解的計算機存儲設備類型?

現代計算機以存儲器爲中心,主要由 CPU、I / O 設備以及主存儲器三大部分組成。各個部分之間經過總線進行鏈接通訊,具體以下圖所示: image.png 上圖是一種多總線結構的示意圖,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 的簡單工做原理以下所示: image.png CPU 主要由控制單元、運算單元和存儲單元組成(注意忽略了中斷系統),各自的做用以下:程序員

  • 控制單元:在節拍脈衝的做用下,將程序計數器(Program Counter,PC)指向的主存或者多級高速緩存中的指令地址送到地址總線,接着獲取指令地址所對應的指令並放入指令寄存器 (Instruction Register,IR)中,而後經過指令譯碼器(Instruction Decoder,ID)分析指令須要進行的操做,最後經過操做控制器(Operation Controller,OC)向其餘設備發出微操做控制信號。
  • 運算單元:若是控制單元發出的控制信號存在算術運算(加、減、乘、除、增 一、減 一、取反等)或者邏輯運算(與、或、非、異或),那麼須要經過運算單元獲取存儲單元的計算數據進行處理。
  • 存儲單元:包括片內緩存和寄存器組,是 CPU 中臨時數據的存儲地方。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

四、複雜指令集和精簡指令集有什麼區別?

五、JavaScript 是如何運行的?解釋型語言和編譯型語言的差別是什麼?

早期的計算機只有機器語言時,程序設計必須用二進制數(0 和 1)來編寫程序,而且要求程序員對計算機硬件和指令集很是瞭解,編程的難度較大,操做極易出錯。爲了解決機器語言的編程問題,慢慢開始出現了符號式的彙編語言(採用 ADD、SUB、MUL、DIV 等符號表明加減乘除)。爲了使得計算機能夠識別彙編語言,須要將彙編語言翻譯成機器可以識別的機器語言(處理器的指令集): image.png 因爲每一種機器的指令系統不一樣,須要不一樣的彙編語言程序與之匹配,所以程序員每每須要針對不一樣的機器瞭解其硬件結構和指令系統。爲了能夠抹平不一樣機器的指令系統,使得程序員能夠更加關注程序設計自己,前後出現了各類面向問題的高級程序設計語言,例如 BASIC 和 C,具體過程以下圖所示: image.png 高級程序語言會先翻譯成彙編語言或者其餘中間語言,而後再根據不一樣的機器翻譯成機器語言進行執行。除此以外,彙編語言虛擬機和機器語言機器之間還存在一層操做系統虛擬機,主要用於控制和管理操做系統的所有硬件和軟件資源(隨着超大規模集成電路技術的不斷髮展,一些操做系統的軟件功能逐步由硬件來替換,例如目前的操做系統已經實現了部分程序的固化,簡稱固件,將程序永久性的存儲在 ROM 中)。機器語言機器還能夠繼續分解成微程序機器,將每一條機器指令翻譯成一組微指令(微程序)進行執行。

上述虛擬機所提供的語言轉換程序被稱爲編譯器,主要做用是將某種語言編寫的源程序轉換成一個等價的機器語言程序,編譯器的做用以下圖所示: image.png 例如 C 語言,能夠先經過 gcc 編譯器生成 Linux 和 Windows 下的目標 .o 和 .obj 文件(object 文件,即目標文件),而後將目標文件與底層系統庫文件、應用程序庫文件以及啓動文件連接成可執行文件在目標機器上執行。

舒適提示:感興趣的同窗能夠了解一下 ARM 芯片的程序運行原理,包括使用 IDE 進行程序的編譯(IDE 內置編譯器,主流編譯器包含 ARMCC、IAR 以及 GCC FOR ARM 等,其中一些編譯器僅僅隨着 IDE 進行捆綁發佈,不提供獨立使用的能力,而一些編譯器則隨着 IDE 進行發佈的同時,還提供命令行接口的獨立使用方式)、經過串口進行程序下載(下載到芯片的代碼區初始啓動地址映射的存儲空間地址)、啓動的存儲空間地址映射(包括系統存儲器、閃存 FLASH、內置 SRAM 等)、芯片的程序啓動模式引腳 BOOT 的設置(例如調試代碼時經常選擇內置 SRAM、真正程序運行的時候選擇閃存 FLASH)等。

若是某種高級語言或者應用語言(例如用於人工智能的計算機設計語言)轉換的目標語言不是特定計算機的彙編語言,而是面向另外一種高級程序語言(不少研究性的編譯器將 C 做爲目標語言),那麼還須要將目標高級程序語言再進行一次額外的編譯才能獲得最終的目標程序,這種編譯器可稱爲源到源的轉換器。

除此以外,有些程序設計語言將編譯的過程和最終轉換成目標程序進行執行的過程混合在一塊兒,這種語言轉換程序一般被稱爲解釋器,主要做用是將某種語言編寫的源程序做爲輸入,將該源程序執行的結果做爲輸出,解釋器的做用以下圖所示:

image.png

解釋器和編譯器有不少類似之處,都須要對源程序進行分析,並轉換成目標機器可識別的機器語言進行執行。只是解釋器是在轉換源程序的同時立馬執行對應的機器語言(轉換和執行的過程不分離),而編譯器得先把源程序所有轉換成機器語言併產生目標文件,而後將目標文件寫入相應的程序存儲器進行執行(轉換和執行的過程分離)。例如 Perl、Scheme、APL 使用解釋器進行轉換, C、C++ 則使用編譯器進行轉換,而 Java 和 JavaScript 的轉換既包含了編譯過程,也包含了解釋過程。

六、簡單描述一下 Babel 的編譯過程?

七、JavaScript 中的數組和函數在內存中是如何存儲的?

JavaScript 中的數組存儲大體須要分爲兩種狀況:

  • 同種類型數據的數組分配連續的內存空間
  • 存在非同種類型數據的數組使用哈希映射分配內存空間

舒適提示:能夠想象一下連續的內存空間只須要根據索引(指針)直接計算存儲位置便可。若是是哈希映射那麼首先須要計算索引值,而後若是索引值有衝突的場景下還須要進行二次查找(須要知道哈希的存儲方式)。

八、瀏覽器和 Node.js 中的事件循環機制有什麼區別?

閱讀連接:面試分享:兩年工做經驗成功面試阿里P6總結 - 瞭解 Event Loop 嗎?

九、ES6 Modules 相對於 CommonJS 的優點是什麼?

十、高級程序設計語言是如何編譯成機器語言的?

十一、編譯器通常由哪幾個階段組成?數據類型檢查通常在什麼階段進行?

十二、編譯過程當中虛擬機的做用是什麼?

1三、什麼是中間代碼(IR),它的做用是什麼?

1四、什麼是交叉編譯?

編譯器的設計是一個很是龐大和複雜的軟件系統設計,在真正設計的時候須要解決兩個相對重要的問題:

  • 如何分析不一樣高級程序語言設計的源程序
  • 如何將源程序的功能等價映射到不一樣指令系統的目標機器

爲了解決上述兩項問題,編譯器的設計最終被分解成前端(注意這裏所說的不是 Web 前端)和後端兩個編譯階段,前端用於解決第一個問題,然後端用於解決第二個問題,具體以下圖所示: image.png 上圖中的中間表示(Intermediate Representation,IR)是程序結構的一種表現方式,它會比 AST(後續講解)更加接近彙編語言或者指令集,同時也會保留源程序中的一些高級信息,除此以外 ,它的種類不少,包括三地址碼(Three Address Code, TAC)靜態單賦值形式(Static Single Assignment Form, SSA)以及基於棧的 IR 等,具體做用包括:

  • 靠近前端部分主要適配不一樣的源程序,靠近後端部分主要適配不一樣的指令集,更易於編譯器的錯誤調試,容易識別是 IR 以前仍是以後出問題
  • 以下左圖所示,若是沒有 IR,那麼源程序到指令集之間須要進行一一適配,而有了中間表示,則可使得編譯器的職責更加分離,源程序的編譯更多關注如何轉換成 IR,而不是去適配不一樣的指令集
  • IR 自己能夠作到多趟迭代從而優化源程序,在每一趟迭代的過程當中能夠研究代碼並記錄優化的細節,方便後續的迭代查找並利用這些優化信息,最終能夠高效輸出更優的目標程序

image.png 因爲 IR 能夠進行多趟迭代進行程序優化,所以在編譯器中可插入一個新的優化階段,以下圖所示: image.png 優化器能夠對 IR 處理一遍或者多遍,從而生成更快執行速度(例如找到循環中不變的計算並對其進行優化從而減小運算次數)或者更小體積的目標程序,也可能用於產生更少異常或者更低功耗的目標程序。除此以外,前端和後端內部還能夠細分爲多個處理步驟,具體以下圖所示: image.png 優化器中的每一遍優化處理均可以使用一個或多個優化技術來改進代碼,每一趟處理最終都是讀寫 IR 的操做,這樣不只僅可使得優化能夠更加高效,同時也能夠下降優化的複雜度,還提升了優化的靈活性,可使得編譯器配置不一樣的優化選項,達到組合優化的效果。

1五、發佈 / 訂閱模式和觀察者模式的區別是什麼?

閱讀連接:基於Vue實現一個簡易MVVM - 觀察者模式和發佈/訂閱模式

1六、裝飾器模式通常會在什麼場合使用?

1七、談談你對大型項目的代碼解耦設計理解?什麼是 Ioc?通常 DI 採用什麼設計模式實現?

1八、列舉你所瞭解的編程範式?

編程範式(Programming paradigm)是指計算機編程的基本風格或者典型模式,能夠簡單理解爲編程學科中實踐出來的具備哲學和理論依據的一些經典原型。常見的編程範式有:

  • 面向過程(Process Oriented Programming,POP)
  • 面向對象(Object Oriented Programming,OOP)
  • 面向接口(Interface Oriented Programming, IOP)
  • 面向切面(Aspect Oriented Programming,AOP)
  • 函數式(Funtional Programming,FP)
  • 響應式(Reactive Programming,RP)
  • 函數響應式(Functional Reactive Programming,FRP)

閱讀連接::若是你對於編程範式的定義相對模糊,能夠繼續閱讀 What is the precise definition of programming paradigm? 瞭解更多。

不一樣的語言能夠支持多種不一樣的編程範式,例如 C 語言支持 POP 範式,C++ 和 Java 語言支持 OOP 範式,Swift 語言則能夠支持 FP 範式,而 Web 前端中的 JavaScript 能夠支持上述列出的全部編程範式。

1九、什麼是面向切面(AOP)的編程?

20、什麼是函數式編程?

顧名思義,函數式編程是使用函數來進行高效處理數據或數據流的一種編程方式。在數學中,函數的三要素是定義域、值域和**對應關係。假設 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,徹底符合預期。咱們知道了 addmultiply 兩個函數的實際對應關係,經過將對應關係進行有效的組合和傳遞,達到了最終的計算結果。除此以外,這兩個函數還能夠根據數學定律得出更優雅的組合方式:

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 編程範式中可能存在的內部狀態數據,事實上對於數學定律中的結合律、交換律、同一概以及分配律,上述的函數式編程代碼足能夠勝任。

原則

經過上述簡單的例子能夠發現,要實現高可複用的函數**(對應關係)**,必定要遵循某些特定的原則,不然在使用的時候可能沒法進行高效的傳遞和組合,例如

  • 高內聚低耦合
  • 最小意外原則
  • 單一職責原則
  • ...

若是你以前常常進行無原則性的代碼設計,那麼在設計過程當中可能會出現各類出乎意料的問題(這是爲何新手總是出現一些稀奇古怪問題的主要緣由)。函數式編程能夠有效的經過一些原則性的約束使你設計出更加健壯和優雅的代碼,而且在不斷的實踐過程當中進行經驗式疊加,從而提升開發效率。

特色

雖然咱們在使用函數的過程當中更多的再也不關注函數如何實現(對應關係),可是真正在使用和設計函數的時候須要注意如下一些特色:

  • 聲明式(Declarative Programming)
  • 一等公民(First Class Function)
  • 純函數(Pure Function)
  • 無狀態和數據不可變(Statelessness and Immutable Data)
  • ...

聲明式

咱們之前設計的代碼一般是命令式編程方式,這種編程方式每每注重具體的實現的過程(對應關係),而函數式編程則採用聲明式的編程方式,每每注重如何去組合已有的**對應關係。**簡單舉個例子:

// 命令式
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 產生的機率。固然要實現純函數的這種特性,須要函數不能包含如下一些反作用:

  • 操做 Http 請求
  • 可變數據(包括在函數內部改變輸入參數)
  • DOM 操做
  • 打印日誌
  • 訪問系統狀態
  • 操做文件系統
  • 操做數據庫
  • ...

從以上常見的一些反作用能夠看出,純函數的實現須要遵循最小意外原則,爲了確保函數的穩定惟一的輸入和輸出,儘可能應該避免與函數外部的環境進行任何交互行爲,從而防止外部環境對函數內部產生沒法預料的影響。純函數的實現應該自給自足,舉幾個例子:

// 若是使用 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);
  }
}
複製代碼

純函數的特性使得函數式編程具有如下特性:

  • 可緩存性(Cacheable)
  • 可移植性(Portable)
  • 可測試性(Testable)

可緩存性和可測試性基於純函數輸入輸出惟一不變的特性,可移植性則主要基於純函數不依賴外部環境的特性。這裏舉一個可緩存的例子:

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 每次調用都在修改原數組,且產生的輸出也不相同。 在函數式編程中,這種會改變原有數據的函數已經再也不是純函數,應該儘可能避免使用。

閱讀連接:若是想要了解更深刻的函數式編程知識點,能夠額外閱讀函數式編程指北

2一、響應式編程的使用場景有哪些?

響應式編程是一種基於觀察者(發佈 / 訂閱)模式而且面向異步(Asynchronous)數據流(Data Stream)和變化傳播的聲明式編程範式。響應式編程主要適用的場景包含:

  • 用戶和系統發起的連續事件處理,例如鼠標的點擊、鍵盤的按鍵或者通訊設備發起的信號等
  • 非可靠的網絡或者通訊處理(例如 HTTP 網絡的請求重試)
  • 連續的異步 IO 處理
  • 複雜的繼發事務處理(例如一次事件涉及到多個繼發的網絡請求)
  • 高併發的消息處理(例如 IM 聊天)
  • ...

語法

2二、如何實現一個上中下三行佈局,頂部和底部最小高度是 100px,中間自適應?

2三、如何判斷一個元素 CSS 樣式溢出,從而能夠選擇性的加 title 或者 Tooltip?

2四、如何讓 CSS 元素左側自動溢出(... 溢出在左側)?

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/…

2五、什麼是沙箱?瀏覽器的沙箱有什麼做用?

2六、如何處理瀏覽器中表單項的密碼自動填充問題?

2七、Hash 和 History 路由的區別和優缺點?

2八、JavaScript 中對象的屬性描述符有哪些?分別有什麼做用?

2九、JavaScript 中 console 有哪些 api ?

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:

  • console.log
  • console.error
  • console.time
  • console.timeEnd
  • console.group

具體查看:developer.mozilla.org/en-US/docs/…

30、 簡單對比一下 Callback、Promise、Generator、Async 幾個異步 API 的優劣?

在 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,它的特色以下:

  • Promise 對象的執行狀態不受外界影響。Promise 對象的異步操做有三種狀態: pending(進行中)、 fulfilled(已成功)和 rejected(已失敗) ,只有 Promise 對象自己的異步操做結果能夠決定當前的執行狀態,任何其餘的操做沒法改變狀態的結果
  • Promise 對象的執行狀態不可變。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 的錯誤回調能夠同時捕獲 firstPromisenextPromise 兩個函數的 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 對於異步的處理更加優雅,而且能力也更增強大, 可是也存在一些自身的缺點:

  • 沒法取消 Promise 的執行
  • 沒法在 Promise 外部經過 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 具備如下優點:

  • 豐富了狀態類型,Generator 經過 next 能夠產生不一樣的狀態信息,也能夠經過 return 結束函數的執行狀態,相對於 Promise 的 resolve 不可變狀態更加豐富 
  • Generator 函數內部的異步代碼執行看起來和同步代碼執行一致,很是利於代碼的維護
  • Generator 函數內部的執行邏輯和相應的狀態變化邏輯解耦,下降了代碼的複雜度

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 的時候還須要注意如下一些事項:

  • 若是 Generator 函數自己沒有捕獲錯誤,那麼 Generator 函數內部拋出的錯誤能夠在執行處進行錯誤捕獲
  • 若是 Generator 函數內部和執行處都沒有進行錯誤捕獲,則終止進程並拋出錯誤信息
  • 若是沒有執行過 g.next,則 g.throw 不會在 Gererator 函數中被捕獲(由於執行指針沒有啓動 Generator 函數的執行),此時能夠在執行處進行執行錯誤捕獲

Async

Async 是 Generator 函數的語法糖,相對於 Generator 而言 Async 的特性以下:

  • 內置執行器:Generator 函數須要設計手動執行器或者通用執行器(例如 Co 執行器)進行執行,Async 語法則內置了自動執行器,設計代碼時無須關心執行步驟
  • yield 命令無約束:在 Generator 中使用 Co 執行器時 yield 後必須是 Promise 對象或者 Thunk 函數,而 Async 語法中的 await 後能夠是 Promise 對象或者原始數據類型對象、數字、字符串、布爾值等(此時會對其進行 Promise.resolve() 包裝處理) 
  • 返回 Promise: 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

3一、 Object.defineProperty 有哪幾個參數?各自都有什麼做用?

3二、 Object.defineProperty 和 ES6 的 Proxy 有什麼區別?

3三、 ES6 中 Symbol、Map、Decorator 的使用場景有哪些?或者你在哪些庫的源碼裏見過這些 API 的使用?

3四、 爲何要使用 TypeScript ? TypeScript 相對於 JavaScript 的優點是什麼?

3五、 TypeScript 中 const 和 readonly 的區別?枚舉和常量枚舉的區別?接口和類型別名的區別?

3六、 TypeScript 中 any 類型的做用是什麼?

3七、 TypeScript 中 any、never、unknown 和 void 有什麼區別?

3八、 TypeScript 中 interface 能夠給 Function / Array / Class(Indexable)作聲明嗎?

3九、 TypeScript 中可使用 String、Number、Boolean、Symbol、Object 等給類型作聲明嗎?

40、 TypeScript 中的 this 和 JavaScript 中的 this 有什麼差別?

4一、 TypeScript 中使用 Unions 時有哪些注意事項?

4二、 TypeScript 如何設計 Class 的聲明?

4三、 TypeScript 中如何聯合枚舉類型的 Key?

4四、 TypeScript 中 ?.、??、!.、_、** 等符號的含義?

4五、 TypeScript 中預約義的有條件類型有哪些?

4六、 簡單介紹一下 TypeScript 模塊的加載機制?

4七、 簡單聊聊你對 TypeScript 類型兼容性的理解?抗變、雙變、協變和逆變的簡單理解?

4八、 TypeScript 中對象展開會有什麼反作用嗎?

4九、 TypeScript 中 interface、type、enum 聲明有做用域的功能嗎?

50、 TypeScript 中同名的 interface 或者同名的 interface 和 class 能夠合併嗎?

5一、 如何使 TypeScript 項目引入並識別編譯爲 JavaScript 的 npm 庫包?

5二、 TypeScript 的 tsconfig.json 中有哪些配置項信息?

5三、 TypeScript 中如何設置模塊導入的路徑別名?

框架

5四、 React Class 組件有哪些周期函數?分別有什麼做用?

5五、 React Class 組件中請求能夠在 componentWillMount 中發起嗎?爲何?

5六、 React Class 組件和 React Hook 的區別有哪些?

5七、 React 中高階函數和自定義 Hook 的優缺點?

5八、 簡要說明 React Hook 中 useState 和 useEffect 的運行原理?

5九、 React 如何發現重渲染、什麼緣由容易形成重渲染、如何避免重渲染?

60、 React Hook 中 useEffect 有哪些參數,如何檢測數組依賴項的變化?

6一、 React 的 useEffect 是如何監聽數組依賴項的變化的?

6二、 React Hook 和閉包有什麼關聯關係?

6三、 React 中 useState 是如何作數據初始化的?

6四、 列舉你經常使用的 React 性能優化技巧?

6五、 Vue 2.x 模板中的指令是如何解析實現的?

6六、 簡要說明 Vue 2.x 的全鏈路運做機制?

6七、 簡單介紹一下 Element UI 的框架設計?

6八、 如何理解 Vue 是一個漸進式框架?

6九、 Vue 裏實現跨組件通訊的方式有哪些?

70、 Vue 中響應式數據是如何作到對某個對象的深層次屬性的監聽的?

7一、 MVVM、MVC 和 MVP 的區別是什麼?各自有什麼應用場景?、

7二、 什麼是 MVVM 框架?

工程

7三、Vue CLI 3.x 有哪些功能?Vue CLI 3.x 的插件系統瞭解?

7四、Vue CLI 3.x 中的 Webpack 是如何組裝處理的?

7五、Vue 2.x 如何支持 TypeScript 語法?

7六、如何配置環境使得 JavaScript 項目能夠支持 TypeScript 語法?

7七、如何對 TypeScript 進行 Lint 校驗?ESLint 和 TSLint 有什麼區別?

7八、Node.js 如何支持 TypeScript 語法?

7九、TypeScript 如何自動生成庫包的聲明文件?

80、Babel 對於 TypeScript 的支持有哪些限制?

8一、Webpack 中 Loader 和 Plugin 的區別是什麼?

8二、在 Webpack 中是如何作到支持相似於 JSX 語法的 Sourcemap 定位?

8三、發佈 Npm 包如何指定引入地址?

8四、如何發佈開發項目的特定文件夾爲 Npm 包的根目錄?

8五、如何發佈一個支持 Tree Shaking 機制的 Npm 包?

8六、Npm 包中 peerDependencies 的做用是什麼?

8七、如何優雅的調試須要發佈的 Npm 包?

8八、在設計一些庫包時如何生成版本日誌?

8九、瞭解 Git (Submodule)子模塊嗎?簡單介紹一下 Git 子模塊的做用?

90、Git 如何修改已經提交的 Commit 信息?

9一、Git 如何撤銷 Commit 並保存以前的修改?

9二、Git 如何 ignore 被 commit 過的文件?

9三、在使用 Git 的時候如何規範 Git 的提交說明(Commit 信息)?

9四、簡述符合 Angular 規範的提交說明的結構組成?

9五、Commit 信息如何和 Github Issues 關聯?

9六、Git Hook 在項目中哪些做用?

9七、Git Hook 中客戶端和服務端鉤子各自用於什麼做用?

9八、Git Hook 中經常使用的鉤子有哪些?

9九、pre-commit 和 commit-msg 鉤子的區別是什麼?各自可用於作什麼?

100、husky 以及 ghook 等工具製做 Git Hook 的原理是什麼?

10一、如何設計一個通用的 Git Hook ?

10二、Git Hook 能夠採用 Node 腳本進行設計嗎?如何作到?

10三、如何確保別人上傳的代碼沒有 Lint 錯誤?如何確保代碼構建沒有 Lint 錯誤?

10四、如何在 Vs Code 中進行 Lint 校驗提示?如何在 Vs Code 中進行 Lint 保存格式化?

10五、ESLint 和 Prettier 的區別是什麼?二者在一塊兒工做時會產生問題嗎?

10六、如何有效的識別 ESLint 和 Prettier 可能產生衝突的格式規則?如何解決此類規則衝突問題?

10七、在一般的腳手架項目中進行熱更新(hot module replacement)時如何作到 ESLint 實時打印校驗錯誤信息?

10八、談談你對 SourceMap 的瞭解?

10九、如何調試 Node.js 代碼?如何調試 Node.js TypeScript 代碼?在瀏覽器中如何調試 Node.js 代碼?

1十、列舉你知道的全部構建工具並說說這些工具的優缺點?這些構建工具在不一樣的場景下應該如何選型?

1十一、VS Code 配置中的用戶和工做區有什麼區別?

1十二、VS Code 的插件能夠只對當前項目生效嗎?

11三、你所知道的測試有哪些測試類型?

11四、你所知道的測試框架有哪些?

11五、什麼是 e2e 測試?有哪些 e2e 的測試框架?

11六、假設如今有一個插入排序算法,如何對該算法進行單元測試?

網絡

11七、CDN 服務如何實現網絡加速?

11八、WebSocket 使用的是 TCP 仍是 UDP 協議?

11九、什麼是單工、半雙工和全雙工通訊?

120、簡單描述 HTTP 協議發送一個帶域名的 URL 請求的協議傳輸過程?(DNS、TCP、IP、鏈路)

12一、什麼是正向代理?什麼是反向代理?

12二、Cookie 能夠在服務端生成嗎?Cookie 在服務端生成後的工做流程是什麼樣的?

12三、Session、Cookie 的區別和關聯?如何進行臨時性和永久性的 Session 存儲?

12四、設置 Cookie 時候如何防止 XSS 攻擊?

12五、簡單描述一下用戶免登錄的實現過程?可能會出現哪些安全性問題?通常如何對用戶登陸的密碼進行加密?

12六、HTTP 中提高傳輸速率的方式有哪些?經常使用的內容編碼方式有哪些?

12七、傳輸圖片的過程當中若是忽然中斷,如何在恢復後從以前的中斷中恢復傳輸?

12八、什麼是代理?什麼是網關?代理和網關的做用是什麼?

12九、HTTPS 相比 HTTP 爲何更加安全可靠?

130、什麼是對稱密鑰(共享密鑰)加密?什麼是非對稱密鑰(公開密鑰)加密?哪一個更加安全?

13一、你以爲 HTTP 協議目前存在哪些缺點?

性能

13三、在 React 中如何識別一個表單項裏的表單作到了最小粒度 / 代價的渲染?

13四、在 React 的開發的過程當中你能想到哪些控制渲染成本的方法?

插件

13五、Vue CLI 3.x 的插件系統是如何設計的?

13六、Webpack 中的插件機制是如何設計的?

系統

13七、\r\n(CRLF) 和 \n (LF)的區別是什麼?(Vs Code 的右下角能夠切換)

13八、/dev/null 的做用是啥?

13九、如何在 Mac 的終端中設置一個命令的別名?

140、如何在 Windows 中設置環境變量?

14一、Mac 的文件操做系統默認區分文件路徑的大小寫嗎?

14二、編寫 Shell 腳本時如何設置文件的絕對路徑?

後端

14三、Session、Cookie 的區別和關聯?如何進行臨時性和永久性的 Session 存儲?

14四、如何部署 Node.js 應用?如何處理負載均衡中 Session 的一致性問題?

14五、如何提高 Node.js 代碼的運行穩定性?

14六、GraphQL 與 Restful 的區別,它有什麼優勢?

14七、Vue SSR 的工做原理?Vuex 的數據如何同構渲染?

14八、SSR 技術和 SPA 技術的各自的優缺點是什麼?

14九、如何處理 Node.js 渲染 HTML 壓力過大問題?

業務思考

業務思考更多的是結合基礎知識的廣度和深度進行的具體業務實踐,主要包含如下幾個方面:

  • 工程化:代碼部署、CI / CD 流程設計、Jenkins、Gitlab、Docker 等
  • 通用性:腳手架、SDK、組件庫等框架設計
  • 應用框架:Hybrid 混合、微前端、BFF、Monorepo
  • 可視化:
  • 低代碼:通用表單設計、通用佈局設計、通用頁面設計、JSON Schema 協議設計等
  • 測試:E2E 測試、單元測試、測試覆蓋率、測試報告等
  • 業務:數據、體驗、複雜度、監控

工程化

150、你所知道的 CI / CD 工具備哪些?在項目中有接觸過相似的流程嗎?

15一、若是讓你實現一個 Web 前端的 CI / CD 工程研發平臺,你會如何設計?

15二、若是咱們須要將已有項目中的線上產物資源(例如圖片)轉換成本地私有化資源,你有什麼解決方案?

15三、如何使用 Vue CLI 3.x 定製一個腳手架?好比內部自動集成了 i18n、 axios、Element UI、路由守衛等?

15四、Jenkins 如何配合 Node.js 腳本進行 CI / CD 設計?

通用性

15五、若是讓你設計一個通用的項目腳手架,你會如何設計?一個通用的腳手架通常須要具有哪些能力?

15六、若是讓你設計一個通用的工具庫,你會如何設計?一個通用的工具庫通常須要具有哪些能力?

15七、假設你本身實現的 React 或 Vue 的組件庫要設計演示文檔,你會如何設計?設計的文檔須要實現哪些功能?

15八、在設計工具庫包的時候你是如何設計 API 文檔的?

應用框架

15九、談談 Electron、Nw.js、CEF、Flutter 和原生開發的理解?

160、談談桌面端應用中 HotFix 的理解?

16一、你以爲什麼樣的場景須要使用微前端框架?

業務

16二、什麼是單點登陸?如何作單點登陸?

16三、如何作一個項目的國際化方案?

16四、如何作一個項目的監控和埋點方案?

16五、如何建設項目的穩定性(監控、灰度、錯誤降級、回滾...)?

16六、通常管理後臺型的應用須要考慮哪些性能方面的優化?

16七、簡述一些提高項目體驗的案例和技術方案(骨架屏、Loading 處理、緩存、錯誤降級、請求重試...)?

16八、假設須要對頁面設計一個水印方案,你會如何設計?

低代碼

16九、如何設計一個通用的 JSON Schema 協議使其能夠動態渲染一個通用的聯動表單?

170、通常的低代碼平臺須要具有哪些能力?

筆試實踐

筆試更多的是考驗應聘者的邏輯思惟能力和代碼書寫風格,主要包含如下幾個方面:

  • 正則表達式
  • 算法
  • 數據結構
  • 設計模式
  • 框架的部分原理實現
  • TypeScript 語法
  • 模板解析

數據結構

17一、使用 TypeScript 語法將沒有層級的扁平數據轉換成樹形結構的數據

// 扁平數據
[{
  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
    }]
  }]
}]
複製代碼

模板解析

17二、實現一個簡易的模板引擎

const template = '嗨,{{ info.name.value }}您好,今天是星期 {{ day.value }}';

const data = {
  info: {
    name: {
      value: '張三'
    }
  },
  day: {
    value: '三'
  }
};

render(template, data); // 嗨,張三您好,今天是星期三
複製代碼

設計模式

17三、簡單實現一個發佈 / 訂閱模式

正則表達式

17四、匹配出字符串中 const a = require('xxx') 中的 xxx

相關文章
相關標籤/搜索