每一個 JavaScript 工程師都應當知道的 10 個面試題

1. 能說出來兩種對於 JavaScript 工程師很重要的編程範式麼?前端

JavaScript 是一門多範式(multi-paradigm)的編程語言,它既支持命令式(imperative)/面向過程(procedural)編程,也支持面向對象編程(OOP,Object-Oriented Programming),還支持函數式編程(functional programming)。JavaScript 所支持的面向對象編程包括原型繼承(prototypal inheritance)。面試

面試加分項算法

原型繼承(即:原型,OLOO——連接到其它對象的對象);
函數式編程(即:閉包(closure),一類函數(first class functions),lambda 函數:箭頭函數)。
面試減分項編程

連範式都不知道,更別提什麼原型 OO(prototypal oo)或者函數式編程了。設計模式

深刻了解數組

The Two Pillars of JavaScript Part 1:JS 兩大支柱之一:原型 OO
The Two Pillars of JavaScript Part 2:JS 兩大支柱之二:函數式編程安全


2. 什麼是函數式編程?網絡

函數式編程,是將數學函數組合起來,而且避免了狀態共享(shared state)及可變數據(mutable data),由此而產生的編程語言。發明於 1958 年的 Lisp 就是首批支持函數式編程的語言之一,而 λ 演算(lambda calculus)則能夠說是孕育了這門語言。即便在今天,Lisp 這個家族的編程語言應用範圍依然很廣。閉包

函數式編程但是 JavaScript 語言中很是重要的一個概念(它但是 JavaScript 的兩大支柱之一)。ES5 規範中就增長了不少經常使用的函數式工具。架構

面試加分項

純函數(pure functions)/函數的純粹性(function purity)
知道如何避免反作用(side-effects)
簡單函數的組合
函數式編程語言:Lisp,ML,Haskell,Erlang,Clojure,Elm,F#,OCaml,等等
提到了 JavaScript 語言中支持函數式編程(FP)的特性:一類函數,高階函數(higher order functions),做爲參數(arguments)/值(values)的函數
面試減分項

沒有提到純函數,以及如何避免反作用
沒有提供函數式編程語言的例子
沒有說是 JavaScript 中的哪些特性使得函數式編程得以實現
深刻了解

The Two Pillars of JavaScript Part 2:JS 兩大支柱之二:函數式編程
The Dao of Immutability
Composing Software
The Haskell School of Music


3. 類繼承和原型繼承有什麼區別?

類繼承(Class Inheritance):實例(instances)由類繼承而來(類和實例的關係,能夠類比爲建築圖紙和實際建築 🏠 的關係),同時還會建立父類—子類這樣一種關係,也叫作類的分層分類(hierarchical class taxonomies)。一般是用 new 關鍵字調用類的構造函數(constructor functions)來建立實例的。不過在 ES6 中,要繼承一個類,不用 class 關鍵字也能夠。

原型繼承(Prototypal Inheritance):實例/對象直接從其它對象繼承而來,建立實例的話,每每用工廠函數(factory functions)或者 Object.create() 方法。實例能夠從多個不一樣的對象組合而來,這樣就能選擇性地繼承了。

在 JavaScript 中,原型繼承比類繼承更簡單,也更靈活。
面試加分項

類:會建立緊密的耦合,或者說層級結構(hierarchies)/分類(taxonomies)。
原型:提到了銜接繼承(concatenative inheritance)、原型委託( prototype delegation)、函數繼承(functional inheritance),以及對象組合(object composition)。
面試減分項

原型繼承和組合,與類繼承相比,不知道哪一個更好。

深刻了解

The Two Pillars of JavaScript Part 1:JS 兩大支柱之一:原型 OO
Common Misconceptions About Inheritance in JavaScript:對於 JavaScript 中繼承這個概念,所廣泛存在的誤解


4. 函數式編程和麪向對象編程,各有什麼優勢和不足呢?

面向對象編程的優勢:關於「對象」的一些基礎概念理解起來比較容易,方法調用的含義也好解釋。面向對象編程一般使用命令式的編碼風格,聲明式(declarative style)的用得比較少。這樣的代碼讀起來,像是一組直接的、計算機很容易就能遵循的指令。

面向對象編程的不足:面向對象編程每每須要共享狀態。對象及其行爲經常會添加到同一個實體上,這樣一來,若是一堆函數都要訪問這個實體,並且這些函數的執行順序不肯定的話,極可能就會出亂子了,好比競爭條件(race conditions)這種現象(函數 A 依賴於實體的某個屬性,可是在 A 訪問屬性以前,屬性已經被函數 B 修改了,那麼函數 A 在使用屬性的時候,極可能就得不到預期的結果)。

