前端面試:分享一道曾讓我栽在二面的面試題|項(xiàng)目復(fù)盤

前言

一轉(zhuǎn)眼又到了跳槽季,是時(shí)候跟大家分享一波面試題了,通常來(lái)說(shuō)大家在跳槽之前都會(huì)刷題,甚至說(shuō)從不刷題的多年開發(fā)經(jīng)驗(yàn)的大神在面試中可能干不過(guò)刷過(guò)各種題的面試者,導(dǎo)致了一頓內(nèi)卷,這也側(cè)面印證了看面試題的重要性。

我也看過(guò)很多的面試題文章,比較反感的是那種代碼片段巨長(zhǎng)或者牽扯到的知識(shí)點(diǎn)巨廣抑或是與各種復(fù)雜的數(shù)學(xué)公式相關(guān)的還有就是過(guò)于底層的那種題:像什么手寫Promise A+規(guī)范、手撕紅黑樹、用公式實(shí)現(xiàn)個(gè)什么Canvas xx特效、瀏覽器是怎么實(shí)現(xiàn)xxx API的、如果瀏覽器沒(méi)有提供這個(gè) API 的話要怎么去模仿實(shí)現(xiàn)該 API,Vue3 的 diff 算法和 React 的 diff 算法有什么區(qū)別、能不能手寫個(gè)他倆的結(jié)合版 diff 算法、手寫個(gè) React 的時(shí)間切片……

相信大部分人也和我一樣,每次看這種文章的時(shí)候,不是看著看著就沒(méi)耐心往下繼續(xù)看了、就是看到一半就忍不住翻到評(píng)論區(qū)看評(píng)論了,然后點(diǎn)贊、關(guān)注、收藏一鍵三連,收藏夾里都快積攢上千篇文章了,這種文章雖然技術(shù)含量很高,但是過(guò)于的枯燥乏味,抑或是牽扯到的知識(shí)點(diǎn)過(guò)廣,要是其中哪個(gè)知識(shí)點(diǎn)自己不太熟的話,后續(xù)的內(nèi)容也就都看不懂了。就像是上數(shù)學(xué)課一樣,剛開始沒(méi)認(rèn)真聽(tīng)講,落下了某個(gè)知識(shí)點(diǎn)沒(méi)聽(tīng)到,再回過(guò)神來(lái)的時(shí)候發(fā)現(xiàn)已經(jīng)聽(tīng)不懂了。

比如有一次看一篇文章是實(shí)現(xiàn)個(gè)什么非常炫酷的 Canvas 特效,看著看著突然冒出來(lái)了三角函數(shù),雖然中學(xué)的時(shí)候也都學(xué)過(guò)這些,但經(jīng)過(guò)這么多年后早就把什么 sin、cos、tan 這些符號(hào)的意思忘的差不多了,但也懶不想再打開瀏覽器一頓搜索一頓查,就繼續(xù)往下看吧,看著看著又出現(xiàn)個(gè)什么矩陣算法,大學(xué)的時(shí)候其實(shí)也學(xué)過(guò),總之看到最后實(shí)現(xiàn)出來(lái)的效果非常酷,但具體是怎么實(shí)現(xiàn)的心里也是云里霧里的。除非真的工作中要用到這個(gè),才會(huì)仔細(xì)看文章去鉆研,即使是工作中不會(huì)用到的同時(shí)還仔細(xì)鉆研了一番,通常很快也就會(huì)忘記。

而另外一種文章則是非常立竿見(jiàn)影的:那就是講述的知識(shí)點(diǎn)并不復(fù)雜,只是以前從未想過(guò)可以這樣用,相當(dāng)于是一種思路,抑或是自己以前不知道的一個(gè) API,用起來(lái)很方便。這種文章看著也不會(huì)特別的枯燥乏味、并且還看的津津有味的,感嘆:原來(lái)還可以這樣用啊!自己以前怎么就沒(méi)有想到呢?

這種文章看過(guò)了不會(huì)特別容易忘記、甚至在工作的過(guò)程中還會(huì)找機(jī)會(huì)去用一下試驗(yàn)試驗(yàn)。給大家舉幾個(gè)例子:

一定時(shí)間無(wú)操作時(shí)播放視頻

當(dāng)時(shí)處于剛剛?cè)胄械碾A段,經(jīng)驗(yàn)比較差,所以有些很普通的需求自己卻沒(méi)思路。當(dāng)時(shí)做的是 Electron 項(xiàng)目,放在陽(yáng)明古鎮(zhèn)的一面墻上展示,需求是當(dāng)用戶十分鐘都不操作界面的話就自動(dòng)播放陽(yáng)明古鎮(zhèn)的宣傳視頻,當(dāng)時(shí)腦子就像是卡住了一樣:怎么才能知道用戶十分鐘都沒(méi)有操作呢?為此還專門去查找有沒(méi)有這樣的 API,后來(lái)看到一篇文章讓我大呼真妙!??

原理也超級(jí)簡(jiǎn)單,就是在頁(yè)面上設(shè)置一個(gè)十分鐘的變量:

let minute = 10
復(fù)制代碼

然后設(shè)置個(gè)定時(shí)器每分鐘 -1:

setInterval(() => {
  minute--
  if (minute <= 0) {
    // 播放視頻
  }
 }, 1000 * 60)
復(fù)制代碼

當(dāng)有事件發(fā)生時(shí)就代表用戶在操作,需要還原變量:

window.addEventListener('click', () => minute = 10)
復(fù)制代碼

還可以監(jiān)聽(tīng) mousemove 或者鍵盤等事件,但那個(gè)項(xiàng)目是觸摸大屏,沒(méi)有鼠標(biāo)或者鍵盤,所以監(jiān)聽(tīng)點(diǎn)擊事件就夠了

短短幾行代碼就解決了我的燃眉之急,當(dāng)然那時(shí)候也菜,這么簡(jiǎn)單的需求都沒(méi)想出來(lái),不過(guò)誰(shuí)還不是從小白一步步走上來(lái)的呢?正是靠著這些文章一步步擴(kuò)展了思路才會(huì)很快的進(jìn)步。

Vue 性能優(yōu)化

看了黃老師出品的 《揭秘 Vue.js 九個(gè)性能優(yōu)化技巧》

才知道原來(lái) computed 里面的函數(shù)是可以接收一個(gè) this 參數(shù)的:

computed: {
  a () { return 1 },
  b ({ a }) {
      return a + 10
  }
}
復(fù)制代碼

這樣就不會(huì)在組件刷新時(shí)重復(fù)獲取 getter 了,以前從來(lái)沒(méi)注意過(guò)這些。

純 CSS 實(shí)現(xiàn)拖拽效果

以前我們做拖拽的時(shí)候基本都會(huì)用 JS 去實(shí)現(xiàn),很麻煩,但看了閱文前端團(tuán)隊(duì)的《純 CSS 也能實(shí)現(xiàn)拖拽效果》令我佩服的五體投地:

在傳統(tǒng) web 中,頁(yè)面滾動(dòng)是一個(gè)很常見(jiàn)交互,操作上就是利用鼠標(biāo)滾輪或者直接拖動(dòng)滾動(dòng)條。但是,移動(dòng)端可不一樣,直接用手指拖動(dòng)頁(yè)面就可以滾動(dòng)了。通常頁(yè)面是要么垂直方向滾動(dòng),要么水平方向滾動(dòng),如果兩個(gè)方向都可以滾動(dòng)呢?例如:

.dragbox {
  width: 300px;
  height: 300px;
  overflow: auto
}
.dragcon {
  width: 500px;
  height: 500px;
}
復(fù)制代碼

只需要內(nèi)部元素寬高都大于容器就實(shí)現(xiàn)兩個(gè)方向的滾動(dòng)了(記得設(shè)置overflow:auto),示意如下:

一般情況下,鼠標(biāo)滾輪只能同時(shí)滾動(dòng)一個(gè)方向(按住Shift可以滾動(dòng)另一方向),但是移動(dòng)端可以直接拖著內(nèi)容任意滾動(dòng),如下所示:

