這4個問題可以檢測出你JavaScript水平的高低
JavaScript現在是一種非常流行的編程語言,基于該語言,派生了大量庫和框架。 但是,無論高層生態系統如何發展,離不開原始的JavaScript。 在這里,我選擇了4個JavaScript面試問題來測試程序員使用普通JavaScript的技能。
1.實現Array.prototype.map
如何手動實現Array.prototype.map方法?
熟練使用數組的內置方法并不難。但是,如果您只是熟悉語法而又不了解原理,那么很難真正理解JavaScript。
對于Array.prototype.map,它將創建一個新數組,其中將填充在調用數組中每個元素上調用提供的函數的結果。

如果引用lodash,我們可以編寫一個map函數,如下所示:

- function map(array, iteratee) {
- let index = -1
- const length = array == null ? 0 : array.length
- const result = new Array(length)
- while (++index < length) {
- result[index] = iteratee(array[index], index, array)
- }
- return result
- }
使用示例:

2. Object.defineProperty和代理
如何實現這種編碼效果?

我們可以看到,當我們嘗試連續打印obj.a三次時,會得到三種不同的結果。看起來多么不可思議!
您可以創建一個神秘的對象obj來實現此效果嗎?
實際上,此問題有三種解決方案:
- 訪問者屬性
- Object.defineProperty
- 代理
根據ECMAScript,對象的屬性可以采用兩種形式:

從邏輯上講,對象是屬性的集合。每個屬性都是數據屬性或訪問器屬性:
- 數據屬性將鍵值與ECMAScript語言值和一組布爾屬性相關聯。
- 訪問器屬性將鍵值與一個或兩個訪問器函數以及一組布爾屬性相關聯。訪問器函數用于存儲或檢索與屬性關聯的ECMAScript語言值。
所謂的數據屬性通常是我們寫的:
let obj = { a: 1, b: 2}
我們對一個對象的屬性只有兩個操作:讀取屬性和設置屬性。對于訪問器屬性,我們使用get和set方法定義屬性,其編寫方式如下:

- let obj = {
- get a(){
- console.log('triggle get a() method')
- console.log('you can do anything as you want')
- return 1
- },
- set a(value){
- console.log('triggle set a() method')
- console.log('you can do anything as you want')
- console.log(`you are trying to assign ${value} to obj.a`)
- }
- }

訪問屬性為我們提供了強大的元編程能力,因此我們可以通過以下方式滿足我們的要求:
- let obj = {
- _initValue: 0,
- get a() {
- this._initValue++;
- return this._initValue
- }
- }
- console.log(obj.a, obj.a, obj.a)

第二種方法是使用Object.defineProperty,該方法的工作方式與我們用來訪問屬性的方法相同,除了不是直接聲明訪問屬性,而是通過Object.defineProperty配置訪問屬性。
這使用起來更加靈活,因此我們可以這樣編寫:

- let obj = {}Object.defineProperty(obj, 'a', { get: (function(){ let initValue = 0; return function(){ initValue++; return initValue } })()})console.log(obj.a, obj.a, obj.a)
在這里的get方法中,我們使用了一個閉包,以便我們需要使用的變量initValue隱藏在閉包中,并且不會污染其他范圍。
第三種方法是使用代理。
使用代理,我們可以攔截對對象屬性的訪問。 只要我們使用代理來攔截對obj.a的訪問,然后依次返回1、2和3,我們就可以在以下條件之前完成要求:
- let initValue = 0;
- let obj = new Proxy({}, {
- get: function(item, property, itemProxy){
- if(property === 'a'){
- initValue++;
- return initValue
- }
- return item[property]
- }
- })
- console.log(obj.a, obj.a, obj.a)

