<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手撕釘釘前端面試題

          共 54166字,需瀏覽 109分鐘

           ·

          2021-08-06 11:37


          作者:子弈,原文鏈接:https://juejin.cn/post/6987549240436195364,如需轉(zhuǎn)載請(qǐng)聯(lián)系作者授權(quán)

          前言

          最近用團(tuán)隊(duì)的賬號(hào)發(fā)了一篇文章專有釘釘前端面試指南[1],初衷是希望給大家傳遞一些可能沒有接觸過(guò)的知識(shí),其中某些知識(shí)可能也超出了前端的范疇,本質(zhì)是希望給大家提供一些掃盲的思路。但是文章的評(píng)論使我意識(shí)到大家對(duì)于這個(gè)文章的抵觸心情非常大。我有很認(rèn)真的看大家的每一條評(píng)論,然后可能過(guò)多的解釋也沒有什么用。我自己也反思可能文章就不應(yīng)該以面試為標(biāo)題進(jìn)行傳播,因?yàn)槊嬖嚨脑捤鸵馕吨毼灰约肮ぷ飨⑾⑾嚓P(guān),更何況我還是以團(tuán)隊(duì)的名義去發(fā)這個(gè)文章。在這里,先跟這些讀完文章體驗(yàn)不是很好的同學(xué)道個(gè)歉。

          以前寫文章感覺都很開心,寫完發(fā)完感覺都能給大家?guī)?lái)一些新的輸入。但是這一次,我感覺挺難受的,也確實(shí)反思了很多,感覺自己以這樣的方式傳播可能有些問(wèn)題,主要如下:

          • 題目取的不對(duì),不應(yīng)該拿面試作為標(biāo)題,題目就應(yīng)該是“腳撕專有釘釘前端面試題”

          • 如果作為面試題,其中某些問(wèn)題問(wèn)的太大,范圍太廣,確實(shí)不適合面試者進(jìn)行回答

          • 如果作為面試題,其中某些問(wèn)題問(wèn)的不夠?qū)I(yè),甚至是有歧義

          • 給出了面試題,就應(yīng)該給出面試題的答案,這樣才是真正幫助到大家掃盲

          • ...

          這里不再過(guò)多解釋和糾結(jié)面試題的問(wèn)題了,因?yàn)槲腋杏X不管在評(píng)論中做什么解釋,不認(rèn)可的同學(xué)還是會(huì)一如既往的懟上來(lái)(挺好的,如果懟完感覺自己還能釋放一些小壓力,或許還能適當(dāng)?shù)慕o子弈增加一些蒼白解釋的動(dòng)力)。當(dāng)然我也很開心很多同學(xué)在評(píng)論中求答案,接下來(lái)我會(huì)好好認(rèn)真做一期答案,希望能夠給大家?guī)?lái)一些新的輸入,當(dāng)然答案不可能一下子做完,也不一定全面或者讓大家感覺滿意,或許大家這次的評(píng)論又能給我?guī)?lái)一些學(xué)習(xí)的機(jī)會(huì)。

          溫馨提示:這里盡量多給出一些知識(shí)點(diǎn),所以不會(huì)針對(duì)問(wèn)題進(jìn)行機(jī)械式的回答,可能更多的需要大家自行理解和抽象。其中大部分面試題可能會(huì)已文章鏈接的形式出現(xiàn),或許是我自己以前寫過(guò)的文章,或者是我覺得別人寫的不錯(cuò)的文章。

          基礎(chǔ)知識(shí)

          基礎(chǔ)知識(shí)主要包含以下幾個(gè)方面:

          • 基礎(chǔ):計(jì)算機(jī)原理、編譯原理、數(shù)據(jù)結(jié)構(gòu)、算法、設(shè)計(jì)模式、編程范式等基本知識(shí)了解

          • 語(yǔ)法:JavaScript、ECMAScript、CSS、TypeScript、HTML、Node.js 等語(yǔ)法的了解和使用

          • 框架:React、Vue、Egg、Koa、Express、Webpack 等原理的了解和使用

          • 工程:編譯工具、格式工具、Git、NPM、單元測(cè)試、Nginx、PM2、CI / CD 了解和使用

          • 網(wǎng)絡(luò):HTTP、TCP、UDP、WebSocket、Cookie、Session、跨域、緩存、協(xié)議的了解

          • 性能:編譯性能、監(jiān)控、白屏檢測(cè)、SEO、Service Worker 等了解

          • 插件:Chrome 、Vue CLI 、Webpack 等插件設(shè)計(jì)思路的理解

          • 系統(tǒng):Mac、Windows、Linux 系統(tǒng)配置的實(shí)踐

          • 后端:Redis 緩存、數(shù)據(jù)庫(kù)、Graphql、SSR、模板引擎等了解和使用

          基礎(chǔ)

          1、列舉你所了解的計(jì)算機(jī)存儲(chǔ)設(shè)備類型?

          現(xiàn)代計(jì)算機(jī)以存儲(chǔ)器為中心,主要由 CPU、I / O 設(shè)備以及主存儲(chǔ)器三大部分組成。各個(gè)部分之間通過(guò)總線進(jìn)行連接通信,具體如下圖所示:

          image.png

          上圖是一種多總線結(jié)構(gòu)的示意圖,CPU、主存以及 I / O 設(shè)備之間的所有數(shù)據(jù)都是通過(guò)總線進(jìn)行并行傳輸,使用局部總線是為了提高 CPU 的吞吐量(CPU 不需要直接跟 I / O 設(shè)備通信),而使用高速總線(更貼近 CPU)和 DMA 總線則是為了提升高速 I / O 設(shè)備(外設(shè)存儲(chǔ)器、局域網(wǎng)以及多媒體等)的執(zhí)行效率。

          主存包括隨機(jī)存儲(chǔ)器 RAM 和只讀存儲(chǔ)器 ROM,其中 ROM 又可以分為 MROM(一次性)、PROM、EPROM、EEPROM 。ROM 中存儲(chǔ)的程序(例如啟動(dòng)程序、固化程序)和數(shù)據(jù)(例如常量數(shù)據(jù))在斷電后不會(huì)丟失。RAM 主要分為靜態(tài) RAM(SRAM) 和動(dòng)態(tài) RAM(DRAM) 兩種類型(DRAM 種類很多,包括 SDRAM、RDRAM、CDRAM 等),斷電后數(shù)據(jù)會(huì)丟失,主要用于存儲(chǔ)臨時(shí)程序或者臨時(shí)變量數(shù)據(jù)。DRAM 一般訪問(wèn)速度相對(duì)較慢。由于現(xiàn)代 CPU 讀取速度要求相對(duì)較高,因此在 CPU 內(nèi)核中都會(huì)設(shè)計(jì) L1、L2 以及 L3 級(jí)別的多級(jí)高速緩存,這些緩存基本是由 SRAM 構(gòu)成,一般訪問(wèn)速度較快。

          2、一般代碼存儲(chǔ)在計(jì)算機(jī)的哪個(gè)設(shè)備中?代碼在 CPU 中是如何運(yùn)行的?

          高級(jí)程序設(shè)計(jì)語(yǔ)言不能直接被計(jì)算機(jī)理解并執(zhí)行,需要通過(guò)翻譯程序?qū)⑵滢D(zhuǎn)換成特定處理器上可執(zhí)行的指令,計(jì)算機(jī) CPU 的簡(jiǎn)單工作原理如下所示:

          image.png

          CPU 主要由控制單元、運(yùn)算單元和存儲(chǔ)單元組成(注意忽略了中斷系統(tǒng)),各自的作用如下:

          • 控制單元:在節(jié)拍脈沖的作用下,將程序計(jì)數(shù)器(Program Counter,PC)指向的主存或者多級(jí)高速緩存中的指令地址送到地址總線,接著獲取指令地址所對(duì)應(yīng)的指令并放入指令寄存器 (Instruction Register,IR)中,然后通過(guò)指令譯碼器(Instruction Decoder,ID)分析指令需要進(jìn)行的操作,最后通過(guò)操作控制器(Operation Controller,OC)向其他設(shè)備發(fā)出微操作控制信號(hào)。

          • 運(yùn)算單元:如果控制單元發(fā)出的控制信號(hào)存在算術(shù)運(yùn)算(加、減、乘、除、增 1、減 1、取反等)或者邏輯運(yùn)算(與、或、非、異或),那么需要通過(guò)運(yùn)算單元獲取存儲(chǔ)單元的計(jì)算數(shù)據(jù)進(jìn)行處理。

          • 存儲(chǔ)單元:包括片內(nèi)緩存和寄存器組,是 CPU 中臨時(shí)數(shù)據(jù)的存儲(chǔ)地方。CPU 直接訪問(wèn)主存數(shù)據(jù)大概需要花費(fèi)數(shù)百個(gè)機(jī)器周期,而訪問(wèn)寄存器或者片內(nèi)緩存只需要若干個(gè)或者幾十個(gè)機(jī)器周期,因此會(huì)使用內(nèi)部寄存器或緩存來(lái)存儲(chǔ)和獲取臨時(shí)數(shù)據(jù)(即將被運(yùn)算或者運(yùn)算之后的數(shù)據(jù)),從而提高 CPU 的運(yùn)行效率。

          除此之外,計(jì)算機(jī)系統(tǒng)執(zhí)行程序指令時(shí)需要花費(fèi)時(shí)間,其中取出一條指令并執(zhí)行這條指令的時(shí)間叫指令周期。指令周期可以分為若干個(gè)階段(取指周期、間址周期、執(zhí)行周期和中斷周期),每個(gè)階段主要完成一項(xiàng)基本操作,完成基本操作的時(shí)間叫機(jī)器周期。機(jī)器周期是時(shí)鐘周期的分頻,例如最經(jīng)典的 8051 單片機(jī)的機(jī)器周期為 12 個(gè)時(shí)鐘周期。時(shí)鐘周期是 CPU 工作的基本時(shí)間單位,也可以稱為節(jié)拍脈沖或 T 周期(CPU 主頻的倒數(shù)) 。假設(shè) CPU 的主頻是 1 GHz(1 Hz 表示每秒運(yùn)行 1 次),那么表示時(shí)鐘周期為 1 / 109 s。理論上 CPU 的主頻越高,程序指令執(zhí)行的速度越快。

          3、什么是指令和指令集?

          上圖右側(cè)主存中的指令是 CPU 可以支持的處理命令,一般包含算術(shù)指令(加和減)、邏輯指令(與、或和非)、數(shù)據(jù)指令(移動(dòng)、輸入、刪除、加載和存儲(chǔ))、流程控制指令以及程序結(jié)束指令等,由于 CPU 只能識(shí)別二進(jìn)制碼,因此指令是由二進(jìn)制碼組成。除此之外,指令的集合稱為指令集(例如匯編語(yǔ)言就是指令集的一種表現(xiàn)形式),常見的指令集有精簡(jiǎn)指令集(ARM)和復(fù)雜指令集(Inter X86)。一般指令集決定了 CPU 處理器的硬件架構(gòu),規(guī)定了處理器的相應(yīng)操作。

          4、復(fù)雜指令集和精簡(jiǎn)指令集有什么區(qū)別?

          5、JavaScript 是如何運(yùn)行的?解釋型語(yǔ)言和編譯型語(yǔ)言的差異是什么?

          早期的計(jì)算機(jī)只有機(jī)器語(yǔ)言時(shí),程序設(shè)計(jì)必須用二進(jìn)制數(shù)(0 和 1)來(lái)編寫程序,并且要求程序員對(duì)計(jì)算機(jī)硬件和指令集非常了解,編程的難度較大,操作極易出錯(cuò)。為了解決機(jī)器語(yǔ)言的編程問(wèn)題,慢慢開始出現(xiàn)了符號(hào)式的匯編語(yǔ)言(采用 ADD、SUB、MUL、DIV 等符號(hào)代表加減乘除)。為了使得計(jì)算機(jī)可以識(shí)別匯編語(yǔ)言,需要將匯編語(yǔ)言翻譯成機(jī)器能夠識(shí)別的機(jī)器語(yǔ)言(處理器的指令集):

          image.png

          由于每一種機(jī)器的指令系統(tǒng)不同,需要不同的匯編語(yǔ)言程序與之匹配,因此程序員往往需要針對(duì)不同的機(jī)器了解其硬件結(jié)構(gòu)和指令系統(tǒng)。為了可以抹平不同機(jī)器的指令系統(tǒng),使得程序員可以更加關(guān)注程序設(shè)計(jì)本身,先后出現(xiàn)了各種面向問(wèn)題的高級(jí)程序設(shè)計(jì)語(yǔ)言,例如 BASIC 和 C,具體過(guò)程如下圖所示:

          image.png

          高級(jí)程序語(yǔ)言會(huì)先翻譯成匯編語(yǔ)言或者其他中間語(yǔ)言,然后再根據(jù)不同的機(jī)器翻譯成機(jī)器語(yǔ)言進(jìn)行執(zhí)行。除此之外,匯編語(yǔ)言虛擬機(jī)和機(jī)器語(yǔ)言機(jī)器之間還存在一層操作系統(tǒng)虛擬機(jī),主要用于控制和管理操作系統(tǒng)的全部硬件和軟件資源(隨著超大規(guī)模集成電路技術(shù)的不斷發(fā)展,一些操作系統(tǒng)的軟件功能逐步由硬件來(lái)替換,例如目前的操作系統(tǒng)已經(jīng)實(shí)現(xiàn)了部分程序的固化,簡(jiǎn)稱固件,將程序永久性的存儲(chǔ)在 ROM 中)。機(jī)器語(yǔ)言機(jī)器還可以繼續(xù)分解成微程序機(jī)器,將每一條機(jī)器指令翻譯成一組微指令(微程序)進(jìn)行執(zhí)行。

          上述虛擬機(jī)所提供的語(yǔ)言轉(zhuǎn)換程序被稱為編譯器,主要作用是將某種語(yǔ)言編寫的源程序轉(zhuǎn)換成一個(gè)等價(jià)的機(jī)器語(yǔ)言程序,編譯器的作用如下圖所示:

          image.png

          例如 C 語(yǔ)言,可以先通過(guò) gcc 編譯器生成 Linux 和 Windows 下的目標(biāo) .o 和 .obj 文件(object 文件,即目標(biāo)文件),然后將目標(biāo)文件與底層系統(tǒng)庫(kù)文件、應(yīng)用程序庫(kù)文件以及啟動(dòng)文件鏈接成可執(zhí)行文件在目標(biāo)機(jī)器上執(zhí)行。

          溫馨提示:感興趣的同學(xué)可以了解一下 ARM 芯片的程序運(yùn)行原理,包括使用 IDE 進(jìn)行程序的編譯(IDE 內(nèi)置編譯器,主流編譯器包含 ARMCC、IAR 以及 GCC FOR ARM 等,其中一些編譯器僅僅隨著 IDE 進(jìn)行捆綁發(fā)布,不提供獨(dú)立使用的能力,而一些編譯器則隨著 IDE 進(jìn)行發(fā)布的同時(shí),還提供命令行接口的獨(dú)立使用方式)、通過(guò)串口進(jìn)行程序下載(下載到芯片的代碼區(qū)初始啟動(dòng)地址映射的存儲(chǔ)空間地址)、啟動(dòng)的存儲(chǔ)空間地址映射(包括系統(tǒng)存儲(chǔ)器、閃存 FLASH、內(nèi)置 SRAM 等)、芯片的程序啟動(dòng)模式引腳 BOOT 的設(shè)置(例如調(diào)試代碼時(shí)常常選擇內(nèi)置 SRAM、真正程序運(yùn)行的時(shí)候選擇閃存 FLASH)等。

          如果某種高級(jí)語(yǔ)言或者應(yīng)用語(yǔ)言(例如用于人工智能的計(jì)算機(jī)設(shè)計(jì)語(yǔ)言)轉(zhuǎn)換的目標(biāo)語(yǔ)言不是特定計(jì)算機(jī)的匯編語(yǔ)言,而是面向另一種高級(jí)程序語(yǔ)言(很多研究性的編譯器將 C 作為目標(biāo)語(yǔ)言),那么還需要將目標(biāo)高級(jí)程序語(yǔ)言再進(jìn)行一次額外的編譯才能得到最終的目標(biāo)程序,這種編譯器可稱為源到源的轉(zhuǎn)換器。

          除此之外,有些程序設(shè)計(jì)語(yǔ)言將編譯的過(guò)程和最終轉(zhuǎn)換成目標(biāo)程序進(jìn)行執(zhí)行的過(guò)程混合在一起,這種語(yǔ)言轉(zhuǎn)換程序通常被稱為解釋器,主要作用是將某種語(yǔ)言編寫的源程序作為輸入,將該源程序執(zhí)行的結(jié)果作為輸出,解釋器的作用如下圖所示:

          image.png

          解釋器和編譯器有很多相似之處,都需要對(duì)源程序進(jìn)行分析,并轉(zhuǎn)換成目標(biāo)機(jī)器可識(shí)別的機(jī)器語(yǔ)言進(jìn)行執(zhí)行。只是解釋器是在轉(zhuǎn)換源程序的同時(shí)立馬執(zhí)行對(duì)應(yīng)的機(jī)器語(yǔ)言(轉(zhuǎn)換和執(zhí)行的過(guò)程不分離),而編譯器得先把源程序全部轉(zhuǎn)換成機(jī)器語(yǔ)言并產(chǎn)生目標(biāo)文件,然后將目標(biāo)文件寫入相應(yīng)的程序存儲(chǔ)器進(jìn)行執(zhí)行(轉(zhuǎn)換和執(zhí)行的過(guò)程分離)。例如 Perl、Scheme、APL 使用解釋器進(jìn)行轉(zhuǎn)換, C、C++ 則使用編譯器進(jìn)行轉(zhuǎn)換,而 Java 和 JavaScript 的轉(zhuǎn)換既包含了編譯過(guò)程,也包含了解釋過(guò)程。

          6、簡(jiǎn)單描述一下 Babel 的編譯過(guò)程?

          7、JavaScript 中的數(shù)組和函數(shù)在內(nèi)存中是如何存儲(chǔ)的?

          JavaScript 中的數(shù)組存儲(chǔ)大致需要分為兩種情況:

          • 同種類型數(shù)據(jù)的數(shù)組分配連續(xù)的內(nèi)存空間

          • 存在非同種類型數(shù)據(jù)的數(shù)組使用哈希映射分配內(nèi)存空間

          溫馨提示:可以想象一下連續(xù)的內(nèi)存空間只需要根據(jù)索引(指針)直接計(jì)算存儲(chǔ)位置即可。如果是哈希映射那么首先需要計(jì)算索引值,然后如果索引值有沖突的場(chǎng)景下還需要進(jìn)行二次查找(需要知道哈希的存儲(chǔ)方式)。

          8、瀏覽器和 Node.js 中的事件循環(huán)機(jī)制有什么區(qū)別?

          閱讀鏈接:面試分享:兩年工作經(jīng)驗(yàn)成功面試阿里 P6 總結(jié)[2] - 了解 Event Loop 嗎?

          9、ES6 Modules 相對(duì)于 CommonJS 的優(yōu)勢(shì)是什么?

          10、高級(jí)程序設(shè)計(jì)語(yǔ)言是如何編譯成機(jī)器語(yǔ)言的?

          11、編譯器一般由哪幾個(gè)階段組成?數(shù)據(jù)類型檢查一般在什么階段進(jìn)行?

          12、編譯過(guò)程中虛擬機(jī)的作用是什么?

          13、什么是中間代碼(IR),它的作用是什么?

          14、什么是交叉編譯?

          編譯器的設(shè)計(jì)是一個(gè)非常龐大和復(fù)雜的軟件系統(tǒng)設(shè)計(jì),在真正設(shè)計(jì)的時(shí)候需要解決兩個(gè)相對(duì)重要的問(wèn)題:

          • 如何分析不同高級(jí)程序語(yǔ)言設(shè)計(jì)的源程序

          • 如何將源程序的功能等價(jià)映射到不同指令系統(tǒng)的目標(biāo)機(jī)器

          為了解決上述兩項(xiàng)問(wèn)題,編譯器的設(shè)計(jì)最終被分解成前端(注意這里所說(shuō)的不是 Web 前端)和后端兩個(gè)編譯階段,前端用于解決第一個(gè)問(wèn)題,而后端用于解決第二個(gè)問(wèn)題,具體如下圖所示:

          image.png

          上圖中的中間表示(Intermediate Representation,IR)是程序結(jié)構(gòu)的一種表現(xiàn)方式,它會(huì)比 AST(后續(xù)講解)更加接近匯編語(yǔ)言或者指令集,同時(shí)也會(huì)保留源程序中的一些高級(jí)信息,除此之外 ,它的種類很多,包括三地址碼(Three Address Code, TAC)[3]靜態(tài)單賦值形式(Static Single Assignment Form, SSA)[4]以及基于棧的 IR 等,具體作用包括:

          • 靠近前端部分主要適配不同的源程序,靠近后端部分主要適配不同的指令集,更易于編譯器的錯(cuò)誤調(diào)試,容易識(shí)別是 IR 之前還是之后出問(wèn)題

          • 如下左圖所示,如果沒有 IR,那么源程序到指令集之間需要進(jìn)行一一適配,而有了中間表示,則可以使得編譯器的職責(zé)更加分離,源程序的編譯更多關(guān)注如何轉(zhuǎn)換成 IR,而不是去適配不同的指令集

          • IR 本身可以做到多趟迭代從而優(yōu)化源程序,在每一趟迭代的過(guò)程中可以研究代碼并記錄優(yōu)化的細(xì)節(jié),方便后續(xù)的迭代查找并利用這些優(yōu)化信息,最終可以高效輸出更優(yōu)的目標(biāo)程序

          image.png

          由于 IR 可以進(jìn)行多趟迭代進(jìn)行程序優(yōu)化,因此在編譯器中可插入一個(gè)新的優(yōu)化階段,如下圖所示:

          image.png

          優(yōu)化器可以對(duì) IR 處理一遍或者多遍,從而生成更快執(zhí)行速度(例如找到循環(huán)中不變的計(jì)算并對(duì)其進(jìn)行優(yōu)化從而減少運(yùn)算次數(shù))或者更小體積的目標(biāo)程序,也可能用于產(chǎn)生更少異常或者更低功耗的目標(biāo)程序。除此之外,前端和后端內(nèi)部還可以細(xì)分為多個(gè)處理步驟,具體如下圖所示:

          image.png

          優(yōu)化器中的每一遍優(yōu)化處理都可以使用一個(gè)或多個(gè)優(yōu)化技術(shù)來(lái)改進(jìn)代碼,每一趟處理最終都是讀寫 IR 的操作,這樣不僅僅可以使得優(yōu)化可以更加高效,同時(shí)也可以降低優(yōu)化的復(fù)雜度,還提高了優(yōu)化的靈活性,可以使得編譯器配置不同的優(yōu)化選項(xiàng),達(dá)到組合優(yōu)化的效果。

          15、發(fā)布 / 訂閱模式和觀察者模式的區(qū)別是什么?

          閱讀鏈接:基于 Vue 實(shí)現(xiàn)一個(gè)簡(jiǎn)易 MVVM [5]- 觀察者模式和發(fā)布/訂閱模式

          16、裝飾器模式一般會(huì)在什么場(chǎng)合使用?

          17、談?wù)勀銓?duì)大型項(xiàng)目的代碼解耦設(shè)計(jì)理解?什么是 Ioc?一般 DI 采用什么設(shè)計(jì)模式實(shí)現(xiàn)?

          18、列舉你所了解的編程范式?

          編程范式(Programming paradigm)是指計(jì)算機(jī)編程的基本風(fēng)格或者典型模式,可以簡(jiǎn)單理解為編程學(xué)科中實(shí)踐出來(lái)的具有哲學(xué)和理論依據(jù)的一些經(jīng)典原型。常見的編程范式有:

          • 面向過(guò)程(Process Oriented Programming,POP)

          • 面向?qū)ο螅∣bject Oriented Programming,OOP)

          • 面向接口(Interface Oriented Programming, IOP)

          • 面向切面(Aspect Oriented Programming,AOP)

          • 函數(shù)式(Funtional Programming,F(xiàn)P)

          • 響應(yīng)式(Reactive Programming,RP)

          • 函數(shù)響應(yīng)式(Functional Reactive Programming,F(xiàn)RP)

          閱讀鏈接::如果你對(duì)于編程范式的定義相對(duì)模糊,可以繼續(xù)閱讀 What is the precise definition of programming paradigm?[6] 了解更多。

          不同的語(yǔ)言可以支持多種不同的編程范式,例如 C 語(yǔ)言支持 POP 范式,C++ 和 Java 語(yǔ)言支持 OOP 范式,Swift 語(yǔ)言則可以支持 FP 范式,而 Web 前端中的 JavaScript 可以支持上述列出的所有編程范式。

          19、什么是面向切面(AOP)的編程?

          20、什么是函數(shù)式編程?

          顧名思義,函數(shù)式編程是使用函數(shù)來(lái)進(jìn)行高效處理數(shù)據(jù)或數(shù)據(jù)流的一種編程方式。在數(shù)學(xué)中,函數(shù)的三要素是定義域、值域和**對(duì)應(yīng)關(guān)系。假設(shè) A、B 是非空數(shù)集,對(duì)于集合 A 中的任意一個(gè)數(shù) x,在集合 B 中都有唯一確定的數(shù) f(x) 和它對(duì)應(yīng),那么可以將 f 稱為從 A 到 B 的一個(gè)函數(shù),記作:y = f(x)。在函數(shù)式編程中函數(shù)的概念和數(shù)學(xué)函數(shù)的概念類似,主要是描述形參 x 和返回值 y 之間的對(duì)應(yīng)關(guān)系,**如下圖所示:

          溫馨提示:圖片來(lái)自于簡(jiǎn)明 JavaScript 函數(shù)式編程——入門篇[7]

          在實(shí)際的編程中,可以將各種明確對(duì)應(yīng)關(guān)系的函數(shù)進(jìn)行傳遞、組合從而達(dá)到處理數(shù)據(jù)的最終目的。在此過(guò)程中,我們的關(guān)注點(diǎn)不在于如何去實(shí)現(xiàn)**對(duì)應(yīng)關(guān)系,**而在于如何將各種已有的對(duì)應(yīng)關(guān)系進(jìn)行高效聯(lián)動(dòng),從而可快速進(jìn)行數(shù)據(jù)轉(zhuǎn)換,達(dá)到最終的數(shù)據(jù)處理目的,提供開發(fā)效率。

          簡(jiǎn)單示例

          盡管你對(duì)函數(shù)式編程的概念有所了解,但是你仍然不知道函數(shù)式編程到底有什么特點(diǎn)。這里我們?nèi)匀荒?OOP 編程范式來(lái)舉例,假設(shè)希望通過(guò) OOP 編程來(lái)解決數(shù)學(xué)的加減乘除問(wèn)題:

          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());

          我們希望通過(guò)上述程序來(lái)解決 (1 + 2) * 2 + 1 * 2 的問(wèn)題,但實(shí)際上計(jì)算出來(lái)的結(jié)果是 24,因?yàn)樵诖a內(nèi)部有一個(gè) this.value 的狀態(tài)值需要跟蹤,這會(huì)使得結(jié)果不符合預(yù)期。  接下來(lái)我們采用函數(shù)式編程的方式:

          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));

          以上程序計(jì)算的結(jié)果是 8,完全符合預(yù)期。我們知道了 add 和 multiply 兩個(gè)函數(shù)的實(shí)際對(duì)應(yīng)關(guān)系,通過(guò)將對(duì)應(yīng)關(guān)系進(jìn)行有效的組合和傳遞,達(dá)到了最終的計(jì)算結(jié)果。除此之外,這兩個(gè)函數(shù)還可以根據(jù)數(shù)學(xué)定律得出更優(yōu)雅的組合方式:

          add(multiply(add(a, b), b), multiply(a, b));



          // 根據(jù)數(shù)學(xué)定律分配律:a * b  +  a * c = a * (b + c),得出:

          // (a + b) * b + a * b = (2a + b) * b



          // 簡(jiǎn)化上述函數(shù)的組合方式

          multiply(add(add(a, a), b), b);

          我們完全不需要追蹤類似于 OOP 編程范式中可能存在的內(nèi)部狀態(tài)數(shù)據(jù),事實(shí)上對(duì)于數(shù)學(xué)定律中的結(jié)合律、交換律、同一律以及分配律,上述的函數(shù)式編程代碼足可以勝任。

          原則

          通過(guò)上述簡(jiǎn)單的例子可以發(fā)現(xiàn),要實(shí)現(xiàn)高可復(fù)用的函數(shù)**(對(duì)應(yīng)關(guān)系)**,一定要遵循某些特定的原則,否則在使用的時(shí)候可能無(wú)法進(jìn)行高效的傳遞和組合,例如

          • 高內(nèi)聚低耦合

          • 最小意外原則

          • 單一職責(zé)原則

          • ...

          如果你之前經(jīng)常進(jìn)行無(wú)原則性的代碼設(shè)計(jì),那么在設(shè)計(jì)過(guò)程中可能會(huì)出現(xiàn)各種出乎意料的問(wèn)題(這是為什么新手老是出現(xiàn)一些稀奇古怪問(wèn)題的主要原因)。函數(shù)式編程可以有效的通過(guò)一些原則性的約束使你設(shè)計(jì)出更加健壯和優(yōu)雅的代碼,并且在不斷的實(shí)踐過(guò)程中進(jìn)行經(jīng)驗(yàn)式疊加,從而提高開發(fā)效率。

          特點(diǎn)

          雖然我們?cè)谑褂煤瘮?shù)的過(guò)程中更多的不再關(guān)注函數(shù)如何實(shí)現(xiàn)(對(duì)應(yīng)關(guān)系),但是真正在使用和設(shè)計(jì)函數(shù)的時(shí)候需要注意以下一些特點(diǎn):

          • 聲明式(Declarative Programming)

          • 一等公民(First Class Function)

          • 純函數(shù)(Pure Function)

          • 無(wú)狀態(tài)和數(shù)據(jù)不可變(Statelessness and Immutable Data)

          • ...

          聲明式

          我們以前設(shè)計(jì)的代碼通常是命令式編程方式,這種編程方式往往注重具體的實(shí)現(xiàn)的過(guò)程(對(duì)應(yīng)關(guān)系),而函數(shù)式編程則采用聲明式的編程方式,往往注重如何去組合已有的**對(duì)應(yīng)關(guān)系。**簡(jiǎn)單舉個(gè)例子:

          // 命令式

          const array = [0.81.72.53.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 不會(huì)修改原有數(shù)組,而是產(chǎn)生新的數(shù)組返回

          [0.81.72.53.4].map((item) => Math.floor(item)).filter((item) => item > 1);

          命令式代碼一步一步的告訴計(jì)算機(jī)需要執(zhí)行哪些語(yǔ)句,需要關(guān)心變量的實(shí)例化情況、循環(huán)的具體過(guò)程以及跟蹤變量狀態(tài)的變化過(guò)程。聲明式代碼更多的不再關(guān)心代碼的具體執(zhí)行過(guò)程,而是采用表達(dá)式的組合變換去處理問(wèn)題,不再?gòu)?qiáng)調(diào)怎么做,而是指明**做什么。**聲明式編程方式可以將我們?cè)O(shè)計(jì)代碼的關(guān)注點(diǎn)徹底從過(guò)程式解放出來(lái),從而提高開發(fā)效率。

          一等公民

          在 JavaScript 中,函數(shù)的使用非常靈活,例如可以對(duì)函數(shù)進(jìn)行以下操作:

          interface IHello {

            (name: string): string;

            key?: string;

            arr?: number[];

            fn?(name: string): string;

          }



          // 函數(shù)聲明提升

          console.log(hello instanceof Object); // true



          // 函數(shù)聲明提升

          // hello 和其他引用類型的對(duì)象一樣,都有屬性和方法

          hello.key = 'key';

          hello.arr = [12];

          hello.fn = function (name: string{

            return `hello.fn, ${name}`;

          };



          // 函數(shù)聲明提升

          // 注意函數(shù)表達(dá)式不能在聲明前執(zhí)行,例如不能在這里使用 helloCopy('world')

          hello('world');



          // 函數(shù)

          // 創(chuàng)建新的函數(shù)對(duì)象,將函數(shù)的引用指向變量 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



          // 函數(shù)表達(dá)式

          const helloCopy: IHello = hello;

          helloCopy('world');



          function transferHello(name: string, hello: Hello{

            return hello('world');

          }



          // 把函數(shù)對(duì)象當(dāng)作實(shí)參傳遞

          transferHello('world', helloCopy);



          // 把匿名函數(shù)當(dāng)作實(shí)參傳遞

          transferHello('world'function (name: string{

            return `hello, ${name}`;

          });



          通過(guò)以上示例可以看出,函數(shù)繼承至對(duì)象并擁有對(duì)象的特性。在 JavaScript 中可以對(duì)函數(shù)進(jìn)行參數(shù)傳遞、變量賦值或數(shù)組操作等等,因此把函數(shù)稱為一等公民。函數(shù)式編程的核心就是對(duì)函數(shù)進(jìn)行組合或傳遞,JavaScript 中函數(shù)這種靈活的特性是滿足函數(shù)式編程的重要條件。

          純函數(shù)

          純函數(shù)是是指在相同的參數(shù)調(diào)用下,函數(shù)的返回值唯一不變。這跟數(shù)學(xué)中函數(shù)的映射關(guān)系類似,同樣的 x 不可能映射多個(gè)不同的 y。使用函數(shù)式編程會(huì)使得函數(shù)的調(diào)用非常穩(wěn)定,從而降低 Bug 產(chǎn)生的機(jī)率。當(dāng)然要實(shí)現(xiàn)純函數(shù)的這種特性,需要函數(shù)不能包含以下一些副作用:

          • 操作 Http 請(qǐng)求

          • 可變數(shù)據(jù)(包括在函數(shù)內(nèi)部改變輸入?yún)?shù))

          • DOM 操作

          • 打印日志

          • 訪問(wèn)系統(tǒng)狀態(tài)

          • 操作文件系統(tǒng)

          • 操作數(shù)據(jù)庫(kù)

          • ...

          從以上常見的一些副作用可以看出,純函數(shù)的實(shí)現(xiàn)需要遵循最小意外原則,為了確保函數(shù)的穩(wěn)定唯一的輸入和輸出,盡量應(yīng)該避免與函數(shù)外部的環(huán)境進(jìn)行任何交互行為,從而防止外部環(huán)境對(duì)函數(shù)內(nèi)部產(chǎn)生無(wú)法預(yù)料的影響。純函數(shù)的實(shí)現(xiàn)應(yīng)該自給自足,舉幾個(gè)例子:

          // 如果使用 const 聲明 min 變量(基本數(shù)據(jù)類型),則可以保證以下函數(shù)的純粹性

          let min: number = 1;



          // 非純函數(shù)

          // 依賴外部環(huán)境變量 min,一旦 min 發(fā)生變化則輸入和返回不唯一

          function isEqual(num: number): boolean {

            return num === min;

          }



          // 純函數(shù)

          function isEqual(num: number): boolean {

            return num === 1;

          }



          // 非純函數(shù)

          function request<TS>(url: string, params: T): Promise<S{

            // 會(huì)產(chǎn)生請(qǐng)求成功和請(qǐng)求失敗兩種結(jié)果,返回的結(jié)果可能不唯一

            return $.getJson(url, params);

          }



          // 純函數(shù)

          function request<TS>(url: string, params: T) : () => Promise<S{

            return function({

              return $.getJson(url, params);

            }

          }

          純函數(shù)的特性使得函數(shù)式編程具備以下特性:

          • 可緩存性(Cacheable)

          • 可移植性(Portable)

          • 可測(cè)試性(Testable)

          可緩存性和可測(cè)試性基于純函數(shù)輸入輸出唯一不變的特性,可移植性則主要基于純函數(shù)不依賴外部環(huán)境的特性。這里舉一個(gè)可緩存的例子:

          interface ICache<T> {

            [arg: string]: T;

          }



          interface ISquare<T> {

            (x: T): T;

          }



          // 簡(jiǎn)單的緩存函數(shù)(忽略通用性和健壯性)

          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];

            };

          }



          // 純函數(shù)

          function square(x: number): number {

            return x * x;

          }



          const memoSquare = memoize<number>(square);

          memoSquare(4);



          // 不會(huì)再次調(diào)用純函數(shù) square,而是直接從緩存中獲取值

          // 由于輸入和輸出的唯一性,獲取緩存結(jié)果可靠穩(wěn)定

          // 提升代碼的運(yùn)行效率

          memoSquare(4);

          無(wú)狀態(tài)和數(shù)據(jù)不可變

          在函數(shù)式編程的簡(jiǎn)單示例中已經(jīng)可以清晰的感受到函數(shù)式編程絕對(duì)不能依賴內(nèi)部狀態(tài),而在純函數(shù)中則說(shuō)明了函數(shù)式編程不能依賴外部的環(huán)境或狀態(tài),因?yàn)橐坏┮蕾嚨臓顟B(tài)變化,不能保證函數(shù)根據(jù)對(duì)應(yīng)關(guān)系所計(jì)算的返回值因?yàn)闋顟B(tài)的變化仍然保持不變。

          這里單獨(dú)講解一下數(shù)據(jù)不可變,在 JavaScript 中有很多數(shù)組操作的方法,舉個(gè)例子:

          const arr = [123];



          console.log(arr.slice(02)); // [1, 2]

          console.log(arr); // [1, 2, 3]

          console.log(arr.slice(02)); // [1, 2]

          console.log(arr); // [1, 2, 3]



          console.log(arr.splice(01)); // [1]

          console.log(arr); // [2, 3]

          console.log(arr.splice(01)); // [2]

          console.log(arr); // [3]

          這里的 slice 方法多次調(diào)用都不會(huì)改變?cè)袛?shù)組,且會(huì)產(chǎn)生相同的輸出。而 splice 每次調(diào)用都在修改原數(shù)組,且產(chǎn)生的輸出也不相同。  在函數(shù)式編程中,這種會(huì)改變?cè)袛?shù)據(jù)的函數(shù)已經(jīng)不再是純函數(shù),應(yīng)該盡量避免使用。

          閱讀鏈接:如果想要了解更深入的函數(shù)式編程知識(shí)點(diǎn),可以額外閱讀函數(shù)式編程指北[8]

          21、響應(yīng)式編程的使用場(chǎng)景有哪些?

          響應(yīng)式編程是一種基于觀察者(發(fā)布 / 訂閱)模式并且面向異步(Asynchronous)數(shù)據(jù)流(Data Stream)和變化傳播的聲明式編程范式。響應(yīng)式編程主要適用的場(chǎng)景包含:

          • 用戶和系統(tǒng)發(fā)起的連續(xù)事件處理,例如鼠標(biāo)的點(diǎn)擊、鍵盤的按鍵或者通信設(shè)備發(fā)起的信號(hào)等

          • 非可靠的網(wǎng)絡(luò)或者通信處理(例如 HTTP 網(wǎng)絡(luò)的請(qǐng)求重試)

          • 連續(xù)的異步 IO 處理

          • 復(fù)雜的繼發(fā)事務(wù)處理(例如一次事件涉及到多個(gè)繼發(fā)的網(wǎng)絡(luò)請(qǐng)求)

          • 高并發(fā)的消息處理(例如 IM 聊天)

          • ...

          語(yǔ)法

          22、如何實(shí)現(xiàn)一個(gè)上中下三行布局,頂部和底部最小高度是 100px,中間自適應(yīng)?

          23、如何判斷一個(gè)元素 CSS 樣式溢出,從而可以選擇性的加 title 或者 Tooltip?

          24、如何讓 CSS 元素左側(cè)自動(dòng)溢出(... 溢出在左側(cè))?

          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).

          具體查看:https://developer.mozilla.org/en-US/docs/Web/CSS/direction

          25、什么是沙箱?瀏覽器的沙箱有什么作用?

          26、如何處理瀏覽器中表單項(xiàng)的密碼自動(dòng)填充問(wèn)題?

          27、Hash 和 History 路由的區(qū)別和優(yōu)缺點(diǎn)?

          28、JavaScript 中對(duì)象的屬性描述符有哪些?分別有什么作用?

          29、JavaScript 中 console 有哪些 api ?


          The console object provides access to the browser's debugging console (e.g. the Web console[9] 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

          具體查看:https://developer.mozilla.org/en-US/docs/Web/API/console

          30、 簡(jiǎn)單對(duì)比一下 Callback、Promise、Generator、Async 幾個(gè)異步 API 的優(yōu)劣?

          在 JavaScript 中利用事件循環(huán)機(jī)制[10](Event Loop)可以在單線程中實(shí)現(xiàn)非阻塞式、異步的操作。例如

          • Node.js 中的 Callback、EventEmitter[11]Stream[12]

          • ES6 中的 Promise[13]Generator[14]

          • ES2017 中的 Async[15]

          • 三方庫(kù) RxJS、Q[16] 、Co、[17]Bluebird[18]

          我們重點(diǎn)來(lái)看一下常用的幾種編程方式(Callback、Promise、Generator、Async)在語(yǔ)法糖上帶來(lái)的優(yōu)劣對(duì)比。

          Callback

          Callback(回調(diào)函數(shù))是在 Web 前端開發(fā)中經(jīng)常會(huì)使用的編程方式。這里舉一個(gè)常用的定時(shí)器示例:

          export interface IObj {

            value: string;

            deferExec(): void;

            deferExecAnonymous(): void;

            console(): void;

          }



          export const obj: IObj = {

            value: 'hello',



            deferExecBind() {

              // 使用箭頭函數(shù)可達(dá)到一樣的效果

              setTimeout(this.console.bind(this), 1000);

            },



            deferExec() {

              setTimeout(this.console, 1000);

            },



            console() {

              console.log(this.value);

            },

          };



          obj.deferExecBind(); // hello

          obj.deferExec(); // undefined

          回調(diào)函數(shù)經(jīng)常會(huì)因?yàn)檎{(diào)用環(huán)境的變化而導(dǎo)致 this 的指向性變化。除此之外,使用回調(diào)函數(shù)來(lái)處理多個(gè)繼發(fā)的異步任務(wù)時(shí)容易導(dǎo)致回調(diào)地獄(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{

                  // 假設(shè)在業(yè)務(wù)中 fileD 的讀寫依次依賴 fileA、fileB 和 fileC

                  // 或者經(jīng)常也可以在業(yè)務(wù)中看到多個(gè) HTTP 請(qǐng)求的操作有前后依賴(繼發(fā) HTTP 請(qǐng)求)

                  // 這些異步任務(wù)之間縱向嵌套強(qiáng)耦合,無(wú)法進(jìn)行橫向復(fù)用

                  // 如果某個(gè)異步發(fā)生變化,那它的所有上層或下層回調(diào)可能都需要跟著變化(比如 fileA 和 fileB 的依賴關(guān)系倒置)

                  // 因此稱這種現(xiàn)象為 回調(diào)地獄

                  // ....

                });

              });

            });

          });

          回調(diào)函數(shù)不能通過(guò) return 返回?cái)?shù)據(jù),比如我們希望調(diào)用帶有回調(diào)參數(shù)的函數(shù)并返回異步執(zhí)行的結(jié)果時(shí),只能通過(guò)再次回調(diào)的方式進(jìn)行參數(shù)傳遞:

          // 希望延遲 3s 后執(zhí)行并拿到結(jié)果

          function getAsyncResult(result: number{

            setTimeout(() => {

              return result * 3;

            }, 1000);

          }



          // 盡管這是常規(guī)的編程思維方式

          const result = getAsyncResult(3000);

          // 但是打印 undefined

          console.log('result: ', result);



          function getAsyncResultWithCb(result: number, cb: (result: number) => void{

            setTimeout(() => {

              cb(result * 3);

            }, 1000);

          }



          // 通過(guò)回調(diào)的形式獲取結(jié)果

          getAsyncResultWithCb(3000(result) => {

            console.log('result: ', result); // 9000

          });



          對(duì)于 JavaScript 中標(biāo)準(zhǔn)的異步 API 可能無(wú)法通過(guò)在外部進(jìn)行 try...catch... 的方式進(jìn)行錯(cuò)誤捕獲:

          try {

            setTimeout(() => {

              // 下述是異常代碼

              // 你可以在回調(diào)函數(shù)的內(nèi)部進(jìn)行 try...catch...

              console.log(a.b.c)

            }, 1000)



          catch(err) {

            // 這里不會(huì)執(zhí)行

            // 進(jìn)程會(huì)被終止

            console.error(err)

          }

          上述示例講述的都是 JavaScript 中標(biāo)準(zhǔn)的異步 API ,如果使用一些三方的異步 API 并且提供了回調(diào)能力時(shí),這些 API 可能是非受信的,在真正使用的時(shí)候會(huì)因?yàn)?strong>執(zhí)行反轉(zhuǎn)(回調(diào)函數(shù)的執(zhí)行權(quán)在三方庫(kù)中)導(dǎo)致以下一些問(wèn)題:

          • 使用者的回調(diào)函數(shù)設(shè)計(jì)沒有進(jìn)行錯(cuò)誤捕獲,而恰恰三方庫(kù)進(jìn)行了錯(cuò)誤捕獲卻沒有拋出錯(cuò)誤處理信息,此時(shí)使用者很難感知到自己設(shè)計(jì)的回調(diào)函數(shù)是否有錯(cuò)誤

          • 使用者難以感知到三方庫(kù)的回調(diào)時(shí)機(jī)和回調(diào)次數(shù),這個(gè)回調(diào)函數(shù)執(zhí)行的權(quán)利控制在三方庫(kù)手中

          • 使用者無(wú)法更改三方庫(kù)提供的回調(diào)參數(shù),回調(diào)參數(shù)可能無(wú)法滿足使用者的訴求

          • ...

          舉個(gè)簡(jiǎn)單的例子:

          interface ILib<T> {

            params: T;

            emit(params: T): void;

            on(callback: (params: T) => void): void;

          }



          // 假設(shè)以下是一個(gè)三方庫(kù),并發(fā)布成了npm 包

          export const lib: ILib<string> = {

            params: '',



            emit(params) {

              this.params = params;

            },



            on(callback) {

              try {

                // callback 回調(diào)執(zhí)行權(quán)在 lib 上

                // lib 庫(kù)可以決定回調(diào)執(zhí)行多次

                callback(this.params);

                callback(this.params);

                callback(this.params);

                // lib 庫(kù)甚至可以決定回調(diào)延遲執(zhí)行

                // 異步執(zhí)行回調(diào)函數(shù)

                setTimeout(() => {

                  callback(this.params);

                }, 3000);

              } catch (err) {

                // 假設(shè) lib 庫(kù)的捕獲沒有拋出任何異常信息

              }

            },

          };



          // 開發(fā)者引入 lib 庫(kù)開始使用

          lib.emit('hello');



          lib.on((value) => {

            // 使用者希望 on 里的回調(diào)只執(zhí)行一次

           // 這里的回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)是由三方庫(kù) lib 決定

            // 實(shí)際上打印四次,并且其中一次是異步執(zhí)行

            console.log(value);

          });



          lib.on((value) => {

            // 下述是異常代碼

            // 但是執(zhí)行下述代碼不會(huì)拋出任何異常信息

            // 開發(fā)者無(wú)法感知自己的代碼設(shè)計(jì)錯(cuò)誤

            console.log(value.a.b.c)

          });

          Promise

          Callback 的異步操作形式除了會(huì)造成回調(diào)地獄,還會(huì)造成難以測(cè)試的問(wèn)題。ES6 中的 Promise (基于 Promise A +[19] 規(guī)范的異步編程解決方案)利用有限狀態(tài)機(jī)[20]的原理來(lái)解決異步的處理問(wèn)題,Promise 對(duì)象提供了統(tǒng)一的異步編程 API,它的特點(diǎn)如下:

          • Promise 對(duì)象的執(zhí)行狀態(tài)不受外界影響。Promise 對(duì)象的異步操作有三種狀態(tài): pending(進(jìn)行中)、 fulfilled(已成功)和 rejected(已失敗) ,只有 Promise 對(duì)象本身的異步操作結(jié)果可以決定當(dāng)前的執(zhí)行狀態(tài),任何其他的操作無(wú)法改變狀態(tài)的結(jié)果

          • Promise 對(duì)象的執(zhí)行狀態(tài)不可變。Promise 的狀態(tài)只有兩種變化可能:從 pending(進(jìn)行中)變?yōu)?nbsp;fulfilled(已成功)或從 pending(進(jìn)行中)變?yōu)?nbsp;rejected(已失敗)

          溫馨提示:有限狀態(tài)機(jī)提供了一種優(yōu)雅的解決方式,異步的處理本身可以通過(guò)異步狀態(tài)的變化來(lái)觸發(fā)相應(yīng)的操作,這會(huì)比回調(diào)函數(shù)在邏輯上的處理更加合理,也可以降低代碼的復(fù)雜度。

          Promise 對(duì)象的執(zhí)行狀態(tài)不可變示例如下:

          const promise = new Promise<number>((resolve, reject) => {

            // 狀態(tài)變更為 fulfilled 并返回結(jié)果 1 后不會(huì)再變更狀態(tài)

            resolve(1);

            // 不會(huì)變更狀態(tài)

            reject(4);

          }
          );



          promise

            .then((result) => {

              // 在 ES 6 中 Promise 的 then 回調(diào)執(zhí)行是異步執(zhí)行(微任務(wù))

              // 在當(dāng)前 then 被調(diào)用的那輪事件循環(huán)(Event Loop)的末尾執(zhí)行

              console.log('result: ', result);

            }
          )

            .catch((error) => {

              // 不執(zhí)行

              console.error('error: ', error);

            }
          );

          假設(shè)要實(shí)現(xiàn)兩個(gè)繼發(fā)的 HTTP 請(qǐng)求,第一個(gè)請(qǐng)求接口返回的數(shù)據(jù)是第二個(gè)請(qǐng)求接口的參數(shù),使用回調(diào)函數(shù)的實(shí)現(xiàn)方式如下所示(這里使用 setTimeout 來(lái)指代異步請(qǐng)求):

          // 回調(diào)地獄

          const doubble = (result: number, callback: (finallResult: number) => void) => {

            // Mock 第一個(gè)異步請(qǐng)求

            setTimeout(() => {

              // Mock 第二個(gè)異步請(qǐng)求(假設(shè)第二個(gè)請(qǐng)求的參數(shù)依賴第一個(gè)請(qǐng)求的返回結(jié)果)

              setTimeout(() => {

                callback(result * 2);

              }, 2000);

            }, 1000);

          };



          doubble(1000(result) => {

            console.log('result: ', result);

          });

          溫馨提示:繼發(fā)請(qǐng)求的依賴關(guān)系非常常見,例如人員基本信息管理系統(tǒng)的開發(fā)中,經(jīng)常需要先展示組織樹結(jié)構(gòu),并默認(rèn)加載第一個(gè)組織下的人員列表信息。

          如果采用 Promise 的處理方式則可以規(guī)避上述常見的回調(diào)地獄問(wèn)題:

          const firstPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Mock 異步請(qǐng)求

              // 將 resolve 改成 reject 會(huì)被 catch 捕獲

              setTimeout(() => resolve(result), 1000);

            });

          };



          const nextPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Mock 異步請(qǐng)求

              // 將 resolve 改成 reject 會(huì)被 catch 捕獲

              setTimeout(() => resolve(result * 2), 1000);

            });

          };



          firstPromise(1000)

            .then((result) => {

              return nextPromise(result);

            })

            .then((result) => {

              // 2s 后打印 2000

              console.log('result: ', result);

            })

            // 任何一個(gè) Promise 到達(dá) rejected 狀態(tài)都能被 catch 捕獲

            .catch((err) => {

              console.error('err: ', err);

            });

          Promise 的錯(cuò)誤回調(diào)可以同時(shí)捕獲 firstPromise 和 nextPromise 兩個(gè)函數(shù)的 rejected 狀態(tài)。接下來(lái)考慮以下調(diào)用場(chǎng)景:

          const firstPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Mock 異步請(qǐng)求

              setTimeout(() => resolve(result), 1000);

            });

          };



          const nextPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Mock 異步請(qǐng)求

              setTimeout(() => resolve(result * 2), 1000);

            });

          };



          firstPromise(1000)

            .then((result) => {

              nextPromise(result).then((result) => {

                // 后打印

                console.log('nextPromise result: ', result);

              });

            })

            .then((result) => {

              // 先打印

              // 由于上一個(gè) then 沒有返回值,這里打印 undefined

              console.log('firstPromise result: ', result);

            })

            .catch((err) => {

              console.error('err: ', err);

            });

          首先 Promise 可以注冊(cè)多個(gè) then(放在一個(gè)執(zhí)行隊(duì)列里),并且這些 then 會(huì)根據(jù)上一次返回值的結(jié)果依次執(zhí)行。除此之外,各個(gè) Promise 的 then 執(zhí)行互不干擾。  我們將示例進(jìn)行簡(jiǎn)單的變換:

          const firstPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Mock 異步請(qǐng)求

              setTimeout(() => resolve(result), 1000);

            });

          };



          const nextPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Mock 異步請(qǐng)求

              setTimeout(() => resolve(result * 2), 1000);

            });

          };



          firstPromise(1000)

            .then((result) => {

              // 返回了 nextPromise 的 then 執(zhí)行后的結(jié)果

              return nextPromise(result).then((result) => {

                return result;

              });

            })

            // 接著 nextPromise 的 then 執(zhí)行的返回結(jié)果繼續(xù)執(zhí)行

            .then((result) => {

              // 2s 后打印 2000

              console.log('nextPromise result: ', result);

            })

            .catch((err) => {

              console.error('err: ', err);

            });



          上述例子中的執(zhí)行結(jié)果是因?yàn)?nbsp;then 的執(zhí)行會(huì)返回一個(gè)新的 Promise 對(duì)象,并且如果 then 執(zhí)行后返回的仍然是 Promise 對(duì)象,那么下一個(gè) then 的鏈?zhǔn)秸{(diào)用會(huì)等待該 Promise 對(duì)象的狀態(tài)發(fā)生變化后才會(huì)調(diào)用(能得到這個(gè) Promise 處理的結(jié)果)。接下來(lái)重點(diǎn)看下 Promise 的錯(cuò)誤處理:

          const promise = new Promise<string>((resolve, reject) => {

            // 下述是異常代碼

            console.log(a.b.c);

            resolve('hello');

          }
          );



          promise

            .then((result) => {

              console.log('result: ', result);

            }
          )

            // 去掉 catch 仍然會(huì)拋出錯(cuò)誤,但不會(huì)退出進(jìn)程終止腳本執(zhí)行

            .catch((err) => {

              // 執(zhí)行

              // ReferenceError: a is not defined

              console.error(err);

            }
          );



          setTimeout(() => {

            // 繼續(xù)執(zhí)行

            console.log('hello world!');

          }, 2000
          );

          從上述示例可以看出 Promise 的錯(cuò)誤不會(huì)影響其他代碼的執(zhí)行,只會(huì)影響 Promise 內(nèi)部的代碼本身,因?yàn)?Promise 會(huì)在內(nèi)部對(duì)錯(cuò)誤進(jìn)行異常捕獲,從而保證整體代碼執(zhí)行的穩(wěn)定性。Promise 還提供了其他的一些 API 方便多任務(wù)的執(zhí)行,包括

          • Promise.all:適合多個(gè)異步任務(wù)并發(fā)執(zhí)行但不允許其中任何一個(gè)任務(wù)失敗

          • Promise.race :適合多個(gè)異步任務(wù)搶占式執(zhí)行

          • Promise.allSettled :適合多個(gè)異步任務(wù)并發(fā)執(zhí)行但允許某些任務(wù)失敗

          Promise 相對(duì)于 Callback 對(duì)于異步的處理更加優(yōu)雅,并且能力也更加強(qiáng)大, 但是也存在一些自身的缺點(diǎn):

          • 無(wú)法取消 Promise 的執(zhí)行

          • 無(wú)法在 Promise 外部通過(guò) try...catch... 的形式進(jìn)行錯(cuò)誤捕獲(Promise 內(nèi)部捕獲了錯(cuò)誤)

          • 狀態(tài)單一,每次決斷只能產(chǎn)生一種狀態(tài)結(jié)果,需要不停的進(jìn)行鏈?zhǔn)秸{(diào)用

          溫馨提示:手寫 Promise 是面試官非常喜歡的一道筆試題,本質(zhì)是希望面試者能夠通過(guò)底層的設(shè)計(jì)正確了解 Promise 的使用方式,如果你對(duì) Promise 的設(shè)計(jì)原理不熟悉,可以深入了解一下或者手動(dòng)設(shè)計(jì)一個(gè)。

          Generator

          Promise 解決了 Callback 的回調(diào)地獄問(wèn)題,但也造成了代碼冗余,如果一些異步任務(wù)不支持 Promise 語(yǔ)法,就需要進(jìn)行一層 Promise 封裝。Generator 將 JavaScript 的異步編程帶入了一個(gè)全新的階段,它使得異步代碼的設(shè)計(jì)和執(zhí)行看起來(lái)和同步代碼一致。Generator 使用的簡(jiǎn)單示例如下:

          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 函數(shù)里執(zhí)行的異步代碼看起來(lái)和同步代碼一致

          functiongen(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)



          // 手動(dòng)執(zhí)行 Generator 函數(shù)

          g.next().value.then((res: number) => {

            // 將 firstPromise 的返回值傳遞給第一個(gè) yield 表單式對(duì)應(yīng)的 firstResult

            return g.next(res).value

          }).then((res: number) => {

            // 將 nextPromise 的返回值傳遞給第二個(gè) yield 表單式對(duì)應(yīng)的 nextResult

            return g.next(res).value

          })

          通過(guò)上述代碼,可以看出 Generator 相對(duì)于 Promise 具有以下優(yōu)勢(shì):

          • 豐富了狀態(tài)類型,Generator 通過(guò) next 可以產(chǎn)生不同的狀態(tài)信息,也可以通過(guò) return 結(jié)束函數(shù)的執(zhí)行狀態(tài),相對(duì)于 Promise 的 resolve 不可變狀態(tài)更加豐富

          • Generator 函數(shù)內(nèi)部的異步代碼執(zhí)行看起來(lái)和同步代碼執(zhí)行一致,非常利于代碼的維護(hù)

          • Generator 函數(shù)內(nèi)部的執(zhí)行邏輯和相應(yīng)的狀態(tài)變化邏輯解耦,降低了代碼的復(fù)雜度

          next 可以不停的改變狀態(tài)使得 yield 得以繼續(xù)執(zhí)行的代碼可以變得非常有規(guī)律,例如從上述的手動(dòng)執(zhí)行 Generator 函數(shù)可以看出,完全可以將其封裝成一個(gè)自動(dòng)執(zhí)行的執(zhí)行器,具體如下所示:

          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>



          functiongen(): 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 自動(dòng)執(zhí)行器

          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 => {

                // 通過(guò)遞歸的方式處理相同的邏輯

                next(data)

              })

            }

            // 第一次調(diào)用 next 主要用于啟動(dòng) Generator 函數(shù)

            // 內(nèi)部指針會(huì)從函數(shù)頭部開始執(zhí)行,直到遇到第一個(gè) yield 表達(dá)式

            // 因此第一次 next 傳遞的參數(shù)沒有任何含義(這里傳遞只是為了防止 TS 報(bào)錯(cuò))

            next(0)

          }



          co(gen)



          溫馨提示:TJ Holowaychuk [21]設(shè)計(jì)了一個(gè) Generator 自動(dòng)執(zhí)行器 Co[22],使用 Co 的前提是 yield  命令后必須是 Promise 對(duì)象或者 Thunk 函數(shù)。Co 還可以支持并發(fā)的異步處理,具體可查看官方的 API 文檔[23]

          需要注意的是 Generator 函數(shù)的返回值是一個(gè) Iterator 遍歷器對(duì)象,具體如下所示:

          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>>;



          functiongen(): Gen {

            yield firstPromise(1);

            yield nextPromise(2);

          }



          // 注意使用 next 是繼發(fā)執(zhí)行,而這里是并發(fā)執(zhí)行

          Promise.all([...gen()]).then((res) => {

            console.log('res: ', res);

          });



          for (const promise of gen()) {

            promise.then((res) => {

              console.log('res: ', res);

            });

          }

          Generator 函數(shù)的錯(cuò)誤處理相對(duì)復(fù)雜一些,極端情況下需要對(duì)執(zhí)行和 Generator 函數(shù)進(jìn)行雙重錯(cuò)誤捕獲,具體如下所示:

          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>>;



          functiongen(): Gen {

            try {

              yield firstPromise(1);

              yield nextPromise(2);

            } catch (err) {

              console.error('Generator 函數(shù)錯(cuò)誤捕獲: ', err);

            }

          }



          try {

            const g = gen();

            g.next();

            // 返回 Promise 后還需要通過(guò) Promise.prototype.catch 進(jìn)行錯(cuò)誤捕獲

            g.next();

            // Generator 函數(shù)錯(cuò)誤捕獲

            g.throw('err');

            // 執(zhí)行器錯(cuò)誤捕獲

            g.throw('err');

          catch (err) {

            console.error('執(zhí)行錯(cuò)誤捕獲: ', err);

          }

          在使用 g.throw 的時(shí)候還需要注意以下一些事項(xiàng):

          • 如果 Generator 函數(shù)本身沒有捕獲錯(cuò)誤,那么 Generator 函數(shù)內(nèi)部拋出的錯(cuò)誤可以在執(zhí)行處進(jìn)行錯(cuò)誤捕獲

          • 如果 Generator 函數(shù)內(nèi)部和執(zhí)行處都沒有進(jìn)行錯(cuò)誤捕獲,則終止進(jìn)程并拋出錯(cuò)誤信息

          • 如果沒有執(zhí)行過(guò) g.next,則 g.throw 不會(huì)在 Gererator 函數(shù)中被捕獲(因?yàn)閳?zhí)行指針沒有啟動(dòng) Generator 函數(shù)的執(zhí)行),此時(shí)可以在執(zhí)行處進(jìn)行執(zhí)行錯(cuò)誤捕獲

          Async

          Async 是 Generator 函數(shù)的語(yǔ)法糖,相對(duì)于 Generator 而言 Async 的特性如下:

          • 內(nèi)置執(zhí)行器:Generator 函數(shù)需要設(shè)計(jì)手動(dòng)執(zhí)行器或者通用執(zhí)行器(例如 Co 執(zhí)行器)進(jìn)行執(zhí)行,Async 語(yǔ)法則內(nèi)置了自動(dòng)執(zhí)行器,設(shè)計(jì)代碼時(shí)無(wú)須關(guān)心執(zhí)行步驟

          • yield 命令無(wú)約束:在 Generator 中使用 Co 執(zhí)行器時(shí) yield 后必須是 Promise 對(duì)象或者 Thunk 函數(shù),而 Async 語(yǔ)法中的 await 后可以是 Promise 對(duì)象或者原始數(shù)據(jù)類型對(duì)象、數(shù)字、字符串、布爾值等(此時(shí)會(huì)對(duì)其進(jìn)行 Promise.resolve() 包裝處理)

          • 返回 Promise: async 函數(shù)的返回值是 Promise 對(duì)象(返回原始數(shù)據(jù)類型會(huì)被 Promise 進(jìn)行封裝),  因此還可以作為 await   的命令參數(shù),相對(duì)于 Generator 返回 Iterator 遍歷器更加簡(jiǎn)潔實(shí)用

          舉個(gè)簡(jiǎn)單的示例:

          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 的狀態(tài)發(fā)生變化后執(zhí)行

            const nextResult = await nextPromise(firstResult);

            // 2s 后打印 6

            console.log('nextResult: ', nextResult);

            return nextResult;

          }



          co();



          co().then((res) => {

            console.log('res: ', res); // 6

          });

          通過(guò)上述示例可以看出,async 函數(shù)的特性如下:

          • 調(diào)用 async 函數(shù)后返回的是一個(gè) Promise 對(duì)象,通過(guò) then 回調(diào)可以拿到 async 函數(shù)內(nèi)部 return 語(yǔ)句的返回值

          • 調(diào)用 async 函數(shù)后返回的 Promise 對(duì)象必須等待內(nèi)部所有 await 對(duì)應(yīng)的 Promise 執(zhí)行完(這使得 async 函數(shù)可能是阻塞式執(zhí)行)后才會(huì)發(fā)生狀態(tài)變化,除非中途遇到了 return 語(yǔ)句

          • await 命令后如果是 Promise 對(duì)象,則返回 Promise 對(duì)象處理后的結(jié)果,如果是原始數(shù)據(jù)類型,則直接返回原始數(shù)據(jù)類型

          上述代碼是阻塞式執(zhí)行,nextPromise 需要等待 firstPromise 執(zhí)行完成后才能繼續(xù)執(zhí)行,如果希望兩者能夠并發(fā)執(zhí)行,則可以進(jìn)行下述設(shè)計(jì):

          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 自帶的并發(fā)執(zhí)行 API,也可以通過(guò)讓所有的 Promise 提前并發(fā)執(zhí)行來(lái)處理:

          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({

            // 執(zhí)行 firstPromise

            const first = firstPromise(1);

            // 和 firstPromise 同時(shí)執(zhí)行 nextPromise

            const next = nextPromise(1);

            // 等待 firstPromise 結(jié)果回來(lái)

            const firstResult = await first;

            console.log('firstResult: ', firstResult);

            // 等待 nextPromise 結(jié)果回來(lái)

            const nextResult = await next;

            console.log('nextResult: ', nextResult);

            return nextResult;

          }



          co().then((res) => {

            console.log('res: ', res); // 3

          });

          Async 的錯(cuò)誤處理相對(duì)于 Generator 會(huì)更加簡(jiǎn)單,具體示例如下所示:

          const firstPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Promise 決斷錯(cuò)誤

              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 函數(shù)內(nèi)部拋出的錯(cuò)誤,會(huì)導(dǎo)致函數(shù)返回的 Promise 對(duì)象變?yōu)?nbsp;rejected 狀態(tài),從而可以通過(guò) catch 捕獲,  上述代碼只是一個(gè)粗粒度的容錯(cuò)處理,如果希望 firstPromise 錯(cuò)誤后可以繼續(xù)執(zhí)行 nextPromise,則可以通過(guò) try...catch... 在 async 函數(shù)里進(jìn)行局部錯(cuò)誤捕獲:

          const firstPromise = (result: number): Promise<number> => {

            return new Promise((resolve, reject) => {

              // Promise 決斷錯(cuò)誤

              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 繼續(xù)執(zhí)行

            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 中經(jīng)常使用的編程方式,Node.js 中很多原生的 API 都是采用 Callback 的形式進(jìn)行異步設(shè)計(jì),早期的 Node.js 經(jīng)常會(huì)有 Callback 和 Promise 混用的情況,并且在很長(zhǎng)一段時(shí)間里都沒有很好的支持 Async 語(yǔ)法。如果你對(duì) Node.js 和它的替代品 Deno 感興趣,可以觀看 Ryan Dahl 在 TS Conf 2019 中的經(jīng)典演講 Deno is a New Way to JavaScript[24]

          31、 Object.defineProperty 有哪幾個(gè)參數(shù)?各自都有什么作用?

          32、 Object.defineProperty 和 ES6 的 Proxy 有什么區(qū)別?

          閱讀鏈接:基于 Vue 實(shí)現(xiàn)一個(gè) MVVM[25] - 數(shù)據(jù)劫持的實(shí)現(xiàn)。

          33、 ES6 中 Symbol、Map、Decorator 的使用場(chǎng)景有哪些?或者你在哪些庫(kù)的源碼里見過(guò)這些 API 的使用?

          34、 為什么要使用 TypeScript ? TypeScript 相對(duì)于 JavaScript 的優(yōu)勢(shì)是什么?

          35、 TypeScript 中 const 和 readonly 的區(qū)別?枚舉和常量枚舉的區(qū)別?接口和類型別名的區(qū)別?

          具體分析待補(bǔ),先放個(gè)鏈接

          閱讀鏈接:https://www.typescriptlang.org/docs/handbook/interfaces.html#readonly-properties

          36、 TypeScript 中 any 類型的作用是什么?

          37、 TypeScript 中 any、never、unknown 和 void 有什么區(qū)別?

          具體分析待補(bǔ),先放個(gè)鏈接

          閱讀鏈接:https://www.typescriptlang.org/docs/handbook/basic-types.html#any

          38、 TypeScript 中 interface 可以給 Function / Array / Class(Indexable)做聲明嗎?

          具體分析待補(bǔ),先放個(gè)鏈接

          閱讀鏈接:

          • Interface 可以給 Function(Object) 做聲明:https://www.typescriptlang.org/docs/handbook/interfaces.html#function-types
          • Interface 可以給 Indexable (Array)做聲明:https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types
          • Interface 可以給 Class 做聲明:https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes

          39、 TypeScript 中可以使用 String、Number、Boolean、Symbol、Object 等給類型做聲明嗎?

          40、 TypeScript 中的 this 和 JavaScript 中的 this 有什么差異?

          41、 TypeScript 中使用 Unions 時(shí)有哪些注意事項(xiàng)?

          42、 TypeScript 如何設(shè)計(jì) Class 的聲明?

          具體分析待補(bǔ),先放個(gè)鏈接

          閱讀鏈接:

          • Class 類型聲明:https://www.typescriptlang.org/docs/handbook/classes.html#constructor-functions
          • Interface 可以給 Class 做聲明:https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes

          43、 TypeScript 中如何聯(lián)合枚舉類型的 Key?






          #### 44、 TypeScript 中 ?.、??、!.、_、** 等符號(hào)的含義?

          #### 45、 TypeScript 中預(yù)定義的有條件類型有哪些?

          #### 46、 簡(jiǎn)單介紹一下 TypeScript 模塊的加載機(jī)制?

          #### 47、 簡(jiǎn)單聊聊你對(duì) TypeScript 類型兼容性的理解?抗變、雙變、協(xié)變和逆變的簡(jiǎn)單理解?

          #### 48、 TypeScript 中對(duì)象展開會(huì)有什么副作用嗎?

          #### 49、 TypeScript 中 interface、type、enum 聲明有作用域的功能嗎?

          #### 50、 TypeScript 中同名的 interface 或者同名的 interface 和 class 可以合并嗎?

          #### 51、 如何使 TypeScript 項(xiàng)目引入并識(shí)別編譯為 JavaScript 的 npm 庫(kù)包?

          #### 52、 TypeScript 的 tsconfig.json 中有哪些配置項(xiàng)信息?

          #### 53、 TypeScript 中如何設(shè)置模塊導(dǎo)入的路徑別名?





          ### 框架





          #### 54、 React Class 組件有哪些周期函數(shù)?分別有什么作用?

          #### 55、 React Class 組件中請(qǐng)求可以在 componentWillMount 中發(fā)起嗎?為什么?







          #### 56、 React Class 組件和 React Hook 的區(qū)別有哪些?

          #### 57、 React 中高階函數(shù)和自定義 Hook 的優(yōu)缺點(diǎn)?

          #### 58、 簡(jiǎn)要說(shuō)明 React Hook 中 useState 和 useEffect 的運(yùn)行原理?

          #### 59、 React 如何發(fā)現(xiàn)重渲染、什么原因容易造成重渲染、如何避免重渲染?

          #### 60、 React Hook 中 useEffect 有哪些參數(shù),如何檢測(cè)數(shù)組依賴項(xiàng)的變化?

          #### 61、 React 的 useEffect 是如何監(jiān)聽數(shù)組依賴項(xiàng)的變化的?

          #### 62、 React Hook 和閉包有什么關(guān)聯(lián)關(guān)系?

          #### 63、 React 中 useState 是如何做數(shù)據(jù)初始化的?

          #### 64、 列舉你常用的 React 性能優(yōu)化技巧?

          #### 65、 Vue 2.x 模板中的指令是如何解析實(shí)現(xiàn)的?

          #### 66、 簡(jiǎn)要說(shuō)明 Vue 2.x 的全鏈路運(yùn)作機(jī)制?

          #### 67、 簡(jiǎn)單介紹一下 Element UI 的框架設(shè)計(jì)?

          #### 68、 如何理解 Vue 是一個(gè)漸進(jìn)式框架?

          #### 69、 Vue 里實(shí)現(xiàn)跨組件通信的方式有哪些?

          #### 70、 Vue 中響應(yīng)式數(shù)據(jù)是如何做到對(duì)某個(gè)對(duì)象的深層次屬性的監(jiān)聽的?

          #### 71、 MVVM、MVC 和 MVP 的區(qū)別是什么?各自有什么應(yīng)用場(chǎng)景?、

          #### 72、 什么是 MVVM 框架?







          ### 工程





          #### 73、Vue CLI 3.x 有哪些功能?Vue CLI 3.x 的插件系統(tǒng)了解?

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

          #### 75、Vue 2.x 如何支持 TypeScript 語(yǔ)法?

          #### 76、如何配置環(huán)境使得 JavaScript 項(xiàng)目可以支持 TypeScript 語(yǔ)法?

          #### 77、如何對(duì) TypeScript 進(jìn)行 Lint 校驗(yàn)?ESLint 和 TSLint 有什么區(qū)別?

          #### 78、Node.js 如何支持 TypeScript 語(yǔ)法?

          #### 79、TypeScript 如何自動(dòng)生成庫(kù)包的聲明文件?

          #### 80、Babel 對(duì)于 TypeScript 的支持有哪些限制?

          #### 81、Webpack 中 Loader 和 Plugin 的區(qū)別是什么?

          #### 82、在 Webpack 中是如何做到支持類似于 JSX 語(yǔ)法的 Sourcemap 定位?

          #### 83、發(fā)布 Npm 包如何指定引入地址?

          #### 84、如何發(fā)布開發(fā)項(xiàng)目的特定文件夾為 Npm 包的根目錄?

          #### 85、如何發(fā)布一個(gè)支持 Tree Shaking 機(jī)制的 Npm 包?

          #### 86、Npm 包中 peerDependencies 的作用是什么?

          #### 87、如何優(yōu)雅的調(diào)試需要發(fā)布的 Npm 包?

          #### 88、在設(shè)計(jì)一些庫(kù)包時(shí)如何生成版本日志?

          #### 89、了解 Git (Submodule)子模塊嗎?簡(jiǎn)單介紹一下 Git 子模塊的作用?

          #### 90、Git 如何修改已經(jīng)提交的 Commit 信息?

          #### 91、Git 如何撤銷 Commit 并保存之前的修改?

          #### 92、Git 如何 ignore 被 commit 過(guò)的文件?

          #### 93、在使用 Git 的時(shí)候如何規(guī)范 Git 的提交說(shuō)明(Commit 信息)?

          #### 94、簡(jiǎn)述符合 Angular 規(guī)范的提交說(shuō)明的結(jié)構(gòu)組成?

          #### 95、Commit 信息如何和 Github Issues 關(guān)聯(lián)?

          #### 96、Git Hook 在項(xiàng)目中哪些作用?

          #### 97、Git Hook 中客戶端和服務(wù)端鉤子各自用于什么作用?

          #### 98、Git Hook 中常用的鉤子有哪些?

          #### 99、pre-commit 和 commit-msg 鉤子的區(qū)別是什么?各自可用于做什么?

          #### 100、husky 以及 ghook 等工具制作 Git Hook 的原理是什么?

          #### 101、如何設(shè)計(jì)一個(gè)通用的 Git Hook ?

          #### 102、Git Hook 可以采用 Node 腳本進(jìn)行設(shè)計(jì)嗎?如何做到?

          #### 103、如何確保別人上傳的代碼沒有 Lint 錯(cuò)誤?如何確保代碼構(gòu)建沒有 Lint 錯(cuò)誤?

          #### 104、如何在 Vs Code 中進(jìn)行 Lint 校驗(yàn)提示?如何在 Vs Code 中進(jìn)行 Lint 保存格式化?

          #### 105、ESLint 和 Prettier 的區(qū)別是什么??jī)烧咴谝黄鸸ぷ鲿r(shí)會(huì)產(chǎn)生問(wèn)題嗎?

          #### 106、如何有效的識(shí)別 ESLint 和 Prettier 可能產(chǎn)生沖突的格式規(guī)則?如何解決此類規(guī)則沖突問(wèn)題?

          #### 107、在通常的腳手架項(xiàng)目中進(jìn)行熱更新(hot module replacement)時(shí)如何做到 ESLint 實(shí)時(shí)打印校驗(yàn)錯(cuò)誤信息?

          #### 108、談?wù)勀銓?duì) SourceMap 的了解?

          #### 109、如何調(diào)試 Node.js 代碼?如何調(diào)試 Node.js TypeScript 代碼?在瀏覽器中如何調(diào)試 Node.js 代碼?

          #### 110、列舉你知道的所有構(gòu)建工具并說(shuō)說(shuō)這些工具的優(yōu)缺點(diǎn)?這些構(gòu)建工具在不同的場(chǎng)景下應(yīng)該如何選型?

          #### 111、VS Code 配置中的用戶和工作區(qū)有什么區(qū)別?

          #### 112、VS Code 的插件可以只對(duì)當(dāng)前項(xiàng)目生效嗎?

          #### 113、你所知道的測(cè)試有哪些測(cè)試類型?

          #### 114、你所知道的測(cè)試框架有哪些?

          #### 115、什么是 e2e 測(cè)試?有哪些 e2e 的測(cè)試框架?

          #### 116、假設(shè)現(xiàn)在有一個(gè)插入排序算法,如何對(duì)該算法進(jìn)行單元測(cè)試?







          ### 網(wǎng)絡(luò)





          #### 117、CDN 服務(wù)如何實(shí)現(xiàn)網(wǎng)絡(luò)加速?

          #### 118、WebSocket 使用的是 TCP 還是 UDP 協(xié)議?

          #### 119、什么是單工、半雙工和全雙工通信?

          #### 120、簡(jiǎn)單描述 HTTP 協(xié)議發(fā)送一個(gè)帶域名的 URL 請(qǐng)求的協(xié)議傳輸過(guò)程?(DNS、TCP、IP、鏈路)

          #### 121、什么是正向代理?什么是反向代理?

          #### 122、Cookie 可以在服務(wù)端生成嗎?Cookie 在服務(wù)端生成后的工作流程是什么樣的?

          #### 123、Session、Cookie 的區(qū)別和關(guān)聯(lián)?如何進(jìn)行臨時(shí)性和永久性的 Session 存儲(chǔ)?

          #### 124、設(shè)置 Cookie 時(shí)候如何防止 XSS 攻擊?

          #### 125、簡(jiǎn)單描述一下用戶免登陸的實(shí)現(xiàn)過(guò)程?可能會(huì)出現(xiàn)哪些安全性問(wèn)題?一般如何對(duì)用戶登錄的密碼進(jìn)行加密?

          #### 126、HTTP 中提升傳輸速率的方式有哪些?常用的內(nèi)容編碼方式有哪些?

          #### 127、傳輸圖片的過(guò)程中如果突然中斷,如何在恢復(fù)后從之前的中斷中恢復(fù)傳輸?

          #### 128、什么是代理?什么是網(wǎng)關(guān)?代理和網(wǎng)關(guān)的作用是什么?

          #### 129、HTTPS 相比 HTTP 為什么更加安全可靠?

          #### 130、什么是對(duì)稱密鑰(共享密鑰)加密?什么是非對(duì)稱密鑰(公開密鑰)加密?哪個(gè)更加安全?

          #### 131、你覺得 HTTP 協(xié)議目前存在哪些缺點(diǎn)?







          ### 性能





          #### 133、在 React 中如何識(shí)別一個(gè)表單項(xiàng)里的表單做到了最小粒度 / 代價(jià)的渲染?

          #### 134、在 React 的開發(fā)的過(guò)程中你能想到哪些控制渲染成本的方法?







          ### 插件





          #### 135、Vue CLI 3.x 的插件系統(tǒng)是如何設(shè)計(jì)的?

          #### 136、Webpack 中的插件機(jī)制是如何設(shè)計(jì)的?







          ### 系統(tǒng)





          #### 137、\r\n(CRLF) 和 \n (LF)的區(qū)別是什么?(Vs Code 的右下角可以切換)

          #### 138、/dev/null 的作用是啥?

          #### 139、如何在 Mac 的終端中設(shè)置一個(gè)命令的別名?

          #### 140、如何在 Windows 中設(shè)置環(huán)境變量?

          #### 141、Mac 的文件操作系統(tǒng)默認(rèn)區(qū)分文件路徑的大小寫嗎?

          #### 142、編寫 Shell 腳本時(shí)如何設(shè)置文件的絕對(duì)路徑?







          ### 后端





          #### 143、Session、Cookie 的區(qū)別和關(guān)聯(lián)?如何進(jìn)行臨時(shí)性和永久性的 Session 存儲(chǔ)?

          #### 144、如何部署 Node.js 應(yīng)用?如何處理負(fù)載均衡中 Session 的一致性問(wèn)題?

          #### 145、如何提升 Node.js 代碼的運(yùn)行穩(wěn)定性?

          #### 146、GraphQL 與 Restful 的區(qū)別,它有什么優(yōu)點(diǎn)?

          #### 147、Vue SSR 的工作原理?Vuex 的數(shù)據(jù)如何同構(gòu)渲染?

          #### 148、SSR 技術(shù)和 SPA 技術(shù)的各自的優(yōu)缺點(diǎn)是什么?

          #### 149、如何處理 Node.js 渲染 HTML 壓力過(guò)大問(wèn)題?











          ## 業(yè)務(wù)思考





          業(yè)務(wù)思考更多的是結(jié)合基礎(chǔ)知識(shí)的廣度和深度進(jìn)行的具體業(yè)務(wù)實(shí)踐,主要包含以下幾個(gè)方面:







          - 工程化:代碼部署、CI / CD 流程設(shè)計(jì)、Jenkins、Gitlab、Docker 等

          - 通用性:腳手架、SDK、組件庫(kù)等框架設(shè)計(jì)

          - 應(yīng)用框架:Hybrid 混合、微前端、BFF、Monorepo

          - 可視化:

          - 低代碼:通用表單設(shè)計(jì)、通用布局設(shè)計(jì)、通用頁(yè)面設(shè)計(jì)、JSON Schema 協(xié)議設(shè)計(jì)等

          - 測(cè)試:E2E 測(cè)試、單元測(cè)試、測(cè)試覆蓋率、測(cè)試報(bào)告等

          - 業(yè)務(wù):數(shù)據(jù)、體驗(yàn)、復(fù)雜度、監(jiān)控







          ### 工程化





          #### 150、你所知道的 CI / CD 工具有哪些?在項(xiàng)目中有接觸過(guò)類似的流程嗎?

          #### 151、如果讓你實(shí)現(xiàn)一個(gè) Web 前端的 CI / CD 工程研發(fā)平臺(tái),你會(huì)如何設(shè)計(jì)?

          #### 152、如果我們需要將已有項(xiàng)目中的線上產(chǎn)物資源(例如圖片)轉(zhuǎn)換成本地私有化資源,你有什么解決方案?

          #### 153、如何使用 Vue CLI 3.x 定制一個(gè)腳手架?比如內(nèi)部自動(dòng)集成了 i18n、 axios、Element UI、路由守衛(wèi)等?

          #### 154、Jenkins 如何配合 Node.js 腳本進(jìn)行 CI / CD 設(shè)計(jì)?







          ### 通用性





          #### 155、如果讓你設(shè)計(jì)一個(gè)通用的項(xiàng)目腳手架,你會(huì)如何設(shè)計(jì)?一個(gè)通用的腳手架一般需要具備哪些能力?

          #### 156、如果讓你設(shè)計(jì)一個(gè)通用的工具庫(kù),你會(huì)如何設(shè)計(jì)?一個(gè)通用的工具庫(kù)一般需要具備哪些能力?

          #### 157、假設(shè)你自己實(shí)現(xiàn)的 React 或 Vue 的組件庫(kù)要設(shè)計(jì)演示文檔,你會(huì)如何設(shè)計(jì)?設(shè)計(jì)的文檔需要實(shí)現(xiàn)哪些功能?

          #### 158、在設(shè)計(jì)工具庫(kù)包的時(shí)候你是如何設(shè)計(jì) API 文檔的?







          ### 應(yīng)用框架





          #### 159、談?wù)?Electron、Nw.js、CEF、Flutter 和原生開發(fā)的理解?

          #### 160、談?wù)勛烂娑藨?yīng)用中 HotFix 的理解?

          #### 161、你覺得什么樣的場(chǎng)景需要使用微前端框架?







          ### 業(yè)務(wù)





          #### 162、什么是單點(diǎn)登錄?如何做單點(diǎn)登錄?

          #### 163、如何做一個(gè)項(xiàng)目的國(guó)際化方案?

          #### 164、如何做一個(gè)項(xiàng)目的監(jiān)控和埋點(diǎn)方案?

          #### 165、如何建設(shè)項(xiàng)目的穩(wěn)定性(監(jiān)控、灰度、錯(cuò)誤降級(jí)、回滾...)?

          #### 166、一般管理后臺(tái)型的應(yīng)用需要考慮哪些性能方面的優(yōu)化?

          #### 167、簡(jiǎn)述一些提升項(xiàng)目體驗(yàn)的案例和技術(shù)方案(骨架屏、Loading 處理、緩存、錯(cuò)誤降級(jí)、請(qǐng)求重試...)?

          #### 168、假設(shè)需要對(duì)頁(yè)面設(shè)計(jì)一個(gè)水印方案,你會(huì)如何設(shè)計(jì)?











          ### 低代碼





          #### 169、如何設(shè)計(jì)一個(gè)通用的 JSON Schema 協(xié)議使其可以動(dòng)態(tài)渲染一個(gè)通用的聯(lián)動(dòng)表單?

          #### 170、一般的低代碼平臺(tái)需要具備哪些能力?











          ## 筆試實(shí)踐





          筆試更多的是考驗(yàn)應(yīng)聘者的邏輯思維能力和代碼書寫風(fēng)格,主要包含以下幾個(gè)方面:







          - 正則表達(dá)式

          - 算法

          - 數(shù)據(jù)結(jié)構(gòu)

          - 設(shè)計(jì)模式

          - 框架的部分原理實(shí)現(xiàn)

          - TypeScript 語(yǔ)法

          - 模板解析







          ### 數(shù)據(jù)結(jié)構(gòu)





          #### 171、使用 TypeScript 語(yǔ)法將沒有層級(jí)的扁平數(shù)據(jù)轉(zhuǎn)換成樹形結(jié)構(gòu)的數(shù)據(jù)

          ```javascript

          // 扁平數(shù)據(jù)

          [{

            name: '文本1',

            parent: null,

            id: 1,

          }, {

            name: '文本2',

            id: 2,

            parent: 1

          }, {

            name: '文本3',

            parent: 2,

            id: 3,

          }]



          // 樹狀數(shù)據(jù)

          [{

            name: '文本1',

            id: 1,

            children: [{

              name: '文本2',

              id: 2,

              children: [{

                name: '文本3',

                id: 3

              }]

            }]

          }]

          模板解析

          172、實(shí)現(xiàn)一個(gè)簡(jiǎn)易的模板引擎

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



          const data = {

            info: {

              name: {

                value'張三'

              }

            },

            day: {

              value'三'

            }

          };



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

          設(shè)計(jì)模式

          173、簡(jiǎn)單實(shí)現(xiàn)一個(gè)發(fā)布 / 訂閱模式

          正則表達(dá)式

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

          參考資料

          [1] 

          專有釘釘前端面試指南: https://juejin.cn/post/6986436944913924103

          [2] 

          面試分享:兩年工作經(jīng)驗(yàn)成功面試阿里 P6 總結(jié): https://juejin.cn/post/6844903928442667015#heading-43

          [3] 

          三地址碼(Three Address Code, TAC): https://en.wikipedia.org/wiki/Three-address_code

          [4] 

          靜態(tài)單賦值形式(Static Single Assignment Form, SSA): https://en.wikipedia.org/wiki/Static_single_assignment_form

          [5] 

          基于 Vue 實(shí)現(xiàn)一個(gè)簡(jiǎn)易 MVVM : https://juejin.cn/post/6844904099704471559#heading-10

          [6] 

          What is the precise definition of programming paradigm?: https://softwareengineering.stackexchange.com/questions/166442/what-is-the-precise-definition-of-programming-paradigm#

          [7] 

          簡(jiǎn)明 JavaScript 函數(shù)式編程——入門篇: https://juejin.cn/post/6844903936378273799

          [8] 

          函數(shù)式編程指北: https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/

          [9] 

          Web console: https://developer.mozilla.org/en-US/docs/Tools/Web_Console

          [10] 

          事件循環(huán)機(jī)制: https://juejin.cn/post/6844903843197616136#heading-3

          [11] 

          EventEmitter: http://nodejs.cn/api/events.html#events_class_eventemitter

          [12] 

          Stream: http://nodejs.cn/api/stream.html

          [13] 

          Promise: https://es6.ruanyifeng.com/#docs/promise

          [14] 

          Generator: https://es6.ruanyifeng.com/#docs/generator-async

          [15] 

          Async: https://es6.ruanyifeng.com/#docs/async

          [16] 

          Q: https://github.com/kriskowal/q

          [17] 

          Co、: https://github.com/tj/co

          [18] 

          Bluebird: https://github.com/petkaantonov/bluebird

          [19] 

          Promise A +: https://promisesaplus.com/

          [20] 

          有限狀態(tài)機(jī): http://www.ruanyifeng.com/blog/2013/09/finite-state_machine_for_javascript.html

          [21] 

          TJ Holowaychuk : https://github.com/tj

          [22] 

          Co: https://github.com/tj/co

          [23] 

          API 文檔: https://github.com/tj/co#arrays

          [24] 

          Deno is a New Way to JavaScript: https://www.youtube.com/watch?v=1gIiZfSbEAE

          [25] 

          基于 Vue 實(shí)現(xiàn)一個(gè) MVVM: https://juejin.cn/post/6844904099704471559#heading-23

          瀏覽 88
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  男女操逼在线免费观看 | 国产成人久久777777黄蓉 | 操小骚逼视频 | 国产 欧美 日韩 a | 青榴社区 |