乐趣区

关于前端:金③银④-分享一道曾让我栽在二面的面试题

前言

一转眼又到了跳槽季,是时候跟大家分享一波面试题了,通常来说大家在跳槽之前都会刷题,甚至说从不刷题的多年开发教训的大神在面试中可能干不过刷过各种题的面试者,导致了一顿内卷,这也侧面印证了看面试题的重要性。

我也看过很多的面试题文章,比拟恶感的是那种 代码片段巨长 或者 牵扯到的知识点巨广 抑或是 与各种简单的数学公式相干的 还有就是 过于底层 的那种题:像什么手写 Promise A+ 标准、手撕 红黑树 、用公式实现个什么Canvas xx 特效、浏览器是怎么实现xxx API 的、如果浏览器没有提供这个 API 的话要怎么去 模拟实现该 API,Vue3 的 diff 算法和 React 的 diff 算法有什么区别 、能不能手写个他俩的 联合版 diff 算法、手写个 React 的工夫切片……

置信大部分人也和我一样,每次看这种文章的时候,不是看着看着就没急躁往下持续看了、就是看到一半就忍不住翻到评论区看评论了,而后点赞、关注、珍藏一键三连,收藏夹里都快积攒上千篇文章了,这种文章尽管技术含量很高,然而过于的枯燥乏味,抑或是牵扯到的知识点过广,要是其中哪个知识点本人不太熟的话,后续的内容也就都看不懂了。就像是上数学课一样,刚开始没认真听讲,落下了某个知识点没听到,再回过神来的时候发现曾经听不懂了。

比方有一次看一篇文章是实现个什么十分炫酷的 Canvas 特效,看着看着忽然冒出来了三角函数,尽管中学的时候也都学过这些,但通过这么多年后早就把什么 sin、cos、tan 这些符号的意思忘的差不多了,但也懒不想再关上浏览器一顿搜寻一顿查,就持续往下看吧,看着看着又呈现个什么矩阵算法,大学的时候其实也学过,总之看到最初实现进去的成果十分酷,但具体是怎么实现的心里也是云里雾里的。除非真的工作中要用到这个,才会认真看文章去钻研,即便是工作中不会用到的同时还认真钻研了一番,通常很快也就会遗记。

而另外一种文章则是十分空谷传声的:那就是讲述的知识点并不简单,只是以前从未想过能够这样用,相当于是一种思路,抑或是本人以前不晓得的一个 API,用起来很不便。这种文章看着也不会特地的枯燥乏味、并且还看的津津乐道的,感叹:原来还能够这样用啊!本人以前怎么就没有想到呢?

这种文章看过了不会特地容易遗记、甚至在工作的过程中还会找机会去用一下试验试验。给大家举几个例子:

肯定工夫无操作时播放视频

过后处于刚刚入行的阶段,教训比拟差,所以有些很一般的需要本人却没思路。过后做的是 Electron 我的项目,放在阳明古镇的一面墙上展现,需要是当用户十分钟都不操作界面的话就自动播放阳明古镇的宣传视频,过后脑子就像是卡住了一样:怎么能力晓得用户十分钟都没有操作呢?为此还专门去查找有没有这样的 API,起初看到一篇文章让我大呼真妙!????

原理也超级简略,就是在页面上设置一个十分钟的变量:

let minute = 10

而后设置个定时器每分钟 -1:

setInterval(() => {
  minute--
  if (minute <= 0) {// 播放视频}
 }, 1000 * 60)

当有事件产生时就代表用户在操作,须要还原变量:

window.addEventListener('click', () => minute = 10)

还能够监听 mousemove 或者键盘等事件,但那个我的项目是触摸大屏,没有鼠标或者键盘,所以监听点击事件就够了

短短几行代码就解决了我的当务之急,当然那时候也菜,这么简略的需要都没想进去,不过谁还不是从小白一步步走上来的呢?正是靠着这些文章一步步扩大了思路才会很快的提高。

Vue 性能优化

看了黄老师出品的《揭秘 Vue.js 九个性能优化技巧》

才晓得原来 computed 外面的函数是能够接管一个 this 参数的:

computed: {a () {return 1},
  b ({a}) {return a + 10}
}

这样就不会在组件刷新时反复获取 getter 了,以前素来没留神过这些。

纯 CSS 实现拖拽成果

以前咱们做拖拽的时候根本都会用 JS 去实现,很麻烦,但看了阅文前端团队的《纯 CSS 也能实现拖拽成果》令我拜服的嗤之以鼻:

在传统 web 中,页面滚动是一个很常见交互,操作上就是利用鼠标滚轮或者间接拖动滚动条。然而,挪动端可不一样,间接用手指拖动页面就能够滚动了。通常页面是要么垂直方向滚动,要么程度方向滚动,如果两个方向都能够滚动呢?例如:

.dragbox {
  width: 300px;
  height: 300px;
  overflow: auto
}
.dragcon {
  width: 500px;
  height: 500px;
}

只须要外部元素宽高都大于容器就实现两个方向的滚动了(记得设置 overflow:auto),示意如下:

个别状况下,鼠标滚轮只能同时滚动一个方向(按住 Shift 能够滚动另一方向),然而挪动端能够间接拖着内容任意滚动,如下所示:

当初,在内容两头增加一个元素,追随内容区域一起滚动:

接下来,把前面的文本暗藏起来:

是不是有点拖拽的滋味了?原理就是这么简略!

Vue3 的新语法

当初一搜 Vue3 进去的要不就是 Composition API 要么就是 新的响应式原理,这些货色讲起来都比较复杂,而且大家都疏忽了好多其余的点,比方很多时候咱们想要 CSS 也能是响应式的,比方已经空想过的语法:

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

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

<style>
h1 {color: this.color;}
</style>

不过因为 CSSJS 附属不同上下文,这一点很难做到,但自从看了这篇《Vue 超好玩的新个性:在 CSS 中引入 JS 变量》才发现原来还能够这么写:

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

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

<style>
h1 {color: v-bind(color)
}
</style>

this.color 发生变化时,css 也会一起做出响应。

还有就是《Vue 超好玩的新个性:DOM 传送门》,这些小技巧可能十分不便的晋升咱们的开发效率,但现在的 Vue3 相干文章却很少有人提及到这些。

九宫格面试题

这种面试题代码量不多,但却甚少人可能做对,这篇《千万别小瞧九宫格 一道题就能让候选人暴露无遗!》给咱们提供了很好的一个思路,因为在做这种九宫格时:

很多人认为只须要给每个格子加上一个边框即可,而实际上如果这么做的话会变成上面这样:

因为在给每个盒子退出了边框之后,相邻的两个边框就会贴合在一起,肉眼看起来就是一个两倍粗的边框。而《千万别小瞧九宫格 一道题就能让候选人暴露无遗!》利用负边距轻轻松松的就解决了这个难题:

你不晓得的 CSS 负值

提到负边距就让人想起这篇《你所不晓得的 CSS 负值技巧与细节》:

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

<style>
div {
  width: 200px;
  height: 200px;
  outline: 20px solid #000;
  outline-offset: -118px;
}
</style>

真的是没想到这样就能实现加号。

题目

说了这么多有点跑题了,本意其实是想阐明:本篇文章的算法题就像下面列举进去的文章那样,代码量不多、甚至还很简略,但重点就是考查你对技术的灵活运用水平,你的思维能不能转得过弯来。

当然也不是说那些代码量很多很简单的文章不好,其实那些文章技术含量都很高,但毕竟大部分人没有心理那么认真的钻研各种简单的算法,不过你要去的如果是 字节跳动 百度 阿里 腾讯 这类大厂去面试的话,钻研一下那些简单的文章还是十分有必要的。

情景再现

面试那天我来到了一个看起来像是会议室的屋子里,面试官给了我几张卷子和一只笔,让我先写,而后他就进来了。我还在想:没人看着我难道就不怕我用手机搜寻答案么?是不是有摄像头而后通过屏幕来察看我有没有用手机搜寻答案,进而考查候选人的诚恳与否…

当然我也没有想用手机

卷子有点像是中学考试那样:选择题 + 填空题 + 大题

大题就是手写代码,其实挺烦这种的… 一方面写大括号只能先写一半,因为不晓得在大括号里会写多少行代码,不像是编辑器里那样,尾括号随着行数的减少会主动挪动;另一方面是没有控制台,本人也不晓得本人写的到底对不对,只能通过直觉来判断。

其中一道题目是:写一个函数,这个函数会返回一个数组,数组外面是 2 ~ 32 之间的随机整数(不能反复),这个函数还能够传入一个参数,参数是几,返回的数组长度就是几

就像这样:

刚看到题目的时候还在想这有啥难的,写呗!首先先来生成从 2 ~ 32 之间的随机数…怎么生成 2 ~ 32 之间的随机数呢?Math.random() * 32,可是这是生成 0 ~ 32 的,有了!学生成 0 ~ 30 之间的随机数而后再加上 2 不就得了:

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

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

这样写带来的问题就是,随机生成的数字会有反复:

这时我想到了 ES6 的新增数据结构 Set,它外面是能够保障没有反复值的,而且它还能够用 ... 操作符很不便的转为数组,于是持续写:

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
}