函數式編程的優勢:用函數式範式來編程,就不須要擔憂共享狀態或者反作用了。這樣就避免了幾個函數在調用同一批資源時可能產生的 bug 了。擁有了「無參風格」(point-free style,也叫隱式編程)之類的特性以後,函數式編程就大大簡化了,咱們也能夠用函數式編程的方式來把代碼組合成複用性更強的代碼了,面向對象編程可作不到這一點。

函數式編程更偏心聲明式、符號式(denotational style)的編碼風格,這樣的代碼,並非那種爲了實現某種目的而須要循序漸進地執行的一大堆指令,而是關注宏觀上要作什麼。至於具體應該怎麼作,就都隱藏在函數內部了。這樣一來,要是想重構代碼、優化性能,那就大有可爲了。(譯者注:以作一道菜爲例,就是由 買菜 -> 洗菜 -> 炒菜 這三步組成,每一步都是函數式編程的一個函數,無論作什麼菜,這個流程都是不會變的。而想要優化這個過程,天然就是要深刻每一步之中了。這樣無論內部如何重構、優化,總體的流程並不會變,這就是函數式編程的好處。)甚至能夠把一種算法換成另外一種更高效的算法,同時還基本不須要修改代碼(好比把及早求值策略(eager evaluation)替換爲惰性求值策略(lazy evaluation))。

利用純函數進行的計算,能夠很方便地擴展到多處理器環境下,或者應用到分佈式計算集羣上,同時還不用擔憂線程資源衝突、競爭條件之類的問題。

函數式編程的不足:代碼若是過分利用了函數式的編程特性(如無參風格、大量方法的組合),就會影響其可讀性,從而簡潔度有餘、易讀性不足。

大部分工程師仍是更熟悉面向對象編程、命令式編程,對於剛接觸函數式編程的人來講,即便只是這個領域的一些的簡單術語,均可能讓他懷疑人生。

函數式編程的學習曲線更陡峭,由於面向對象編程太普及了,學習資料太多了。相比而言,函數式編程在學術領域的應用更普遍一些,在工業界的應用稍遜一籌,天然也就不那麼「平易近人」了。在探討函數式編程時,人們每每用 λ 演算、代數、範疇學等學科的專業術語和專業符號來描述相關的概念,那麼其餘人想要入門函數式編程的話,就得先把這些領域的基礎知識搞明白,能不讓人頭大麼。

面試加分項

共享狀態的缺點、資源競爭、等等(面向對象編程)
函數式編程可以極大地簡化應用開發
面向對象編程和函數式編程學習曲線的不一樣
兩種編程方式各自的不足之處,以及對代碼後期維護帶來的影響
函數式風格的代碼庫,學習曲線會很陡峭
面向對象編程風格的代碼庫,修改起來很難,很容易出問題(和水平至關的函數式風格的代碼相比)
不可變性(immutability),可以極大地提高程序歷史狀態(program state history)的可見性(accessible)和擴展性(malleable),這樣一來,想要添加諸如無限撤銷/重作、倒帶/回放、可後退的調試之類的功能的話,就簡單多了。無論是面向對象編程仍是函數式編程,這兩種範式都能實現不可變性,可是要用面向對象來實現的話,共享狀態對象的數量就會劇增,代碼也會變得複雜不少。
面試減分項

沒有講這兩種編程範式的缺點——若是熟悉至少其中一種範式的話,應該可以說出不少這種範式的缺點吧。

深刻了解

老是你倆,看來你倆真是很是重要啊。

The Two Pillars of JavaScript Part 1:JS 兩大支柱之一:原型 OO
The Two Pillars of JavaScript Part 2:JS 兩大支柱之二:函數式編程


5. 何時該用類繼承?

千萬別用類繼承!或者說盡可能別用。若是非要用,就只用它繼承一級(one level)就行了,多級的類繼承簡直就是反模式的。這個話題(不太明白是關於什麼的……)我也參與討論過好些年了,僅有的一些回答最終也淪爲 常見的誤解 之一。更多的時候,這個話題討論着討論着就沒動靜了。

若是一個特性有時候頗有用 但有時候又很危險 而且還有另外一種更好的特性能夠用 那務必要用另外一種更好的特性~ Douglas Crockford
面試加分項

儘可能別用,甚至是完全不用類繼承。
有時候只繼承一級的話也仍是 OK 的,好比從框架的基類繼承,例如 React.Component。
相比類繼承,對象組合(object composition)更好一些。
深刻了解

The Two Pillars of JavaScript Part 1:JS 兩大支柱之一:原型 OO
JS Objects — Inherited a Mess:JS 對象(繼承)——只是繼承了混亂(mess)而已


