面試大廠,手寫程式碼這些就夠了,附 codepen 地址!

shanyue發表於2022-03-23

Offer 駕到,掘友接招!我正在參與2022春招系列活動-經驗覆盤,點選檢視 活動詳情 即算參賽

大家好,我是山月。這篇文章也可在我的部落格面試路線圖進行檢視。

其中有一個高頻問題是:我如何進行程式設計題目的練習?山月再次總結一份關於手寫程式碼的練習路線。

前端手寫程式碼

為了保證程式碼能夠正常執行,除錯和測試。

以下所有的手寫程式碼都貼在 我的 codepen

以下所有的手寫程式碼都貼在 我的 codepen

以下所有的手寫程式碼都貼在 我的 codepen

準備工作

API 設計思考

作為一個工作過三年以上的老前端而言,都會明白一個事情: API的設計比實現更為重要

何解?

compose 函式常用在各種中介軟體設計中,如 redux 等。redux 函式的實現極為簡單,甚至一行就能實現,但是能夠第一個想到 compose 的更不容易。

因此前端面試中的許多面試題以 ES API 與 lodash API 的模擬實現為主,因此在手寫程式碼前需對 lodashES6+ 文件較為熟悉。

程式碼規範

在面試過程中考察程式碼,除了可以考察候選人的邏輯能力,其次,可檢視候選人的程式碼能力,比如

  1. 是否有一致的程式碼規範
  2. 是否有清晰可讀的變數命名
  3. 是否有更簡介的程式碼

對於優雅程式碼的養成能力可以檢視 Clean Code concepts adapted for JavaScript.,在 Github 上擁有 50+ K的星星數。

比如關於命名優化

// BAD
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);

// GOOD
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;

setTimeout(blastOff, MILLISECONDS_PER_DAY);

空間複雜度與時間複雜度

  • 空間複雜度(Space Complexity) 描述該演算法在執行過程中臨時佔用儲存空間大小
  • 時間複雜度(Time complexity) 描述該演算法的執行時間,常用大O符號表述

時間複雜度

該圖片來自於文章 How to find time complexity of an algorithm?
  • O(1): Constant Complexity,常數複雜度,比如計算兩次即可得出最終結論
  • O(logn): Logarithmic Complexity,最常見的是二分查詢
  • O(n): Linear Complexity,一般為一個 for 迴圈,但半個或者兩個 for 迴圈也視作 O(n)
  • O(n^2): 一般為巢狀的 for 迴圈,如氣泡排序

手寫程式碼路線圖

以下是我在諸多大廠面經中總結的程式碼題,我將根據難易程度、模組屬性總結為不同的部分。

備註: 山月總結的所有大廠面試請點選此處

因此我把題目分為以下幾類,可以按照我列出所有程式碼題的星星數及順序進行準備,每天找三道題目進行編碼,並且堅持下來,三個月後面試大廠時的編碼階段不會出問題。

  1. ES API
  2. lodash API
  3. 程式設計邏輯題
  4. 演算法與資料結構 (leetcode)

以下所有題目都可以在山月的倉庫中找到,並且大部分程式碼題可在 codepen 上,找到我的題解測試並直接執行。

01 ES API

很多大廠面試題醉心於對於原生 API 的模擬實現,雖然大部分時候無所裨益,但有時可以更進一步加深對該 API 的理解。

例如,當你手寫結束 Promise.all 時,對手寫一個併發控制的 Promises 將會更加得心應手。

bind/call/apply ⭐⭐⭐⭐⭐️️️️

高頻問題,中頻實現。

Function.prototype.fakeBind = function(obj, ...args) {
  return (...rest) => this.call(obj, ...args, ...rest)
}

sleep/delay ⭐⭐⭐⭐⭐

sleep 函式既是面試中常問到的一道程式碼題,也是日常工作,特別是測試中常用的一個工具函式。

const sleep = (seconds) => new Promise(resolve => setTimeout(resolve, seconds))

function delay (func, seconds, ...args) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Promise.resolve(func(...args)).then(resolve)
    }, seconds)
  })
}

Promise.all ⭐️⭐️⭐️⭐️⭐️

乍看簡單,實現時方覺不易。

function pAll (_promises) {
  return new Promise((resolve, reject) => {
    // Iterable => Array
    const promises = Array.from(_promises)
    // 結果用一個陣列維護
    const r = []
    const len = promises.length
    let count = 0
    for (let i = 0; i < len; i++) {
      // Promise.resolve 確保把所有資料都轉化為 Promise
      Promise.resolve(promises[i]).then(o => { 
        // 因為 promise 是非同步的,保持陣列一一對應
        r[i] = o;

        // 如果陣列中所有 promise 都完成,則返回結果陣列
        if (++count === len) {
          resolve(r)
        }
        // 當發生異常時,直接 reject
      }).catch(e => reject(e))
    }
  })
}

