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

如果引用lodash,我們可以編寫一個(gè)map函數(shù),如下所示:

- 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和代理
如何實(shí)現(xiàn)這種編碼效果?

我們可以看到,當(dāng)我們嘗試連續(xù)打印obj.a三次時(shí),會(huì)得到三種不同的結(jié)果??雌饋矶嗝床豢伤甲h!
您可以創(chuàng)建一個(gè)神秘的對(duì)象obj來實(shí)現(xiàn)此效果嗎?
實(shí)際上,此問題有三種解決方案:
- 訪問者屬性
- Object.defineProperty
- 代理
根據(jù)ECMAScript,對(duì)象的屬性可以采用兩種形式:

從邏輯上講,對(duì)象是屬性的集合。每個(gè)屬性都是數(shù)據(jù)屬性或訪問器屬性:
- 數(shù)據(jù)屬性將鍵值與ECMAScript語言值和一組布爾屬性相關(guān)聯(lián)。
- 訪問器屬性將鍵值與一個(gè)或兩個(gè)訪問器函數(shù)以及一組布爾屬性相關(guān)聯(lián)。訪問器函數(shù)用于存儲(chǔ)或檢索與屬性關(guān)聯(lián)的ECMAScript語言值。
所謂的數(shù)據(jù)屬性通常是我們寫的:
let obj = { a: 1, b: 2}
我們對(duì)一個(gè)對(duì)象的屬性只有兩個(gè)操作:讀取屬性和設(shè)置屬性。對(duì)于訪問器屬性,我們使用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`)
- }
- }

訪問屬性為我們提供了強(qiáng)大的元編程能力,因此我們可以通過以下方式滿足我們的要求:
- 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方法中,我們使用了一個(gè)閉包,以便我們需要使用的變量initValue隱藏在閉包中,并且不會(huì)污染其他范圍。
第三種方法是使用代理。
使用代理,我們可以攔截對(duì)對(duì)象屬性的訪問。 只要我們使用代理來攔截對(duì)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)

為什么理解這個(gè)問題很重要?因?yàn)镺bject.defineProperty和Proxy給了我們強(qiáng)大的元編程能力,所以我們可以適當(dāng)?shù)匦薷膶?duì)象以做一些特殊的事情。
在著名的前端框架Vue中,其核心機(jī)制之一是數(shù)據(jù)的雙向綁定。在Vue2.0中,Vue通過使用Object.defineProperty實(shí)現(xiàn)了該機(jī)制。在Vue3.0中,使用Proxy完成此機(jī)制。
如果不掌握Vue之類的框架,您將無法真正理解。如果您掌握了這些原則,則只需學(xué)習(xí)Vue的一半,就可以獲得兩倍的結(jié)果。
3.范圍和閉包
運(yùn)行此代碼的結(jié)果是什么?

- 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);
上面的代碼同時(shí)具有多個(gè)嵌套函數(shù)和三個(gè)foo嵌套函數(shù),乍一看看起來非常繁瑣。那么,我們?nèi)绾卫斫膺@一點(diǎn)呢?
首先,請(qǐng)確保上面的代碼中有多少個(gè)功能?我們可以看到在上面的代碼中的兩個(gè)地方都使用了關(guān)鍵字函數(shù),因此上面的代碼中有兩個(gè)函數(shù),即第一行函數(shù)foo(a,b) 和第四行 foo:function(c)。并且這兩個(gè)函數(shù)具有相同的名稱。
第二個(gè)問題:第5行的foo(c,a)調(diào)用哪個(gè)函數(shù)?如果不確定,讓我們來看一個(gè)簡單的示例:
- var obj={
- fn:function (){
- console.log(fn);
- }
- };
- obj.fn()
如果我們運(yùn)行該代碼,是否會(huì)引發(fā)異常? 答案是肯定的。

這是因?yàn)閛bj.fn()方法的上限是全局的,并且無法訪問obj內(nèi)部的fn方法。
回到前面的示例,以同樣的邏輯,當(dāng)我們調(diào)用foo(c,a)時(shí),實(shí)際上是在第一行上調(diào)用foo函數(shù)。
當(dāng)我們調(diào)用res.foo(1)時(shí),將調(diào)用哪個(gè)foo? 顯然,第4行的foo函數(shù)被調(diào)用。
因?yàn)檫@兩個(gè)foo函數(shù)的工作方式不同,所以我們可以將其中一個(gè)的名稱更改為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);
此更改不會(huì)影響最終結(jié)果,但會(huì)使我們更容易理解代碼。如果將來遇到類似的問題,請(qǐng)嘗試此技巧。
每次調(diào)用一個(gè)函數(shù)時(shí),都會(huì)創(chuàng)建一個(gè)新的作用域,因此我們可以繪制圖表以幫助我們理解代碼工作原理的邏輯。
當(dāng)我們執(zhí)行l(wèi)et res = foo(0);時(shí),實(shí)際上是在執(zhí)行foo(0,undefiend)。此時(shí),將在程序中創(chuàng)建一個(gè)新的作用域,在當(dāng)前作用域中a = 0,b = undefined。因此,我繪制的圖看起來像這樣。

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

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

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

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

實(shí)際上,上述問題可以用其他方式改變。例如,可以將其更改為以下內(nèi)容:

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

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

然后執(zhí)行.bar(1)創(chuàng)建一個(gè)新的作用域。此參數(shù)1實(shí)際上是c的值。

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

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

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

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

好了,經(jīng)過漫長的旅程,我們終于得到了答案。 這個(gè)問題很好地檢驗(yàn)了受訪者對(duì)封閉和范圍的理解。
4.撰寫 Compose
假設(shè)我們有一個(gè)看起來像這樣的函數(shù):

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

- 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()
當(dāng)我們嘗試執(zhí)行fn時(shí),它將調(diào)用中間件中的函數(shù),并將下一個(gè)函數(shù)作為參數(shù)傳遞給每個(gè)小函數(shù)。
如果我們?cè)谝粋€(gè)小函數(shù)中執(zhí)行next,則將調(diào)用中間件中該函數(shù)的next函數(shù)。而且,如果您接下來不執(zhí)行,程序也不會(huì)崩潰。
執(zhí)行完上面的代碼后,我們得到以下結(jié)果:
1232.11.1
那么,我們?nèi)绾尉帉懸粋€(gè)compose函數(shù)來做到這一點(diǎn)呢?
首先,compose函數(shù)必須返回一個(gè)composed函數(shù),因此我們可以編寫如下代碼:

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

function compose (middleware) {
- function compose (middleware) {
- return function () {
- let f1 = middleware[0]
- f1(function next(){ })
- }
- }
下一個(gè)功能充當(dāng)繼續(xù)在中間件中運(yùn)行的開關(guān),如下所示:

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

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

好吧,此功能完全可以完成其所需的工作。 但是我們也可以優(yōu)化我們的compose函數(shù)可以支持異步函數(shù)。 我們可以改進(jìn)以下代碼:

- 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)
- })
- }
- }
- }
實(shí)際上,以上的撰寫功能是眾所周知的節(jié)點(diǎn)框架koa的核心機(jī)制。
當(dāng)我選擇候選人時(shí),我接受他/她對(duì)某些框架不熟悉。畢竟,JavaScript生態(tài)系統(tǒng)中有太多的庫和框架,沒有人能完全掌握它們。但是我確實(shí)希望候選人知道這些重要的原始JavaScript技巧,因?yàn)樗鼈兪撬袔旌涂蚣艿幕A(chǔ)。
結(jié)論
實(shí)際上,我的草稿中還有其他一些面試問題,但由于本文篇幅有限,因此在此不再繼續(xù)解釋。稍后再與您分享。
本文主要涉及普通JavaScript,而不涉及瀏覽器,節(jié)點(diǎn),框架,算法,設(shè)計(jì)模式等。如果您對(duì)這些主題也感興趣,請(qǐng)隨時(shí)發(fā)表評(píng)論。