乐趣区

关于前端:42JavaScript高频手写题及详细答案胖头鱼喊你直接通过考核

前言

昨天遇见小学同学,没有想到它混的这么差 — 只放了一块钱到我的碗里 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 6
add(1, 2)(3) // 1 2 3 6
add(1, 2, 3) // 1 2 3 6
add(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. [].slice
console.log([].slice.call(arrayLikeObj))
// 2. Array.from
console.log(Array.from(arrayLikeObj))
// 3. Array.apply
console.log(Array.apply(null, arrayLikeObj))
// 4. [].concat
console.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) = 1
F(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)) // 1
console.log(fib(2)) // 1
// 试着统计一下计算工夫
const t1 = Date.now()
console.log(fib(44)) // 701408733
console.log(Date.now() - t1) // 靠近 4393

缓存优化

下面的代码能够实现成果,然而性能堪忧,来看一个计算 fib(10) 的过程

// 计算 10
10 => 9 + 8 // 须要计算 9 和 8
9 => 8 + 7 // 须要计算 8 和 7
8 => 7 + 6 // 须要计算 7 和 6
7 => 6 + 5 // 须要计算 6 和 5
6 => 5 + 4 // 须要计算 5 和 4
5 => 4 + 3 // 须要计算 4 和 3
4 => 3 + 2 // 须要计算 3 和 2
2 => 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)) // 1
console.log(fib(2)) // 1

const t1 = Date.now()
console.log(fib(44)) // 701408733
console.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() // 10
sum(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()) // 6
console.log(sum(2, 3)(2).valueOf()) // 7
console.log(sum(1)(2)(3)(4).valueOf()) // 10
console.log(sum(2)(4, 1)(2).valueOf()) // 9
退出移动版