前言

昨天遇见小学同学,没有想到它混的这么差 --- 只放了一块钱到我的碗里o(╥﹏╥)o

生存这么无聊,总得逗逗本人是不,当前我要常常给大家讲笑话,你违心听不O(∩_∩)O哈哈~

前几天写了一篇 【中高级前端】必备,30+高频手写题及具体答案(万字长文),看“你”怎么难倒我总结了30+常见手写题实现,宽广兄弟姐妹指出了其中不少问题,还有人提出没有防抖和节流等实现,胖头鱼不吃不睡又搞了12+手写题(已靠近42+),一起来看看吧。

## 直通车

点击查看日拱一题源码地址(目前已有62+个手写题实现)

1. 防抖

// 防抖:能够和你的电脑设定了10分钟睡眠工夫的场景联合起来了解// 如果你始终在用电脑,那么电脑就不会睡眠(频繁的把前一个定时器关掉,开启新的定时器)// 当你最初一次没操作电脑10分钟之后,电脑陷入睡眠const debounce = function (func, delay) {  let timer = null  return function (...args) {    clearTimeout(timer)    timer = setTimeout(() => {      func.apply(this, args)    }, delay)  }}// 测试// html局部<input type="text" id="input"/>// js局部const showName = debounce(function (name) {  console.log($input.value, this, name)}, 500)$input.addEventListener('input', (e) => {  // 500ms内进行输出才会输入  showName.call({ name: '前端胖头鱼' }, '前端胖头鱼')})

2. 节流

节流: 听凭你怎么触发,其在指定的工夫距离内只会触发一次

基于工夫戳(形式1)

const throttle = function (func, delay) {  let startTime = Date.now()  return function (...args) {    let lastTime = Date.now()    if (lastTime - startTime > delay) {      func.apply(this, args)      startTime = Date.now()    }  }}// 测试let t1 = Date.now()const showName = throttle(function (name) {  const t2 = Date.now()  console.log(this, name, t2 - t1)  t1 = Date.now()}, 1000)// 尽管设置了每隔10毫秒就会执行一次showName函数, 然而理论还是会每隔1秒才输入setInterval(() => {  showName.call({ name: '前端胖头鱼' }, '前端胖头鱼')}, 10)// { name: '前端胖头鱼' } '前端胖头鱼' 1013// { name: '前端胖头鱼' } '前端胖头鱼' 1001// { name: '前端胖头鱼' } '前端胖头鱼' 1006// { name: '前端胖头鱼' } '前端胖头鱼' 1006// { name: '前端胖头鱼' } '前端胖头鱼' 1005

基于setTimeout(形式2)

const throttle2 = function (func, delay) {  let timer = null  return function (...args) {    if (!timer) {      timer = setTimeout(() => {        func.apply(this, args)        timer = null      }, delay)     }  }}// 测试let t1 = Date.now()const showName = throttle2(function (name) {  const t2 = Date.now()  console.log(this, name, t2 - t1)  t1 = Date.now()}, 1000)setInterval(() => {  showName.call({ name: '前端胖头鱼' }, '前端胖头鱼')}, 10)// { name: '前端胖头鱼' } '前端胖头鱼' 1014// { name: '前端胖头鱼' } '前端胖头鱼' 1001// { name: '前端胖头鱼' } '前端胖头鱼' 1007// { name: '前端胖头鱼' } '前端胖头鱼' 1011// { name: '前端胖头鱼' } '前端胖头鱼' 1009// { name: '前端胖头鱼' } '前端胖头鱼' 1008

3. 函数柯里化

