42+JavaScript高頻手寫題及詳細答案,胖頭魚喊你直接通過考核

前端胖頭魚發表於2021-10-20

前言

歡迎關注”前端胖頭魚“公眾號,也許你為素未謀面,但很可能相見恨晚噢。

昨天遇見小學同學,沒有想到它混的這麼差 --- 只放了一塊錢到我的碗裡o(╥﹏╥)o

生活這麼無聊,總得逗逗自己是不,以後我要經常給大家講笑話,你願意聽不O(∩_∩)O哈哈~

前幾天寫了一篇 【中高階前端】必備,30+高頻手寫題及詳細答案(萬字長文),看“你”怎麼難倒我總結了30+常見手寫題實現,廣大兄弟姐妹指出了其中不少問題,還有人提出沒有防抖和節流等實現,胖頭魚不吃不睡又搞了12+手寫題(已接近42+),一起來看看吧。

## 直通車

點選檢視日拱一題原始碼地址(目前已有62+個手寫題實現)

手寫實現題.jpg

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>

image.png

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

相關文章