Array.isArray ⭐️⭐️⭐️⭐️⭐️

面試常問,不過也足夠簡單。

Array.prototype.flat ⭐️⭐️⭐️⭐️⭐️

reduceconcat 簡直是絕配

function flatten (list, depth = 1) {
  if (depth === 0) return list
  return list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b, depth - 1) : b), [])
}

注意,flatten 擁有第二個引數 depth

Promise ⭐️⭐️⭐️⭐️

不說了,要實現真正的符合規範的 Promise,十分不容易。

Array.prototype.reduce ⭐️⭐️⭐️

const reduce = (list, fn, ...init) => {
  let next = init.length ? init[0] : list[0]
  for (let i = init.length ? 0 : 1; i < list.length; i++) {
    next = fn(next, list[i], i)
  }
  return next
}

該題目看起來簡單,實際做起來有許多邊界問題需要注意,如

  1. 回撥函式中第一個 Index 是多少?
  2. 陣列為稀疏陣列如何處理?

String.prototype.trim ⭐️⭐️⭐️

在正規表示式中,\s 指匹配一個空白字元,包括空格、製表符、換頁符和換行符。等價於[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]

const trim = str => str.trim || str.replace(/^\s+|\s+$/g, '')

一般在考察正則時會考察該 API

02 lodash API

throtle/debounce ⭐⭐⭐⭐⭐️️

效能優化中減少渲染的必要手段,程式碼也足夠容易,面試題中經常會被提到。

function throttle (f, wait) {
  let timer
  return (...args) => {
    if (timer) { return }
    timer = setTimeout(() => {
      f(...args)
      timer = null
    }, wait)
  }
}
function debounce (f, wait) {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      f(...args)
    }, wait)
  }
}

cloneDeep ⭐⭐️⭐⭐⭐

深拷貝,無論在工作中的效能優化,還是面試中,都大受青睞。

使用 JSON 序列化反序列化無法解決一些複雜物件的拷貝問題,難點在於對不同的資料型別進行處理。

isEqual ⭐⭐⭐⭐⭐

深比較,在效能優化中也常用到,比 cloneDeep 難度要低一些。

get ⭐️⭐️⭐️⭐️⭐️

在 ES6+ 中,使用可選鏈操作符 ?. 可進一步減小實現難度

function get (source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b -> [a, 3, b]
  const paths = path.replace(/\[(\w+)\]/g, '.$1').replace(/\["(\w+)"\]/g, '.$1').replace(/\['(\w+)'\]/g, '.$1').split('.')
  let result = source
  for (const p of paths) {
    result = result?.[p]
  }
  return result === undefined ? defaultValue : result 
}

compose(flowRight) ⭐️⭐️⭐️⭐️⭐️

const compose = (...fns) =>
  // 注意 f、g 的位置,如果實現從左到右計算,則置換順序
  fns.reduce((f, g) => (...args) => f(g(...args)))

shuffle ⭐️⭐️⭐️⭐️⭐️

對於實現一個簡單的 shuffle,可能極其簡單。

const shuffle = (list) => list.sort((x, y) => Math.random() - 0.5)

如果不借助 Array.prototype.sort,可由以下程式碼實現

function shuffle (list) {
  const len = list.length
  let result = [...list]
  for (let i = len - 1; i > 0; i--) {
    const swapIndex = Math.floor(Math.random() * (i + 1));
    [result[i], result[swapIndex]] = [result[swapIndex], result[i]]
  }
  return result
}

生產實踐中很多場景會使用到 shuffle,如隨機生成不重複六位數的手機驗證碼

sample ⭐️⭐️⭐️⭐️⭐️

Math.random() 函式返回一個浮點, 偽隨機數在範圍從0到小於1,用數學表示就是 [0, 1),可以利用它來實現 sample 函式

Array.prototype.sample = function () { return this[Math.floor(Math.random() * this.length)] }

sampleSize ⭐️⭐️⭐️⭐️⭐️

可以根據 shuffle 來實現一個簡單的 sampleSize

const shuffle = (list) => list.sort((x, y) => Math.random() - 0.5)
const sampleSize = (list, n) => shuffle(list).slice(0, n)

maxBy ⭐⭐⭐⭐⭐

keyBy ⭐⭐⭐⭐

groupeBy ⭐⭐⭐⭐

chunk ⭐️⭐️⭐️⭐️

function chunk (list, size) {
  const l = []
  for (let i = 0; i < list.length; i++ ) {
    const index = Math.floor(i / size)
    l[index] ??= [];
    l[index].push(list[i])
  }
  return l
}