const curry = (func, ...args) => {  // 获取函数的参数个数  const fnLen = func.length  return function (...innerArgs) {    innerArgs = args.concat(innerArgs)    // 参数未收集足的话,持续递归收集    if (innerArgs.length < fnLen) {      return curry.call(this, func, ...innerArgs)    } else {      // 否则拿着收集的参数调用func      func.apply(this, innerArgs)    }  }}// 测试const add = curry((num1, num2, num3) => {  console.log(num1, num2, num3, num1 + num2 + num3)})add(1)(2)(3) // 1 2 3 6add(1, 2)(3) // 1 2 3 6add(1, 2, 3) // 1 2 3 6add(1)(2, 3) // 1 2 3 6

4. bind

bind()  办法创立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时应用。MDN

姐妹篇 call实现

姐妹篇 apply实现

Function.prototype.bind2 = function (context, ...args) {  if (typeof this !== 'function') {    throw new TypeError('Bind must be called on a function')  }  const executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {    if (!(callingContext instanceof boundFunc)) {      // 如果调用形式不是new func的模式就间接调用sourceFunc,并且给到对应的参数即可      return sourceFunc.apply(context, args)    } else {      // 相似于执行new的几个过程      const self = Object.create(sourceFunc.prototype) // 解决new调用的模式      const result = sourceFunc.apply(self, args)      // 判断函数执行后的返回后果 非对象函数,则返回self      if (result && typeof result === 'object' || typeof result === 'function') {        return result      } else {        return self      }    }  }  const func = this    const bound = function (...innerArgs) {    return executeBound(func, bound, context, this, args.concat(innerArgs))  }  return bound}// 测试// 1. 一般调用const showName = function (sex, age) {  console.log(this, sex, age)}showName.bind2({ name: '前端胖头鱼' }, 'boy')(100) // { name: '前端胖头鱼' } 'boy' 100// 2. new 调用const Person = function (name) {  this.name = name}Person.prototype.showName = function (age) {  console.log(this, this.name, age)}const bindPerson = Person.bind(null, 'boy')const p1 = new bindPerson('前端胖头鱼')p1.showName(100) // Person { name: 'boy' } 'boy' 100

5. 实现一个简易版模板引擎

jQuery时代,模板引擎用的还是比拟多的,能够了解为它是这样一个函数,通过模板 + 数据通过一段黑盒操作最初失去须要展现的页面
const render = (template, data) => {  // \s*?是为了兼容{{name}} {{ name }}这种写法  return template.replace(/{{\s*?(\w+)\s*?}}/g, (match, key) => {    // 匹配中了则读取替换,否则替换为空字符串    return key && data.hasOwnProperty(key) ? data[ key ] : ''  })}const data = {  name: '前端胖头鱼',  age: 100}const template = `  我是: {{ name }}  年龄是: {{age}}`console.log(render(template, data))/*我是: 前端胖头鱼年龄是: 100*/

6. 类数组转化为数组的4种形式

// 类数组转化为数组const arrayLikeObj = {  0: '前端胖头鱼',  1: 100,  length: 2}// 1. [].sliceconsole.log([].slice.call(arrayLikeObj))// 2. Array.fromconsole.log(Array.from(arrayLikeObj))// 3. Array.applyconsole.log(Array.apply(null, arrayLikeObj))// 4. [].concatconsole.log([].concat.apply([], arrayLikeObj))

7. 请实现 DOM2JSON 一个函数,能够把一个 DOM 节点输入 JSON 的格局

已经在字节的面试中呈现过
const dom2json = (rootDom) => {  if (!rootDom) {    return   }  let rootObj = {    tagName: rootDom.tagName,    children: []  }  const children = rootDom.children  // 读取子节点(元素节点)  if (children && children.length) {    Array.from(children).forEach((ele, i) => {      // 递归解决      rootObj.children[ i ] = dom2json(ele)    })  }  return rootObj}

测试

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>dom2json</title></head><body>  <div class="box">    <p class="p">hello world</p>    <div class="person">      <span class="name">前端胖头鱼</span>      <span class="age">100</span>    </div>  </div>  <script>    const dom2json = (rootDom) => {      if (!rootDom) {        return       }      let rootObj = {        tagName: rootDom.tagName,        children: []      }      const children = rootDom.children      if (children && children.length) {        Array.from(children).forEach((ele, i) => {          rootObj.children[ i ] = dom2json(ele)        })      }      return rootObj    }    const json = dom2json(document.querySelector('.box'))    console.log(json)  </script></body></html>

8. 列表转树形构造

置信大家工作中也遇到过相似的问题,前端须要的是树形构造的数据,然而后盾返回的是一个list,咱们须要将list转化为树形构造(当然这里你也能够把你的后端同学干啪为啥不给我想要的数据)。
const arrayToTree = (array) => {  const hashMap = {}  let result = []  array.forEach((it) => {    const { id, pid } = it    // 不存在时,先申明children树形    // 这一步也有可能在上面呈现    if (!hashMap[id]) {      hashMap[id] = {        children: []      }    }    hashMap[id] = {      ...it,      children: hashMap[id].children    }    // 解决以后的item    const treeIt = hashMap[id]    // 根节点,间接push    if (pid === 0) {      result.push(treeIt)    } else {      // 也有可能以后节点的父父节点还没有退出hashMap,所以须要独自解决一下      if (!hashMap[pid]) {        hashMap[pid] = {          children: []        }      }      // 非根节点的话,找到父节点,把本人塞到父节点的children中即可      hashMap[pid].children.push(treeIt)    }  })  return result}// 测试const data = [  // 留神这里,专门把pid为1的元素放一个在后面  { id: 2, name: '部门2', pid: 1 },  { id: 1, name: '部门1', pid: 0 },  { id: 3, name: '部门3', pid: 1 },  { id: 4, name: '部门4', pid: 3 },  { id: 5, name: '部门5', pid: 4 },  { id: 7, name: '部门7', pid: 6 },]console.log(JSON.stringify(arrayToTree(data), null, 2))/*[  {    "id": 1,    "name": "部门1",    "pid": 0,    "children": [      {        "id": 2,        "name": "部门2",        "pid": 1,        "children": []      },      {        "id": 3,        "name": "部门3",        "pid": 1,        "children": [          {            "id": 4,            "name": "部门4",            "pid": 3,            "children": [              {                "id": 5,                "name": "部门5",                "pid": 4,                "children": []              }            ]          }        ]      }    ]  }]*/

9. 树形构造转列表

反过来也能够试试看
const tree2list = (tree) => {  let list = []  let queue = [...tree]  while (queue.length) {    // 从后面开始取出节点    const node = queue.shift()    const children = node.children    // 取出以后节点的子节点,放到队列中,期待下一次循环    if (children.length) {      queue.push(...children)    }    // 删除多余的children树形    delete node.children    // 放入列表    list.push(node)  }  return list}// 测试const data = [  {    "id": 1,    "name": "部门1",    "pid": 0,    "children": [      {        "id": 2,        "name": "部门2",        "pid": 1,        "children": []      },      {        "id": 3,        "name": "部门3",        "pid": 1,        "children": [          {            "id": 4,            "name": "部门4",            "pid": 3,            "children": [              {                "id": 5,                "name": "部门5",                "pid": 4,                "children": []              }            ]          }        ]      }    ]  }]console.log(tree2list(data))/*[   { id: 1, name: '部门1', pid: 0 },  { id: 2, name: '部门2', pid: 1 },  { id: 3, name: '部门3', pid: 1 },  { id: 4, name: '部门4', pid: 3 },  { id: 5, name: '部门5', pid: 4 } ]*/

10. sleep

实现一个函数,n秒后执行函数func
const sleep = (func, delay) => {  return new Promise((resolve) => {    setTimeout(() => {      resolve(func())    }, delay)  })}const consoleStr = (str) => {  return () => {    console.log(str)    return str  }}const doFns = async () => {  const name = await sleep(consoleStr('前端胖头鱼'), 1000)  const sex = await sleep(consoleStr('boy'), 1000)  const age = await sleep(consoleStr(100), 1000)  console.log(name, sex, age)}doFns()// 前端胖头鱼  1s later// boy 2s later// 100 3s later// 前端胖头鱼 boy 100

11. 菲波那切数列

斐波那契数,通常用 F(n) 示意,造成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,前面的每一项数字都是后面两项数字的和。也就是:F(0) = 0,F(1) = 1F(n) = F(n - 1) + F(n - 2),其中 n > 1给你 n ,请计算 F(n) 。示例 1:输出:2输入:1解释:F(2) = F(1) + F(0) = 1 + 0 = 1示例 2:输出:3输入:2解释:F(3) = F(2) + F(1) = 1 + 1 = 2示例 3:输出:4输入:3解释:F(4) = F(3) + F(2) = 2 + 1 = 3

暴力实现

依据题目意思,很容易写出上面递归的暴力代码
const fib = (n) => {  if (n === 0) {    return 0  }  if (n === 1 || n === 2) {    return 1  }  return fib(n -2) + fib(n - 1)}// 测试console.log(fib(1)) // 1console.log(fib(2)) // 1// 试着统计一下计算工夫const t1 = Date.now()console.log(fib(44)) // 701408733console.log(Date.now() - t1) // 靠近4393

缓存优化

下面的代码能够实现成果,然而性能堪忧,来看一个计算fib(10)的过程
// 计算1010 => 9 + 8 // 须要计算9和89 => 8 + 7 // 须要计算8和78 => 7 + 6 // 须要计算7和67 => 6 + 5 // 须要计算6和56 => 5 + 4 // 须要计算5和45 => 4 + 3 // 须要计算4和34 => 3 + 2 // 须要计算3和22 => 1 + 0 // 须要计算1和0

这个过程中如果依照下面暴力实现的代码会反复屡次计算某些已经计算过的值,比方8、7、6、5...等等,这个损耗是没有必要的,所以咱们能够把计算的后果进行缓存,下次遇到求同样的值,间接返回即可

const fib = (n) => {  // 缓存过间接返回  if (typeof fib[ n ] !== 'undefined') {    return fib[ n ]  }  if (n === 0) {    return 0  }  if (n === 1 || n === 2) {    return 1  }  const res = fib(n -2) + fib(n - 1)  // 缓存计算的后果  fib[ n ] = res  return res}console.log(fib(1)) // 1console.log(fib(2)) // 1const t1 = Date.now()console.log(fib(44)) // 701408733console.log(Date.now() - t1) // 1ms

12. 实现一个函数sum函数

实现一个函数sum函数满足以下法则
sum(1, 2, 3).valueOf() // 6 sum(2, 3)(2).valueOf() // 7 sum(1)(2)(3)(4).valueOf() // 10sum(2)(4, 1)(2).valueOf() // 9

剖析

仔细观察这几种调用形式能够失去以下信息

  1. sum函数能够传递一个或者多个参数
  2. sum函数调用后返回的是一个新的函数且参数可传递一个或者多个
  3. 调用.valueOf时实现最初计算

看起来是不是有点函数柯里化的感觉,后面的函数调用仅仅是在缓存每次调用的参数,而valueOf的调用则是拿着这些参数进行一次求和运算并返回后果

const sum = (...args) => {  // 申明add函数,其实次要是缓存参数的作用  // 留神add调用实现还是会返回add函数自身,使其能够链式调用  const add = (...args2) => {    args = [ ...args, ...args2 ]    return add  }  // 求和计算  add.valueOf = () => args.reduce((ret, num) => ret + num, 0)  return add}// 测试console.log(sum(1, 2, 3).valueOf()) // 6console.log(sum(2, 3)(2).valueOf()) // 7console.log(sum(1)(2)(3)(4).valueOf()) // 10console.log(sum(2)(4, 1)(2).valueOf()) // 9