这样写尽管解决了反复值的问题,但却带来了新的问题:如果有几个反复值数组的长度就会少几,就像这样:

过后我的思路是这样的:如果 fn(10) 传的参数是 10,如果最终进去的数组不为 10,那就用 10 减去数组的长度,就是相差的位数了。比方 fn(10) 导致 arr.length = 8,那么 10 - 8 就代表只须要再生成 2 个随机数就能够了,但随机的两个数也可能会和现有的 8 位数组重合,所以咱们要把随机生成的两位数连贯到原来的 8 位数组中去,而后再用 Set 数据结构去重,用 while 循环判断,如果传进来的参数 10 减去数组长度 arr.length 不等于 0 的话就证实仍然还是有反复项,那就持续再生成随机数反复方才的步骤,直到生成 10 位所有数字都不反复的数组就会主动跳出 while 循环,而后返回这个数组:

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
}

运行后果:

当然我在口试的过程中是看不到运行后果的,这是我回到家之后凭借着印象写进去的代码,想试验一下写的对不对。

二面加难度后的题

到了二面的时候 ( 省略问的其余问题),面试官说那道题尽管你做对了,但其实有点像是暴力破解的感觉,效率很差。比方我在函数里传入 30,从 2 ~ 32 总共也就 30 个数,你想想生成随机数的这个办法会运行多少次,如果足够侥幸,第一次运行函数就生成了 29 位不同数字的数组,那么还差一位就齐了,你想想最初这一位反复的几率有多大?是不是30 分之 29?反复一次就要再运行一遍、反复一次再运行… 每次都要新建数组而后再新建 Set 再转回数组,开销很大的,你有没有什么想优化的点?

此时我想的是不必 Set 来去重,想的是怎么复用原来的数组不让它从新生成,每次只返回一个数而后递归?还是退出第二个参数,传入原来生成的数组?

我把我的想法说给他听后,他并不称心,感觉我没有说到他想要的点上,于是他给了我点提醒:假如生成随机数这个操作是特地费时的一项操作,你有没有方法只让他运行传进来的参数那么屡次?就好比 fn(10),能不能只让 Math.random 这个函数只运行 10 次?

过后我一听头都大了,这怎么可能呢?既然是在肯定范畴内生成随机数,那么必定无可避免的会有反复项,哪怕不必 Set 用别的形式去重,那也必须得运行超过 10 次啊!不过他既然这么问了就证实必定有什么方法可能做到,于是我搜索枯肠想啊想,最终还是钻了牛角尖:认为无论什么办法都无奈防止生成反复项,即便是足够侥幸运行一次就失去了想要的后果,那也不算是技术实现进去的,只能算是运气好,最初只好摊牌说本人没思路。

我本认为面试到这里就要完结了,没想到他竟然被动跟我说了一下这道题的解法,但过后心里有些丧气,他说的那一大堆都没听进去,只记住了他说要定义两个数组,在坐地铁回家的路上我始终在想:两个数组… 两个数组?

回到家关上电脑开始写代码,先把我原来的那个解法做个测试,看看性能到底有没有他说的那么差:

0.1 毫秒,也还行啊!可能是数字小了吧,如果是生成从 0 ~ 10000 之间的随机数应该就解体了吧?批改一下函数:

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
}

运行后果:

这回的确显著的感觉到卡顿了,两秒多钟才进去后果,在计算机运算里两千毫秒曾经算得上是天文数字了。那再来试试他说的两个数组:我的了解是先当时定义一个外面装着从 2 ~ 32 范畴内的所有整数,而后再定义一个空数组用来寄存后果,在数组的长度 (length) 范畴内随机生成整数,用这个生成进去的整数当作下标从那个数组中取出数字来放入空数组中,这样即便生成进去的随机数有反复项也没有关系,因为这两个数组不会有反复项:

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
}

这回再来试一下:

没有任何故障,那性能呢?来测一下:

的确是比以前快得多,再来试一下 0 ~ 10000 的随机数:

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
}

运行后果:

这回差距就特地显著了:一个两千三百多毫秒,可能让人显著的感觉到卡顿、而另一个只须要七毫秒,人类的感觉就是按下回车就可能出后果。

其实这个函数封装的还不够彻底,因为生成从几到几的随机数齐全是写死在函数外部的,如果不想要生成从 2 ~ 32 的随机数的话还须要去函数外部改代码,这显著是不合乎凋谢关闭准则的,并且用起来也不够灵便,咱们来再次封装一下,令其成为一个更加通用的函数:

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
}

运行后果:

能够看到十分完满的运行出了咱们想要的后果,不过生成十位从 2 到 12 的数字为什么没有 12 这个数呢?原来咱们封装的这个函数是为了合乎程序中 左闭右开 的潜规则,仔细的同学应该早就发现了在程序中 左闭右开 的这么一种景象,比方说咱们用 substring 办法来举例:

'0123456'.substring(1, 5)

运行后果:

能够看到咱们传入的参数是从 1 到 5,然而最初的后果却蕴含 1 而不包含 5。咱们中学的时候就学过开区间闭区间的这么一种概念,开区间指的是 不包含 这个数,而闭区间是 包含 。想当年数学老师就始终反复强调过这个概念,所以程序中的 左闭右开 应该也是为了尽量合乎数学规定而设计的吧!

但我感觉并不能说前面实现的这个 随机数生成器 就比后面的那个好,也是要分状况的,举个极其点的案例:如果从 0 ~ 10000 里随机生成 10 个不反复的数字,在范畴这么大的状况下反复的几率是不是就很低了?所以第一种计划很可能只须要生成十个随机数就满足需要了。但如果是第二种计划的话:先要生成一个从 0 ~ 10000 的数组,这个数组太大了,然而却只须要其中的 10 个数,有点像是高射炮打蚊子、杀鸡焉用宰牛刀的感觉,只有在范畴内占比越大的状况下,第二种函数才越适合。如果想要封装得更智能一点的话,能够给大家提供个思路:

用 to – from 除以 len 的值,就是比例了。比方从 0 ~ 10000 里获取 10 个数,就相当于 10 / 10000,也就是千分之一,这种状况下就用第一种函数去获取随机数、而如果是从 0 ~ 10000 里获取 3000 个数,就是非常之三的比例,此时用第二种函数会更加适合一点:

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
}

当然这个函数还短少很多判断:比方当 fromto 大的时候怎么办?传 正数 的话怎么办?传 小数 的话怎么办?bigintnumber 混着传的时候该怎么办?lento - from 还大的时候怎么办?这些就不在这里节约篇幅的去挨个封装了,大家感兴趣的话能够本人去封装一下。

用途

大家是不是感觉这个函数除了能当面试题简直没有其余的用途了?还真不肯定!做完这道题我立马就想起来以前做 [[青岛银行] 文化体验桌](https://juejin.cn/post/686559…:

这个文化体验桌其实是让来到青岛银行办业务的敌人在期待的过程中不那么无聊,尤其是有那种带小孩来的顾客,在宣传山东文化的同时顺便插播两条广告赚点外快 ( 是他们赚不是我赚),这个模块一开始后端让我从论语里挑二三十道题发给他,他录入到数据库里,而后我通过申请来随机获取 10 道题。起初他切实太忙了,说反正也没几道题,让我全写在前端本人随机获取吧!

于是轻易找了些论语放到外面去,那么接下来就是算法了,用户必定不心愿每次进去都是完全相同的十道题,最起码得带点随机性吧?过后比拟赶进度赶时间,没有好好写算法,只写了个简易版的:

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

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

result.length = 10

这么写是实现了需要,然而并不谨严,导致的后果就是数组外面越靠前的题目越会经常出现,而越靠后的题目就越不容易呈现,来测试一下:

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
}

运行后果:

能够看到越靠后的数字呈现的几率越小,前几位数倒是常常会呈现,0(第一题 ) 或 1( 第二题) 简直次次都呈现,而最初一题在大部分状况下甚至连出场的机会都没有。

不过因为过后天天加班到凌晨,累得不行基本没精力想算法,找了找库,像 LodashUnderscore 等库外面也并没有发现能实现相似性能的办法,于是就先这样了,等测试说这里有问题的时候再改吧!先把软件做进去才是头等大事。不过起初也没人发现这个问题,只有我本人晓得 ( 当初你们也晓得啦!),天知地知、你知我知,不要去青岛银行里跟他人说哦!

当然有了咱们后面面试题做进去的那个函数,这所有都会迎刃而解,生成进去的随机题目将会十分平衡,不会呈现前三题出场机会偏高,后三题靠碰运气能力遇到的状况啦!如果屏幕前的你是青岛人或者身处青岛地区的话能够去青岛银行看看,试一下夫子问答是不是会有这种状况。

如果你问我明明写进去算法了为什么不把青岛银行里的文化体验桌算法替换掉呢?因为曾经到职了呀!我没有权限再碰这个我的项目了,而且在青岛驻场的那些共事们也都撤回来了,公司跟青岛银行的合同也曾经完结了,验收的时候甲方领导也很称心,并未提出什么整改意见,于是这个我的项目也就这么圆满的落下帷幕了…

我只能把那个算法的缺点埋藏在岁月之中,在这里跟你们倾诉一下了。

本文首发于公众号:《前端学不动》

退出移动版