chunk ⭐️⭐️⭐️⭐️

const f = x => x

const onceF = once(f)

//=> 3
onceF(3)

//=> 3
onceF(4)

template ⭐⭐⭐⭐️️️️️

難度稍微大一點的程式設計題目。

const template = '{{ user["name"] }},今天你又學習了嗎 - 使用者ID: {{ user.id }}';

const data = {
  user: {
    id: 10086,
    name: '山月',
  }
};

//=> "山月,今天你又學習了嗎 - 使用者ID: 10086"
render(template, data); 

注意:

  1. 注意深層巢狀資料
  2. 注意 user['name'] 屬性

pickBy/omitBy ⭐⭐⭐⭐

camelCase ⭐️⭐⭐⭐

difference ⭐️⭐️⭐️

03 程式設計邏輯題

關於程式設計邏輯題,指在工作中常會遇到的一些資料處理

FizzBuzz,是否能被3或5整除 ⭐️⭐️⭐️⭐️⭐️

輸入一個整數,如果能夠被3整除,則輸出 Fizz

如果能夠被5整除,則輸出 Buzz

如果既能被3整數,又能被5整除,則輸出 FizzBuzz

//=> 'fizz'
fizzbuzz(3)

//=> 'buzz'
fizzbuzz(5)

//=> 'fizzbuzz'
fizzbuzz(15)

//=> 7
fizzbuzz(7)

這道題雖然很簡單,但在面試中仍然有大部分人無法做對

實現 Promise.map 用以控制併發數 ⭐️⭐️⭐️⭐️⭐️

用以 Promise 併發控制,面試中經常會有問到,在工作中也經常會有涉及。在上手這道問題之前,瞭解 [Promise.all]() 的實現將對實現併發控制有很多的幫助。

另外,最受歡迎的 Promise 庫 bluebirdPromise.map 進行了實現,在專案中大量使用。

非同步的 sum/add ⭐️⭐️⭐️⭐️⭐️

編碼題中的集大成者,出自頭條的面經,promise 序列,並行,二分,併發控制,層層遞進。

如何使用 JS 實現一個釋出訂閱模式 ⭐️⭐️⭐️⭐️⭐️

如何實現無限累加的 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
sum(1)(2)(3)(4)(5)(6).valueOf() // 21

這還是位元組、快手、阿里一眾大廠最為偏愛的題目,實際上有一點技巧問題。

這還是位元組、快手、阿里一眾大廠最為偏愛的題目,實際上有一點技巧問題。

這還是位元組、快手、阿里一眾大廠最為偏愛的題目,實際上有一點技巧問題。

這是一個關於懶計算的函式,使用 sum 收集所有累加項,使用 valueOf 進行計算

  • sum 返回一個函式,收集所有的累加項,使用遞迴實現
  • 返回函式帶有 valueOf 屬性,用於統一計算
function sum (...args) {
  const f = (...rest) => sum(...args, ...rest)
  f.valueOf = () => args.reduce((x, y) => x + y, 0)
  return f
}

統計陣列中最大的數/第二大的數 ⭐️⭐️⭐️⭐️⭐️

求最大的一個值:

function max (list) {
  if (!list.length) { return 0 }
  return list.reduce((x, y) => x > y ? x : y)
}

求最大的兩個值:

程式碼見 找出陣列中最大的兩個值 - codepen
function maxTwo (list) {
  let max = -Infinity, secondMax = -Infinity
  for (const x of list) {
    if (x > max) {
      secondMax = max
      max = x
    } else if (x > secondMax) {
      secondMax = x
    }
  }
  return [max, secondMax]
}

如果求 TopN,可使用大頂堆、小頂堆實現,見另一個問題

統計字串中出現次數最多的字元 ⭐️⭐️⭐️⭐️⭐️

function getFrequentChar (str) {
  const dict = {}
  for (const char of str) {
    dict[char] = (dict[char] || 0) + 1
  }
  const maxBy = (list, keyBy) => list.reduce((x, y) => keyBy(x) > keyBy(y) ? x : y)
  return maxBy(Object.entries(dict), x => x[1])
}

以下方案一邊進行計數統計一遍進行大小比較,只需要 1 次 O(n) 的演算法複雜度

function getFrequentChar2 (str) {
  const dict = {}
  let maxChar = ['', 0]
  for (const char of str) {
    dict[char] = (dict[char] || 0) + 1
    if (dict[char] > maxChar[1]) {
      maxChar = [char, dict[char]]
    }
  }
  return maxChar
}

對以下數字進行編碼壓縮 ⭐️⭐️⭐️⭐️⭐️