為什么理解這個問題很重要?因為Object.defineProperty和Proxy給了我們強大的元編程能力,所以我們可以適當地修改對象以做一些特殊的事情。
在著名的前端框架Vue中,其核心機制之一是數據的雙向綁定。在Vue2.0中,Vue通過使用Object.defineProperty實現了該機制。在Vue3.0中,使用Proxy完成此機制。
如果不掌握Vue之類的框架,您將無法真正理解。如果您掌握了這些原則,則只需學習Vue的一半,就可以獲得兩倍的結果。
3.范圍和閉包
運行此代碼的結果是什么?

- function foo(a,b) {
- console.log(b)
- return {
- foo:function(c){
- return foo(c,a);
- }
- };
- }
- let res = foo(0);
- res.foo(1);
- res.foo(2);
- res.foo(3);
上面的代碼同時具有多個嵌套函數和三個foo嵌套函數,乍一看看起來非常繁瑣。那么,我們如何理解這一點呢?
首先,請確保上面的代碼中有多少個功能?我們可以看到在上面的代碼中的兩個地方都使用了關鍵字函數,因此上面的代碼中有兩個函數,即第一行函數foo(a,b) 和第四行 foo:function(c)。并且這兩個函數具有相同的名稱。
第二個問題:第5行的foo(c,a)調用哪個函數?如果不確定,讓我們來看一個簡單的示例:
- var obj={
- fn:function (){
- console.log(fn);
- }
- };
- obj.fn()
如果我們運行該代碼,是否會引發異常? 答案是肯定的。

這是因為obj.fn()方法的上限是全局的,并且無法訪問obj內部的fn方法。
回到前面的示例,以同樣的邏輯,當我們調用foo(c,a)時,實際上是在第一行上調用foo函數。
當我們調用res.foo(1)時,將調用哪個foo? 顯然,第4行的foo函數被調用。
因為這兩個foo函數的工作方式不同,所以我們可以將其中一個的名稱更改為bar,以使我們更容易理解代碼。

- function foo(a,b) {
- console.log(b)
- return {
- bar:function(c){
- return foo(c,a);
- }
- };
- }
- let res = foo(0);
- res.bar(1);
- res.bar(2);
- res.bar(3);
此更改不會影響最終結果,但會使我們更容易理解代碼。如果將來遇到類似的問題,請嘗試此技巧。
每次調用一個函數時,都會創建一個新的作用域,因此我們可以繪制圖表以幫助我們理解代碼工作原理的邏輯。
當我們執行let res = foo(0);時,實際上是在執行foo(0,undefiend)。此時,將在程序中創建一個新的作用域,在當前作用域中a = 0,b = undefined。因此,我繪制的圖看起來像這樣。

然后將執行console.log(b),因此它第一次在控制臺中打印出" undefined"。
然后執行res.bar(1),創建一個新范圍,其中c = 1:

然后從上面的函數中再次調用foo(c,a),它實際上是foo(1,0),作用域如下所示:

在新作用域中,a的值為1,b的值為0,因此控制臺將打印出0。
再次執行res.bar(2)。注意,res.bar(2)和res.bar(1)是并行關系,因此我們應該像這樣繪制范圍圖:

因此,在此代碼中,控制臺也會打印出值0。
執行res.bar(3)的過程也是如此,控制臺仍顯示0。
因此,以上代碼的最終結果是:

實際上,上述問題可以用其他方式改變。例如,可以將其更改為以下內容:

- function foo(a,b) {
- console.log(b)
- return {
- foo:function(c){
- return foo(c,a);
- }
- };
- }
- foo(0).foo(1).foo(2).foo(3);
在解決這個問題之前,我們要做的第一件事是區分兩個不同的foo函數,因此可以將上面的代碼更改為如下所示:

- function foo(a,b) {
- console.log(b)
- return {
- bar:function(c){
- return foo(c,a);
- }
- };
- }
- foo(0).bar(1).bar(2).bar(3);
執行foo(0)時,作用域與以前相同,然后控制臺將打印出" undefined"。

然后執行.bar(1)創建一個新的作用域。此參數1實際上是c的值。