6. 何時該用原型繼承?

原型繼承能夠分爲下面幾類:

委託(delegation,也就是原型鏈)
組合(concatenative,好比混用(mixins)、 Object.assign())
函數式(functional,這個函數式原型繼承不是函數式編程。這裏的函數是用來建立一個閉包,以實現私有狀態(private state)或者封裝(encapsulation))
上面這三種原型繼承都有各自的適用場景,不過它們都頗有用,由於都能實現組合繼承(composition),也就是創建了 A 擁有特性 B(has-a)、A 用到了特性 B(uses-a) 或者 A 能夠實現特性 B(can-do) 的這樣一種關係。相比而言,類繼承創建的是 A 就是 B 這樣一種關係。

面試加分項

知道在什麼狀況下不適合用模塊化(modules)或者函數式編程。
知道須要組合多個不一樣來源的對象時,應該怎麼作。
知道何時該用繼承。
面試減分項

不知道何時應該用原型。
不知道混用和 Object.assign()。
深刻了解

Programming JavaScript Applications:文章中的「原型」這一節


7. 爲何說「對象組合比類繼承更好」?

這句話引用的是《設計花紋》(Design Patterns,設計模式)這本書的內容。意思是要想實現代碼重用,就應該把一堆小的功能單元組合成知足需求的各類對象,而不是經過類繼承弄出來一層一層的對象。

換句話說,就是儘可能編程實現 can-do、has-a 或者 uses-a 這種關係,而不是 is-a 這種關係。

面試加分項

避免使用類繼承。
避免使用問題多多的基類。
避免緊耦合。
避免極其不靈活的層次分類(taxonomy)(類繼承所產生的 is-a 關係可能會致使不少誤用的狀況)
避免大猩猩香蕉問題(「你只是想要一根香蕉,結果最後卻整出來一隻拿着香蕉的大猩猩,還有整個叢林」)。
要讓代碼更具擴展性。
面試減分項

沒有提到上面任何一種問題。
沒有表達清楚對象組合與類繼承有什麼區別,也沒有提到對象組合的優勢。
深刻了解

Composition over Inheritance
Introducing the Stamp Specification


8. 雙向數據綁定/單向數據流的含義和區別

雙向數據綁定(two-way data binding),意味着 UI 層所呈現的內容和 Model 層的數據動態地綁定在一塊兒了,其中一個發生了變化,就會馬上反映在另外一個上。好比用戶在前端頁面的表單控件中輸入了一個值,Model 層對應該控件的變量就會馬上更新爲用戶所輸入的值;反之亦然,若是 Modal 層的數據有變化,變化後的數據也會馬上反映至 UI 層。

單向數據流(one-way data flow), 意味着只有 Model 層纔是單一數據源(single source of truth)。UI 層的變化會觸發對應的消息機制,告知 Model 層用戶的目的(對應 React 的 store)。只有 Model 層纔有更改應用狀態的權限,這樣一來,數據永遠都是單向流動的,也就更容易瞭解應用的狀態是如何變化的。

採用單向數據流的應用,其狀態的變化是很容易跟蹤的,採用雙向數據綁定的應用,就很難跟蹤並理解狀態的變化了。

面試加分項

React 是單向數據流的典型,面試時提到這個框架的話會加分。Cycle.js 則是另外一個很流行的單向數據流的庫。
Angular 則是雙向數據綁定的典型。
面試減分項

不理解單向數據流/雙向數據綁定的含義,也說不清楚二者之間的區別。

深刻了解

Introduction to React.js


9. 單體架構和微服務架構各有何優劣?

採用單體架構(monolithic architecture)的應用,各組件的代碼是做爲一個總體存在的,組件之間互相合做,共享內存和資源。

而微服務架構(microservice architecture)則是由許許多多個互相獨立的小應用組成,每一個應用都有本身的內存空間,應用在擴容時也是獨立於其它應用進行的。

單體架構的優點:大部分應用都有至關數量的橫切關注點(cross-cutting concerns),好比日誌記錄,流量限制,還有審計跟蹤和 DOS 防禦等安全方面的需求,單體架構在這方面就頗有優點。

當全部功能都運行在一個應用裏的時候,就能夠很方便地將組件與橫切關注點相關聯。

單體架構也有性能上的優點,畢竟訪問共享內存仍是比進程間通訊(inter-process communication,IPC)要快的。

單體架構的劣勢:隨着單體架構應用功能的不斷開發,各項服務之間的耦合程度也會不斷增長,這樣一來就很難把各項服務分離開來了,要作獨立擴容或者代碼維護也就更不方便了。