現(xiàn)在,在內(nèi)容中間添加一個(gè)元素,跟隨內(nèi)容區(qū)域一起滾動(dòng):

接下來(lái),把后面的文本隱藏起來(lái):

是不是有點(diǎn)拖拽的味道了?原理就是這么簡(jiǎn)單!

Vue3 的新語(yǔ)法

現(xiàn)在一搜 Vue3 出來(lái)的要不就是 Composition API 要么就是 新的響應(yīng)式原理,這些東西講起來(lái)都比較復(fù)雜,而且大家都忽略了好多其他的點(diǎn),比如很多時(shí)候我們想要 CSS 也能是響應(yīng)式的,比如曾經(jīng)幻想過(guò)的語(yǔ)法:

<template>
  <h1>{{ color }}</h1>
</template>

<script>
export default {
  data () {
    return {
      color: 'red'
    }
  }
}
</script>

<style>
h1 {
  color: this.color;
}
</style>
復(fù)制代碼

不過(guò)由于 CSSJS 隸屬不同上下文,這一點(diǎn)很難做到,但自從看了這篇《Vue超好玩的新特性:在CSS中引入JS變量》才發(fā)現(xiàn)原來(lái)還可以這么寫:

<template>
  <h1>{{ color }}</h1>
</template>

<script>
export default {
  data () {
    return {
      color: 'yellow'
    }
  }
}
</script>

<style>
h1 {
  color: v-bind(color)
}
</style>
復(fù)制代碼

當(dāng) this.color 發(fā)生變化時(shí),css 也會(huì)一同做出響應(yīng)。

還有就是《Vue超好玩的新特性:DOM傳送門》,這些小技巧能夠非常方便的提升我們的開發(fā)效率,但如今的 Vue3 相關(guān)文章卻很少有人提及到這些。

九宮格面試題

這種面試題代碼量不多,但卻甚少人能夠做對(duì),這篇《千萬(wàn)別小瞧九宮格 一道題就能讓候選人原形畢露!》給我們提供了很好的一個(gè)思路,因?yàn)樵谧鲞@種九宮格時(shí):

很多人以為只需要給每個(gè)格子加上一個(gè)邊框即可,而實(shí)際上如果這么做的話會(huì)變成下面這樣:

因?yàn)樵诮o每個(gè)盒子加入了邊框之后,相鄰的兩個(gè)邊框就會(huì)貼合在一起,肉眼看起來(lái)就是一個(gè)兩倍粗的邊框。而《千萬(wàn)別小瞧九宮格 一道題就能讓候選人原形畢露!》利用負(fù)邊距輕輕松松的就解決了這個(gè)難題:

你不知道的 CSS 負(fù)值

提到負(fù)邊距就讓人想起這篇《你所不知道的 CSS 負(fù)值技巧與細(xì)節(jié)》:

[圖片上傳中...(image-d30ceb-1615723259233-13)]

<template>
  <div></div>
</template>

<style>
div {
  width: 200px;
  height: 200px;
  outline: 20px solid #000;
  outline-offset: -118px;
}
</style>
復(fù)制代碼

真的是沒(méi)想到這樣就能實(shí)現(xiàn)加號(hào)。

題目

說(shuō)了這么多有點(diǎn)跑題了,本意其實(shí)是想說(shuō)明:本篇文章的算法題就像上面列舉出來(lái)的文章那樣,代碼量不多、甚至還很簡(jiǎn)單,但重點(diǎn)就是考察你對(duì)技術(shù)的靈活運(yùn)用程度,你的思維能不能轉(zhuǎn)得過(guò)彎來(lái)。

當(dāng)然也不是說(shuō)那些代碼量很多很復(fù)雜的文章不好,其實(shí)那些文章技術(shù)含量都很高,但畢竟大部分人沒(méi)有心思那么仔細(xì)的鉆研各種復(fù)雜的算法,不過(guò)你要去的如果是字節(jié)跳動(dòng)百度阿里騰訊這類大廠去面試的話,鉆研一下那些復(fù)雜的文章還是非常有必要的。