這是一道大廠常考的程式碼題

  • Input: 'aaaabbbccd'
  • Output: 'a4b3c2d1',代表 a 連續出現四次,b連續出現三次,c連續出現兩次,d連續出現一次

有以下測試用例

//=> a4b3c2
encode('aaaabbbcc')

//=> a4b3a4
encode('aaaabbbaaaa')

//=> a2b2c2
encode('aabbcc')

如果程式碼編寫正確,則可繼續深入:

  • 如果只出現一次,不編碼數字,如 aaab -> a3b
  • 如果只出現兩次,不進行編碼,如 aabbb -> aab3
  • 如果進行解碼數字衝突如何解決

編寫函式 encode 實現該功能

程式碼見 【Q412】對以下字元進行壓縮編碼 - codepen
function encode (str) {
  const l = []
  let i = 0
  for (const s of str) {
    const len = l.length
    const lastChar = len > 0 ? l[len - 1][0] : undefined
    if (lastChar === s) {
      l[len - 1][1]++
    } else {
      l.push([s, 1])
    }
  }
  return l.map(x => x.join('')).join('')
}

測試通過

> encode('aaab')
< "a3b1"

但是面試官往往會繼續深入

  1. 如果只出現一次,不編碼數字,如 aaab -> a3b
  2. 如果只出現兩次,不進行編碼,如 aabbb -> aab3
  3. 如果進行解碼,碰到數字如何處理?

以下是除數字外的進一步編碼

function encode (str) {
  const l = []
  let i = -1;
  let lastChar
  for (const char of str) {
    if (char !== lastChar) {
      lastChar = char
      i++
      l[i] = [char, 1];
    } else {
      l[i][1]++
    }
  }
  return l.map(([x, y]) => {
    if (y === 1) {
      return x
    }
    if (y === 2) {
      return x + x
    }
    return x + y
  }).join('')
}

LRU Cache ⭐️⭐️⭐️⭐️⭐️

實現一個函式用來對 URL 的 querystring 進行編碼與解碼 ⭐️⭐️⭐️⭐️⭐️

JSONP 的原理是什麼,如何實現 ⭐️⭐️⭐️⭐️

JSONP,全稱 JSON with Padding,為了解決跨域的問題而出現。雖然它只能處理 GET 跨域,雖然現在基本上都使用 CORS 跨域,但仍然要知道它,畢竟面試會問

JSONP 基於兩個原理:

  1. 動態建立 script,使用 script.src 載入請求跨過跨域
  2. script.src 載入的指令碼內容為 JSONP: 即 PADDING(JSON) 格式
function jsonp ({ url, onData, params }) {
  const script = document.createElement('script')

  // 一、為了避免全域性汙染,使用一個隨機函式名
  const cbFnName = `JSONP_PADDING_${Math.random().toString().slice(2)}`

  // 二、預設 callback 函式為 cbFnName
  script.src = `${url}?${stringify({ callback: cbFnName, ...params })}`

  // 三、使用 onData 作為 cbFnName 回撥函式,接收資料
  window[cbFnName] = onData;

  document.body.appendChild(script)
}

// 傳送 JSONP 請求
jsonp({
  url: 'http://localhost:10010',
  params: { id: 10000 },
  onData (data) {
    console.log('Data:', data)
  }
})

使用 JS 如何生成一個隨機字串 ⭐️⭐️⭐️⭐️⭐️

const random = (n) => Math.random().toString(36).slice(2, 2 + n)

給數字新增千位符 ⭐️⭐️⭐️

千位符替換可由正則 /(\d)(?=(\d\d\d)+(?!\d))/ 進行匹配

function numberThousands (number, thousandsSeperator = ',') {
  return String(number).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousandsSeperator)
}

04 演算法與資料結構 (leetcode)

Leetcode 簡單與中級難度題目 200/100 道,以簡單題目為主。直接刷就得了。

在我的題庫中也收集了在諸多大廠面經中總結出的多道演算法題,總結如下

輸出 100 以內的菲波那切數列

TopK 問題

典型的二叉堆問題

  1. 取陣列中前 k 個數做小頂堆,堆化
  2. 陣列中的其它數逐一與堆頂元素比較,若大於堆頂元素,則插入該數

時間複雜度 O(nlg(k))

求正序增長的正整數陣列中,其和為 N 的兩個數

求給定陣列中 N 個數相加之和為 sum 所有可能集合

求給定陣列中 N 個數相加之和為 sum 所有可能集合,請補充以下程式碼

function fn(arr, n, sum) {}

如何判斷兩個連結串列是否相交

經典問題

最終

程式碼程式設計題在面試過程中出現頻率很高,且能很大程度考察候選人的程式碼能力與程式碼風格。

相關文章