微服務的優點:微服務架構通常都有更好的組織結構,由於每項服務都有本身特定的分工,並且也不會干涉其它組件所負責的部分。服務解耦以後,想要從新組合、配置來爲各個不一樣的應用提供服務的話,也更方便了(好比同時爲 Web 客戶端和公共 API 提供服務)。

若是用合理的架構來部署微服務的話,它在性能上也是頗有優點的,由於這樣一來,就能夠很輕鬆地分離熱門服務,對其進行擴容,同時還不會影響到應用中的其它部分。

微服務的劣勢:在實際構建一個新的微服務架構的時候,會遇到不少在設計階段沒有預料到的橫切關注點。若是是單體架構應用的話就很簡單,新建一箇中間件(shared magic helpers 不知道怎麼翻譯……)來解決這樣的問題就好了,沒什麼麻煩的。

可是在微服務架構中就不同了,要解決這個問題,要麼爲每一個橫切關注點都引入一個獨立的模塊,要麼就把全部橫切關注點的解決方案封裝到一個服務層中,讓全部流量都從這裏走一遍就好了。

爲了解決橫切關注點的問題,雖然單體架構也趨向於把全部的路由流量都從一個外部服務層走一遍,可是在這種架構中,能夠等到項目很是成熟以後再進行這種改造,這樣就能夠把還這筆技術債的時間儘可能日後拖一拖。

微服務通常都是部署在虛擬機或容器上的,隨着應用規模的不斷增長,虛擬機搶工做(VM wrangling work)的狀況也會迅速增長。任務的分配通常都是經過容器羣(container fleet)管理工具來自動實現的。

面試加分項

對於微服務的積極態度,雖然初始成本會比單體架構要高一些。知道微服務的性能和擴容在長期看來表現更佳。
在微服務架構和單體架構應用上都有實戰經驗。可以使應用中的各項服務在代碼層面互相獨立,可是又能夠在開發初期迅速地將各項服務打包成一整個的單體架構應用。微服務化的改造能夠在應用至關成熟以後,改形成本在可承受範圍內的時候再進行。
面試減分項

不知道單體架構和微服務架構的區別。
不知道微服務架構額外的開銷,或者沒有實際經驗。
不知道微服務架構中,IPC 和網絡通訊所致使的額外的性能開銷。
過度貶低微服務。說不清楚何時應該把單體架構應用解耦成微服務。
低估了可獨立擴容的微服務的優點。


10. 異步編程是什麼?又爲何在 JavaScript 中這麼重要?

在同步編程中,代碼會按順序自頂向下依次執行(條件語句和函數調用除外),若是遇到網絡請求或者磁盤讀/寫(I/O)這類耗時的任務,就會堵塞在這樣的地方。

在異步編程中,JS 運行在事件循環(event loop)中。當須要執行一個阻塞操做(blocking operation)時,主線程發起一個(異步)請求,(工做線程就會去執行這個異步操做,)同時主線程繼續執行後面的代碼。(工做線程執行完畢以後,)就會發起響應,觸發中斷(interrupt),執行事件處理程序(event handler),執行完後主線程繼續日後走。這樣一來,一個程序線程就能夠處理大量的併發操做了。

用戶界面(user interface,UI)自然就是異步的,大部分時間它都在等待用戶輸入,從而中斷事件循環,觸發事件處理程序。

Node.js 默認是異步的,採用它構建的服務端和用戶界面的執行機制差很少,在事件循環中等待網絡請求,而後一個接一個地處理這些請求。

異步在 JavaScript 中很是重要,由於它既適合編寫 UI,在服務端也有上佳的性能表現。

面試加分項

理解阻塞的含義,以及對性能帶來的影響。
理解事件處理程序,以及它爲何對 UI 部分的代碼很重要。
面試減分項

不熟悉同步、異步的概念。
講不清楚異步代碼和 UI 代碼的性能影響,也說不明白它倆之間的關係。
總結

多問問應聘者高層次的知識點,若是能講清楚這些概念,就說明即便應聘者沒怎麼接觸過 JavaScript,也可以在短短几個星期以內就把語言細節和語法之類的東西弄清楚。

不要由於應聘者在一些簡單的知識上表現不佳就把對方 pass 掉,好比經典的 CS-101 算法課,或者一些解謎類的題目。

面試官真正應該關注的,是應聘者是否知道如何把一堆功能組織在一塊兒,造成一個完整的應用。

電話面試的注意點就這些了,在線下的面試中,我更加關注應聘者實際編寫代碼的能力,我會觀察他如何寫代碼。在個人《精通 JavaScript 面試》這個系列文章中,會有更深刻的描述。

相關文章
相關標籤/搜索