情景再現(xiàn)

面試那天我來(lái)到了一個(gè)看起來(lái)像是會(huì)議室的屋子里,面試官給了我?guī)讖埦碜雍鸵恢还P,讓我先寫,然后他就出去了。我還在想:沒(méi)人看著我難道就不怕我用手機(jī)搜索答案么?是不是有攝像頭然后通過(guò)屏幕來(lái)觀察我有沒(méi)有用手機(jī)搜索答案,進(jìn)而考察候選人的誠(chéng)實(shí)與否…

當(dāng)然我也沒(méi)有想用手機(jī)

卷子有點(diǎn)像是中學(xué)考試那樣:選擇題 + 填空題 + 大題

大題就是手寫代碼,其實(shí)挺煩這種的… 一方面寫大括號(hào)只能先寫一半,因?yàn)椴恢涝诖罄ㄌ?hào)里會(huì)寫多少行代碼,不像是編輯器里那樣,尾括號(hào)隨著行數(shù)的增加會(huì)自動(dòng)移動(dòng);另一方面是沒(méi)有控制臺(tái),自己也不知道自己寫的到底對(duì)不對(duì),只能通過(guò)直覺(jué)來(lái)判斷。

其中一道題目是:寫一個(gè)函數(shù),這個(gè)函數(shù)會(huì)返回一個(gè)數(shù)組,數(shù)組里面是 2 ~ 32 之間的隨機(jī)整數(shù)(不能重復(fù)),這個(gè)函數(shù)還可以傳入一個(gè)參數(shù),參數(shù)是幾,返回的數(shù)組長(zhǎng)度就是幾

就像這樣:

剛看到題目的時(shí)候還在想這有啥難的,寫唄!首先先來(lái)生成從 2 ~ 32 之間的隨機(jī)數(shù)…怎么生成 2 ~ 32 之間的隨機(jī)數(shù)呢?Math.random() * 32 ,可是這是生成 0 ~ 32 的,有了!先生成 0 ~ 30 之間的隨機(jī)數(shù)然后再加上 2 不就得了:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 30 + 2))
  }

  return arr
}
復(fù)制代碼

這樣寫帶來(lái)的問(wèn)題就是,隨機(jī)生成的數(shù)字會(huì)有重復(fù):

這時(shí)我想到了 ES6 的新增數(shù)據(jù)結(jié)構(gòu) Set,它里面是可以保證沒(méi)有重復(fù)值的,而且它還可以用 ... 操作符很方便的轉(zhuǎn)為數(shù)組,于是繼續(xù)寫:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 30 + 2))
  }

  arr = [...new Set(arr)]

  return arr
}
復(fù)制代碼

這樣寫雖然解決了重復(fù)值的問(wèn)題,但卻帶來(lái)了新的問(wèn)題:如果有幾個(gè)重復(fù)值數(shù)組的長(zhǎng)度就會(huì)少幾,就像這樣:

當(dāng)時(shí)我的思路是這樣的:假如 fn(10) 傳的參數(shù)是 10,如果最終出來(lái)的數(shù)組不為 10,那就用 10 減去數(shù)組的長(zhǎng)度,就是相差的位數(shù)了。比如 fn(10) 導(dǎo)致 arr.length = 8,那么 10 - 8 就代表只需要再生成 2 個(gè)隨機(jī)數(shù)就可以了,但隨機(jī)的兩個(gè)數(shù)也可能會(huì)和現(xiàn)有的 8 位數(shù)組重合,所以我們要把隨機(jī)生成的兩位數(shù)連接到原來(lái)的 8 位數(shù)組中去,然后再用 Set 數(shù)據(jù)結(jié)構(gòu)去重,用 while 循環(huán)判斷,如果傳進(jìn)來(lái)的參數(shù) 10 減去數(shù)組長(zhǎng)度 arr.length 不等于 0 的話就證明依然還是有重復(fù)項(xiàng),那就繼續(xù)再生成隨機(jī)數(shù)重復(fù)剛才的步驟,直到生成 10 位所有數(shù)字都不重復(fù)的數(shù)組就會(huì)自動(dòng)跳出 while 循環(huán),然后返回這個(gè)數(shù)組:

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 30 + 2))
  }

  arr = [...new Set(arr)]

  let len = arr.length

  while (num - len > 0) {
    arr = [...new Set(arr.concat(fn(num - len)))]
    len = arr.length
  }

  return arr
}
復(fù)制代碼

