Offer 駕到,掘友接招!我正在參與2022春招系列活動-經驗覆盤,點選檢視 活動詳情 即算參賽
大家好,我是山月。這篇文章也可在我的部落格面試路線圖進行檢視。
其中有一個高頻問題是:我如何進行程式設計題目的練習?山月再次總結一份關於手寫程式碼的練習路線。
為了保證程式碼能夠正常執行,除錯和測試。
以下所有的手寫程式碼都貼在 我的 codepen 中
以下所有的手寫程式碼都貼在 我的 codepen 中
以下所有的手寫程式碼都貼在 我的 codepen 中
準備工作
API 設計思考
作為一個工作過三年以上的老前端而言,都會明白一個事情: API的設計比實現更為重要。
何解?
如 compose
函式常用在各種中介軟體設計中,如 redux
等。redux
函式的實現極為簡單,甚至一行就能實現,但是能夠第一個想到 compose
的更不容易。
因此前端面試中的許多面試題以 ES API 與 lodash API 的模擬實現為主,因此在手寫程式碼前需對 lodash
與 ES6+
文件較為熟悉。
程式碼規範
在面試過程中考察程式碼,除了可以考察候選人的邏輯能力,其次,可檢視候選人的程式碼能力,比如
- 是否有一致的程式碼規範
- 是否有清晰可讀的變數命名
- 是否有更簡介的程式碼
對於優雅程式碼的養成能力可以檢視 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 迴圈,如氣泡排序
手寫程式碼路線圖
以下是我在諸多大廠面經中總結的程式碼題,我將根據難易程度、模組屬性總結為不同的部分。
備註: 山月總結的所有大廠面試請點選此處
因此我把題目分為以下幾類,可以按照我列出所有程式碼題的星星數及順序進行準備,每天找三道題目進行編碼,並且堅持下來,三個月後面試大廠時的編碼階段不會出問題。
- ES API
- lodash API
- 程式設計邏輯題
- 演算法與資料結構 (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 ⭐️⭐️⭐️⭐️⭐️
- 程式碼: Promise.all
- 題目: 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.isArray
面試常問,不過也足夠簡單。
Array.prototype.flat ⭐️⭐️⭐️⭐️⭐️
reduce
與 concat
簡直是絕配
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
不說了,要實現真正的符合規範的 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
}
該題目看起來簡單,實際做起來有許多邊界問題需要注意,如
- 回撥函式中第一個 Index 是多少?
- 陣列為稀疏陣列如何處理?
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);
注意:
- 注意深層巢狀資料
- 注意
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.map
- 程式碼: Promise.map
用以 Promise 併發控制,面試中經常會有問到,在工作中也經常會有涉及。在上手這道問題之前,瞭解 [Promise.all]() 的實現將對實現併發控制有很多的幫助。
另外,最受歡迎的 Promise 庫 bluebird 對 Promise.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
}
對以下數字進行編碼壓縮 ⭐️⭐️⭐️⭐️⭐️
- 題目: 【Q412】對以下字串進行壓縮編碼
- 程式碼: 【Q412】對以下字串進行壓縮編碼
這是一道大廠常考的程式碼題
- 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"
但是面試官往往會繼續深入
- 如果只出現一次,不編碼數字,如
aaab -> a3b
- 如果只出現兩次,不進行編碼,如
aabbb -> aab3
- 如果進行解碼,碰到數字如何處理?
以下是除數字外的進一步編碼
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
基於兩個原理:
- 動態建立
script
,使用script.src
載入請求跨過跨域 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 以內的菲波那切數列
- 題目: 輸出 100 以內的菲波那切數列
TopK 問題
典型的二叉堆問題
- 取陣列中前 k 個數做小頂堆,堆化
- 陣列中的其它數逐一與堆頂元素比較,若大於堆頂元素,則插入該數
時間複雜度 O(nlg(k))
求正序增長的正整數陣列中,其和為 N 的兩個數
求給定陣列中 N 個數相加之和為 sum 所有可能集合
求給定陣列中 N 個數相加之和為 sum 所有可能集合,請補充以下程式碼
function fn(arr, n, sum) {}
如何判斷兩個連結串列是否相交
經典問題
最終
程式碼程式設計題在面試過程中出現頻率很高,且能很大程度考察候選人的程式碼能力與程式碼風格。