然后.bar(1)方法再次調用foo(c,a),它實際上是foo(1,0)。這里的參數1實際上將是新作用域中a的值,而0將是新作用域中b的值。

因此,控制臺隨后輸出了b的值,即0。
再次調用.bar(2),在新作用域中c的值為2:

然后.bar(2)調用foo(c,a),它實際上是foo(2,1),其中2是新作用域中a的值,而1是新作用域中b的值。

因此,控制臺隨后輸出了b的值,即0。
然后它將執行.bar(3),該過程與之前相同,因此我將不擴展其描述,此步驟控制臺將打印出2。
如上所述,代碼運行的最終結果是:

好了,經過漫長的旅程,我們終于得到了答案。 這個問題很好地檢驗了受訪者對封閉和范圍的理解。
4.撰寫 Compose
假設我們有一個看起來像這樣的函數:

- function compose (middleware) { // some code}
compose函數接受函數數組中間件:

- let middleware = []
- middleware.push((next) => {
- console.log(1)
- next()
- console.log(1.1)
- })
- middleware.push((next) => {
- console.log(2)
- next()
- console.log(2.1)
- })
- middleware.push(() => {
- console.log(3)
- })
- let fn = compose(middleware)
- fn()
當我們嘗試執行fn時,它將調用中間件中的函數,并將下一個函數作為參數傳遞給每個小函數。
如果我們在一個小函數中執行next,則將調用中間件中該函數的next函數。而且,如果您接下來不執行,程序也不會崩潰。
執行完上面的代碼后,我們得到以下結果:
1232.11.1
那么,我們如何編寫一個compose函數來做到這一點呢?
首先,compose函數必須返回一個composed函數,因此我們可以編寫如下代碼:

- function compose (middleware) {
- return function () { }
- }
然后,在返回的函數中,中間件的第一個函數開始執行。我們還將傳遞下一個函數作為其參數。所以讓我們這樣寫:

function compose (middleware) {
- function compose (middleware) {
- return function () {
- let f1 = middleware[0]
- f1(function next(){ })
- }
- }
下一個功能充當繼續在中間件中運行的開關,如下所示:

- function compose (middleware) {
- return function () {
- let f1 = middleware[0]
- f1(function next(){
- let f2 = middleware[1]
- f2(function next(){ ... })
- })
- }
- }
然后繼續在下一個函數中調用第三個函數…等待,這看起來像遞歸! 因此,我們可以編寫一個遞歸函數來完成此嵌套調用:

- function compose (middleware) {
- return function () {
- dispatch(0)
- function dispatch (i) {
- const fn = middleware[i]
- if (!fn) return null
- fn(function next () {
- dispatch(i + 1)
- })
- }
- }
- }
好的,這就是我們的撰寫功能,所以讓我們對其進行測試:

好吧,此功能完全可以完成其所需的工作。 但是我們也可以優化我們的compose函數可以支持異步函數。 我們可以改進以下代碼:

- function compose (middleware) {
- return async function () {
- await dispatch(0)
- function async dispatch (i) {
- const fn = middleware[i]
- if (!fn)
- return null
- await fn(function next () {
- dispatch(i + 1)
- })
- }
- }
- }
實際上,以上的撰寫功能是眾所周知的節點框架koa的核心機制。
當我選擇候選人時,我接受他/她對某些框架不熟悉。畢竟,JavaScript生態系統中有太多的庫和框架,沒有人能完全掌握它們。但是我確實希望候選人知道這些重要的原始JavaScript技巧,因為它們是所有庫和框架的基礎。
結論
實際上,我的草稿中還有其他一些面試問題,但由于本文篇幅有限,因此在此不再繼續解釋。稍后再與您分享。
本文主要涉及普通JavaScript,而不涉及瀏覽器,節點,框架,算法,設計模式等。如果您對這些主題也感興趣,請隨時發表評論。