運(yùn)行結(jié)果:

當(dāng)然我在筆試的過(guò)程中是看不到運(yùn)行結(jié)果的,這是我回到家之后憑借著印象寫出來(lái)的代碼,想試驗(yàn)一下寫的對(duì)不對(duì)。

二面加難度后的題

到了二面的時(shí)候(省略問(wèn)的其他問(wèn)題),面試官說(shuō)那道題雖然你做對(duì)了,但其實(shí)有點(diǎn)像是暴力破解的感覺(jué),效率很差。比如我在函數(shù)里傳入 30,從 2 ~ 32 總共也就 30 個(gè)數(shù),你想想生成隨機(jī)數(shù)的這個(gè)方法會(huì)運(yùn)行多少次,假如足夠幸運(yùn),第一次運(yùn)行函數(shù)就生成了 29 位不同數(shù)字的數(shù)組,那么還差一位就齊了,你想想最后這一位重復(fù)的幾率有多大?是不是30分之29?重復(fù)一次就要再運(yùn)行一遍、重復(fù)一次再運(yùn)行… 每次都要新建數(shù)組然后再新建 Set 再轉(zhuǎn)回?cái)?shù)組,開銷很大的,你有沒(méi)有什么想優(yōu)化的點(diǎn)?

此時(shí)我想的是不用 Set 來(lái)去重,想的是怎么復(fù)用原來(lái)的數(shù)組不讓它重新生成,每次只返回一個(gè)數(shù)然后遞歸?還是加入第二個(gè)參數(shù),傳入原來(lái)生成的數(shù)組?

我把我的想法說(shuō)給他聽(tīng)后,他并不滿意,覺(jué)得我沒(méi)有說(shuō)到他想要的點(diǎn)上,于是他給了我點(diǎn)提示:假設(shè)生成隨機(jī)數(shù)這個(gè)操作是特別費(fèi)時(shí)的一項(xiàng)操作,你有沒(méi)有辦法只讓他運(yùn)行傳進(jìn)來(lái)的參數(shù)那么多次?就好比 fn(10),能不能只讓 Math.random 這個(gè)函數(shù)只運(yùn)行 10 次?

當(dāng)時(shí)我一聽(tīng)頭都大了,這怎么可能呢?既然是在一定范圍內(nèi)生成隨機(jī)數(shù),那么肯定無(wú)可避免的會(huì)有重復(fù)項(xiàng),哪怕不用 Set 用別的方式去重,那也必須得運(yùn)行超過(guò) 10 次啊!不過(guò)他既然這么問(wèn)了就證明肯定有什么辦法能夠做到,于是我絞盡腦汁想啊想,最終還是鉆了牛角尖:認(rèn)為無(wú)論什么方法都無(wú)法避免生成重復(fù)項(xiàng),即使是足夠幸運(yùn)運(yùn)行一次就得到了想要的結(jié)果,那也不算是技術(shù)實(shí)現(xiàn)出來(lái)的,只能算是運(yùn)氣好,最后只好攤牌說(shuō)自己沒(méi)思路。

我本以為面試到這里就要結(jié)束了,沒(méi)想到他居然主動(dòng)跟我說(shuō)了一下這道題的解法,但當(dāng)時(shí)心里有些沮喪,他說(shuō)的那一大堆都沒(méi)聽(tīng)進(jìn)去,只記住了他說(shuō)要定義兩個(gè)數(shù)組,在坐地鐵回家的路上我一直在想:兩個(gè)數(shù)組… 兩個(gè)數(shù)組?

回到家打開電腦開始寫代碼,先把我原來(lái)的那個(gè)解法做個(gè)測(cè)試,看看性能到底有沒(méi)有他說(shuō)的那么差:

0.1 毫秒,也還行啊!可能是數(shù)字小了吧,如果是生成從 0 ~ 10000 之間的隨機(jī)數(shù)應(yīng)該就崩潰了吧?修改一下函數(shù):

const fn = num => {
  let arr = []

  for (let i = num; i-- > 0;) {
    arr.push(Math.round(Math.random() * 10000))
  }

  arr = [...new Set(arr)]

  let len = arr.length

  while (num - len > 0) {
    arr = [...new Set(arr.concat(fn(num - len)))]
    len = arr.length
  }

  return arr
}
復(fù)制代碼

運(yùn)行結(jié)果:

這回確實(shí)明顯的感覺(jué)到卡頓了,兩秒多鐘才出來(lái)結(jié)果,在計(jì)算機(jī)運(yùn)算里兩千毫秒已經(jīng)算得上是天文數(shù)字了。那再來(lái)試試他說(shuō)的兩個(gè)數(shù)組:我的理解是先事先定義一個(gè)里面裝著從 2 ~ 32 范圍內(nèi)的所有整數(shù),然后再定義一個(gè)空數(shù)組用來(lái)存放結(jié)果,在數(shù)組的長(zhǎng)度(length)范圍內(nèi)隨機(jī)生成整數(shù),用這個(gè)生成出來(lái)的整數(shù)當(dāng)作下標(biāo)從那個(gè)數(shù)組中取出數(shù)字來(lái)放入空數(shù)組中,這樣即使生成出來(lái)的隨機(jī)數(shù)有重復(fù)項(xiàng)也沒(méi)有關(guān)系,因?yàn)檫@兩個(gè)數(shù)組不會(huì)有重復(fù)項(xiàng):

const fn = num => {
  const allNums = Array.from({ length: 31 }, (_, i) => i + 2)
  const result = []

  for (let i = num; i-- > 0;) {
    result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
  }

  return result
}
復(fù)制代碼

這回再來(lái)試一下:

沒(méi)有任何毛病,那性能呢?來(lái)測(cè)一下:

確實(shí)是比以前快得多,再來(lái)試一下 0 ~ 10000 的隨機(jī)數(shù):

const fn = num => {
  const allNums = Array.from({ length: 10001 }, (_, i) => i)
  const result = []

  for (let i = num; i-- > 0;) {
    result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
  }

  return result
}
復(fù)制代碼

運(yùn)行結(jié)果:

這回差距就特別明顯了:一個(gè)兩千三百多毫秒,能夠讓人明顯的感覺(jué)到卡頓、而另一個(gè)只需要七毫秒,人類的感覺(jué)就是按下回車就能夠出結(jié)果。

其實(shí)這個(gè)函數(shù)封裝的還不夠徹底,因?yàn)樯蓮膸椎綆椎碾S機(jī)數(shù)完全是寫死在函數(shù)內(nèi)部的,如果不想要生成從 2 ~ 32 的隨機(jī)數(shù)的話還需要去函數(shù)內(nèi)部改代碼,這明顯是不符合開放封閉原則的,并且用起來(lái)也不夠靈活,咱們來(lái)再次封裝一下,令其成為一個(gè)更加通用的函數(shù):

const fn = (len, from = 0, to = 100) => {
  const allNums = Array.from({ length: to - from }, (_, i) => i + from)
  const result = []

  for (let i = len; i-- > 0;) {
    result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
  }

  return result
}
復(fù)制代碼

運(yùn)行結(jié)果:

image.png

可以看到非常完美的運(yùn)行出了我們想要的結(jié)果,不過(guò)生成十位從 2 到 12 的數(shù)字為什么沒(méi)有 12 這個(gè)數(shù)呢?原來(lái)咱們封裝的這個(gè)函數(shù)是為了符合程序中左閉右開的潛規(guī)則,細(xì)心的同學(xué)應(yīng)該早就發(fā)現(xiàn)了在程序中左閉右開的這么一種現(xiàn)象,比方說(shuō)我們用 substring 方法來(lái)舉例:

'0123456'.substring(1, 5)
復(fù)制代碼

運(yùn)行結(jié)果:

[圖片上傳中...(image-62f0bf-1615723259232-2)]

可以看到咱們傳入的參數(shù)是從 1 到 5,但是最后的結(jié)果卻包含 1 而不包括 5。咱們中學(xué)的時(shí)候就學(xué)過(guò)開區(qū)間閉區(qū)間的這么一種概念,開區(qū)間指的是不包括這個(gè)數(shù),而閉區(qū)間是包括。想當(dāng)年數(shù)學(xué)老師就一直反復(fù)強(qiáng)調(diào)過(guò)這個(gè)概念,所以程序中的左閉右開應(yīng)該也是為了盡量符合數(shù)學(xué)規(guī)則而設(shè)計(jì)的吧!

但我覺(jué)得并不能說(shuō)后面實(shí)現(xiàn)的這個(gè)隨機(jī)數(shù)生成器就比前面的那個(gè)好,也是要分情況的,舉個(gè)極端點(diǎn)的案例:假如從 0 ~ 10000 里隨機(jī)生成 10 個(gè)不重復(fù)的數(shù)字,在范圍這么大的情況下重復(fù)的幾率是不是就很低了?所以第一種方案很可能只需要生成十個(gè)隨機(jī)數(shù)就滿足需求了。但如果是第二種方案的話:先要生成一個(gè)從 0 ~ 10000 的數(shù)組,這個(gè)數(shù)組太大了,但是卻只需要其中的 10 個(gè)數(shù),有點(diǎn)像是高射炮打蚊子、殺雞焉用宰牛刀的感覺(jué),只有在范圍內(nèi)占比越大的情況下,第二種函數(shù)才越合適。如果想要封裝得更智能一點(diǎn)的話,可以給大家提供個(gè)思路:

用 to - from 除以 len 的值,就是比例了。比如從 0 ~ 10000 里獲取 10 個(gè)數(shù),就相當(dāng)于 10 / 10000,也就是千分之一,這種情況下就用第一種函數(shù)去獲取隨機(jī)數(shù)、而如果是從 0 ~ 10000 里獲取 3000 個(gè)數(shù),就是十分之三的比例,此時(shí)用第二種函數(shù)會(huì)更加合適一點(diǎn):

const fn = (len, from = 0, to = 100) => {
  const ratio = (to - from) / len
  let result = []

  if (ratio > 0.3) {
    const allNums = Array.from({ length: to - from }, (_, i) => i + from)

    for (let i = len; i-- > 0;) {
      result.push(allNums.splice(Math.floor(Math.random() * allNums.length), 1)[0])
    }
  } else {
    for (let i = len; i-- > 0;) {
      result.push(Math.round(Math.random() * to + from))
    }

    result = [...new Set(result)]

    let length = result.length

    while (len - length > 0) {
      result = [...new Set(result.concat(fn(len - length, from, to)))]
      length = result.length
    }
  }

  return result
}
復(fù)制代碼

當(dāng)然這個(gè)函數(shù)還缺少很多判斷:比如當(dāng) fromto 大的時(shí)候怎么辦?傳負(fù)數(shù)的話怎么辦?傳小數(shù)的話怎么辦?bigintnumber混著傳的時(shí)候該怎么辦?lento - from 還大的時(shí)候怎么辦?這些就不在這里浪費(fèi)篇幅的去挨個(gè)封裝了,大家感興趣的話可以自己去封裝一下。

用處

大家是不是覺(jué)得這個(gè)函數(shù)除了能當(dāng)面試題幾乎沒(méi)有其他的用處了?還真不一定!做完這道題我立馬就想起來(lái)以前做[青島銀行]文化體驗(yàn)桌時(shí)的夫子問(wèn)答模塊:

image.png

這個(gè)文化體驗(yàn)桌其實(shí)是讓來(lái)到青島銀行辦業(yè)務(wù)的朋友在等待的過(guò)程中不那么無(wú)聊,尤其是有那種帶小孩來(lái)的顧客,在宣傳山東文化的同時(shí)順便插播兩條廣告賺點(diǎn)外快(是他們賺不是我賺),這個(gè)模塊一開始后端讓我從論語(yǔ)里挑二三十道題發(fā)給他,他錄入到數(shù)據(jù)庫(kù)里,然后我通過(guò)請(qǐng)求來(lái)隨機(jī)獲取 10 道題。后來(lái)他實(shí)在太忙了,說(shuō)反正也沒(méi)幾道題,讓我全寫在前端自己隨機(jī)獲取吧!

于是隨便找了些論語(yǔ)放到里面去,那么接下來(lái)就是算法了,用戶肯定不希望每次進(jìn)去都是完全相同的十道題,最起碼得帶點(diǎn)隨機(jī)性吧?當(dāng)時(shí)比較趕進(jìn)度趕時(shí)間,沒(méi)有好好寫算法,只寫了個(gè)簡(jiǎn)易版的:

const arr = ['題', '題', '題', ...'題']

const result = arr.filter(() => Math.random() > 0.5)

result.length = 10
復(fù)制代碼

這么寫是實(shí)現(xiàn)了需求,但是并不嚴(yán)謹(jǐn),導(dǎo)致的結(jié)果就是數(shù)組里面越靠前的題目越會(huì)經(jīng)常出現(xiàn),而越靠后的題目就越不容易出現(xiàn),來(lái)測(cè)試一下:

const arr = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]

const fn = num => {
  const result = arr.filter(() => Math.random() > 0.5)
  result.length = num
  return result
}
復(fù)制代碼

運(yùn)行結(jié)果:

可以看到越靠后的數(shù)字出現(xiàn)的幾率越小,前幾位數(shù)倒是經(jīng)常會(huì)出現(xiàn),0(第一題) 或 1(第二題) 幾乎次次都出現(xiàn),而最后一題在大部分情況下甚至連出場(chǎng)的機(jī)會(huì)都沒(méi)有。

不過(guò)由于當(dāng)時(shí)天天加班到凌晨,累得不行根本沒(méi)精力想算法,找了找?guī)欤?LodashUnderscore 等庫(kù)里面也并沒(méi)有發(fā)現(xiàn)能實(shí)現(xiàn)類似功能的方法,于是就先這樣了,等測(cè)試說(shuō)這里有問(wèn)題的時(shí)候再改吧!先把軟件做出來(lái)才是頭等大事。不過(guò)后來(lái)也沒(méi)人發(fā)現(xiàn)這個(gè)問(wèn)題,只有我自己知道(現(xiàn)在你們也知道啦!),天知地知、你知我知,不要去青島銀行里跟別人說(shuō)哦!

當(dāng)然有了我們前面面試題做出來(lái)的那個(gè)函數(shù),這一切都會(huì)迎刃而解,生成出來(lái)的隨機(jī)題目將會(huì)非常均衡,不會(huì)出現(xiàn)前三題出場(chǎng)機(jī)會(huì)偏高,后三題靠碰運(yùn)氣才能遇到的情況啦!如果屏幕前的你是青島人或者身處青島地區(qū)的話可以去青島銀行看看,試一下夫子問(wèn)答是不是會(huì)有這種情況。

如果你問(wèn)我明明寫出來(lái)算法了為什么不把青島銀行里的文化體驗(yàn)桌算法替換掉呢?因?yàn)橐呀?jīng)離職了呀!我沒(méi)有權(quán)限再碰這個(gè)項(xiàng)目了,而且在青島駐場(chǎng)的那些同事們也都撤回來(lái)了,公司跟青島銀行的合同也已經(jīng)結(jié)束了,驗(yàn)收的時(shí)候甲方領(lǐng)導(dǎo)也很滿意,并未提出什么整改意見(jiàn),于是這個(gè)項(xiàng)目也就這么圓滿的落下帷幕了…

我只能把那個(gè)算法的缺陷埋藏在歲月之中,在這里跟你們傾訴一下了。

了解更多加入我們前端學(xué)習(xí)圈

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,444評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,948評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,185評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,794評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,414評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,750評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容