前言
常见的白板手写题,常见的排序算法以及 js 方法的实现。
🫧 手写冒泡排序 (默认从小到大)
冒泡排序属于稳定的排序,空间复杂度O(n), 时间复杂度O(n^2)。稳定的排序指的是不会改变元素的相对位置
js
/**
* 冒泡排序
* @param {number[]} arr
*/
function bubbleSort (arr) {
for (let i = 0; i < arr.length; i += 1) {
for (let j = i + 1; j < arr.length; j += 1) {
if (arr[i] > arr[j]) {
[arr[i], arr[j]] = [arr[j], arr[i]]
}
}
}
return arr
}
// [0, 1, 2, 3, 4, 5, 6, 7]
console.log(bubbleSort([1, 4, 3, 0, 2, 5, 7, 6]))
手写归并排序 (默认从小到大)
归并排序属于稳定的排序,时间复杂度是 O(nlogn)。
js
// 合并数组
function merge (left, right) {
const arr = []
while (left.length && right.length) {
if (left[0] > right[0]) {
arr.push(right.shift())
} else {
arr.push(left.shift())
}
}
const max = left.length ? left : right
return [...arr, ...max]
}
/**
* 归并排序
* @param {number[]} arr
*/
function mergeSort (arr) {
if (arr.length <= 1) {
return arr
}
const midd = Math.floor(arr.length / 2)
const left = arr.splice(0, midd)
const right = arr
return merge(mergeSort(left), mergeSort(right))
}
// [-3, 0, 0, 1, 1, 2, 2, 3, 4, 4, 100, 101]
console.log(mergeSort([4, 1, 2, 3, 0, 100, -3, 2, 1, 101, 4, 0]))
手写插入排序 (默认从小到大)
插入排序是稳定的排序,时间复杂度是O(n^2)
js
/**
* 插入排序
* @param {number[]} arr
*/
function insertionSort(arr) {
for (let i = 0; i < arr.length; i += 1) {
for (let j = i + 1; j < arr.length; j += 1) {
if (arr[i] > arr[j]) {
const insertionItem = arr.splice(j, 1)[0]
arr.splice(i, 0, insertionItem)
}
}
}
return arr
}
// [-3, -2, 0, 0, 1, 2, 4, 4, 4, 100, 101, 300]
console.log(insertionSort([4, 300, -2, 4, 0, 100, -3, 2, 1, 101, 4, 0]))
手写快速排序 (默认从小到大)
快速排序是不稳定的排序,最坏的情况时间复杂度是O(n^2)。平均的时间复杂度是O(nlogn)
js
/**
* 快速排序
* @param {number[]} arr
*/
function quickSort(arr) {
if (arr.length <= 1) {
return arr
}
const accurateValue = arr[0]
const max = []
const min = []
for (let i = 1; i < arr.length; i += 1) {
if (arr[i] >= accurateValue) {
max.push(arr[i])
} else {
min.push(arr[i])
}
}
return [...quickSort(min), accurateValue, ...quickSort(max)]
}
// [-7, -6, -1, 1, 1, 7, 12, 30, 30, 30, 41, 101]
console.log(quickSort([-1, 30, -7, 1, 30, 101, 30, 12, 41, 1, -6, 7]))
🍓 手写选择排序 (默认从小到大)
选择排序是不稳定排序,时间复杂度是O(n^2)
js
/**
* 选址排序
* @param {number[]} arr
*/
function selectionSort(arr) {
for (let i = 0; i < arr.length; i += 1) {
let minIndex = i
for (let j = i + 1; j < arr.length; j += 1) {
if (arr[j] < arr[minIndex]) {
minIndex = j
}
}
const min = arr.splice(minIndex, 1)[0]
arr.splice(i, 0, min)
}
return arr
}
// [-101, -30, -26, -3, 0, 2, 2, 50, 60, 60, 97, 100]
console.log(selectionSort([100, 60, -3, 2, -30, -101, 50, 60, 2, 0, -26, 97]))
手写 throttle 节流
js
/**
* @param {Function} func
* @param {number} wait
*/
function throttle (func, wait) {
let timer = null
let lastArgs = null
return function (...param) {
if (!timer) {
func.call(this, ...param)
timer = setTimeout(() => {
if (lastArgs) {
func.call(this, ...lastArgs)
}
lastArgs = null
timer = null
}, wait)
} else {
lastArgs = [...param]
}
}
}
手写 debounce 防抖
js
/**
* @param {Function} func
* @param {number} wait
*/
function debounce(func, wait) {
let timer = null
return function (...params) {
function clear () {
clearTimeout(timer)
timer = null
}
function run () {
timer = window.setTimeout(() => {
func.call(this, ...param)
clear()
}, wait)
}
if (!timer) {
run()
} else {
clear()
run()
}
}
}
手现 Array.prototype.flat() 数组扁平化
js
/**
* @param { Array } arr
* @param { number } depth
* @returns { Array }
*/
function flat(arr, depth = 1) {
let result = []
if (depth > 0) {
let isHaveArray = false
for (let i = 0; i < arr.length; i += 1) {
const item = arr[i]
if (Array.isArray(item)) {
isHaveArray = true
result.push(...item)
} else {
result.push(item)
}
}
if (isHaveArray) {
return flat(result, depth - 1)
} else {
return result
}
} else {
result = arr
return result
}
}
// [1, 2, 3, 4, 5, 6, 7, 8]
console.log(flat([1,2,[3,4],[5,6,[7,8]]], 2))
手写数组去重
js
/**
* @param {any[]} arr
*/
function deduplication(arr) {
return [...new Set(arr)]
}
手写洗牌算法
js
/**
* @param {any[]} arr
*/
function shuffle(arr) {
for (let i = 0; i < arr.length; i += 1) {
const array = new Uint32Array(1);
const j = Math.floor((crypto.getRandomValues(array)[0] / 0xffffffff) * arr.length);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// [3, 2, 5, 1, 0, 6, 9, 7, 4, 8]
console.log(shuffle([0,1,2,3,4,5,6,7,8,9]))
🎁 手写二分查找
js
/**
* @param {number[]} arr - ascending unique array
* @param {number} target
* @return {number}
*/
function binarySearch(arr, target){
let start = 0
let end = arr.length - 1
while (start <= end) {
let midd = Math.floor((start + end) / 2)
if (arr[midd] === target) {
return midd
}
if (arr[midd] < target) {
start = midd + 1
} else {
end = midd - 1
}
}
return -1
}
// 0
console.log(binarySearch([1,2,3], 1))
手写 memo
类似 React 中的 memo
js
/**
* @param {Function} func
* @param {(args:[]) => string } [resolver] - cache key generator
*/
function memo(func, resolver) {
const map = new Map()
return function (...params) {
let key
if (typeof resolver === function) {
key = resolver(...params)
} else {
key = [...params].join('-')
}
if (map.has(key)) {
return map.get(key)
} else {
const val = func.apply(this, [...params])
map.set(key, val)
return val
}
}
}
手写 Promise.all
Promise.all, 当所有的 Promise 都成功时,返回的 Promise.all 也会返回成功,并返回一个包含所有值的数组。如果输入的任何 Promise 被拒绝,则返回的 Promise 将被拒绝,并带有第一个被拒绝的原因。
js
function all (promises) {
return new Promise((resolve, reject) => {
if (!promises.length) {
return resolve([])
}
const result = []
let total = 0
for (let i = 0; i < promises.length; i += 1) {
const promise = promises[i] instanceof Promise ? promises[i] : Promise.resolve(promises[i])
promise.then((res) => {
result[i] = res
total += 1
if (total === promises.length) {
resolve(result)
}
}).catch((err) => {
reject(err)
})
}
})
}
手现 Promise.allSettled
Promise.allSettled, 无论所有的 Promise 都成功还是失败,都会将结果返回
js
/**
* @param {Array<any>} promises - notice that input might contains non-promises
* @return {Promise<Array<{status: 'fulfilled', value: any} | {status: 'rejected', reason: any}>>}
*/
function allSettled(promises) {
return new Promise((resolve, reject) => {
if (!promises.length) {
return resolve([])
}
const result = []
let total = 0
for (let i = 0; i < promises.length; i += 1) {
const promise = promises[i] instanceof Promise ? promises[i] : Promise.resolve(promises[i])
promise.then((res) => {
result[i] = {
status:"fulfilled",
value: res
}
total += 1
if (total === promises.length) {
resolve(result)
}
}).catch((err) => {
reject(err)
result[i] = {
status:"rejected",
reason: err
}
total += 1
if (total === promises.length) {
resolve(result)
}
})
}
})
}
手现 Promise.any
Promise.any, 如果有一个成功会立即返回,如果全部失败会一起返回所有失败的结果
js
/**
* @param {Array<Promise>} promises
* @return {Promise}
*/
function any(promises) {
return new Promise((resolve, reject) => {
if (!promises.length) {
return reject(new AggregateError('No Promise in Promise.any was resolved', []))
}
let total = 0
const errors = []
for (let i = 0; i < promises.length; i += 1) {
const promise = promises[i] instanceof Promise ? promises[i] : Promise.resolve(promises[i])
promise.then((res) => {
resolve(res)
}).catch((err) => {
errors[i] = err
total += 1
if (count === promises.length) {
reject(new AggregateError('No Promise in Promise.any was resolved', errors))
}
})
}
})
}
手现 Promise.race
Promise.race, 会返回一组 promise 之中,如果有一个成功或者失败就会立即返回
js
/**
* @param {Array<Promise>} promises
* @return {Promise}
*/
function race(promises) {
return new Promise((resolve, reject) => {
if (promises.length) {
for (let i = 0; i < promises.length; i += 1) {
const promise = promises[i] instanceof Promise ? promises[i] : Promise.resolve(promises[i])
promise.then((res) => {
resolve(res)
}).catch((err) => {
reject(err)
})
}
}
})
}
手写 ES5 继承
js
// ----------------- 👨 父类的构造函数,以及方法 -----------------
function Father (name) {
this.name = name
}
// 父类方法
Father.prototype.sayname = function () {
console.log(this.name)
}
// ----------------- 👦 子类的构造函数,以及方法 -----------------
function Son (name, age) {
// 调用父类的构造函数
Father.call(this, name)
this.age = age
}
Son.prototype = Object.create(Father.prototype)
// 修正原型链
Son.prototype.constructor = Son
// 子类方法
Son.prototype.sayAge = function() {
console.log(this.age)
}
🎊 手写 new
js
/**
* @param {Function} constructor
* @param {any[]} args - argument passed to the constructor
* `myNew(constructor, ...args)` should return the same as `new constructor(...args)`
*/
const myNew = (constructor, ...args) => {
const obj = Object.create({})
// 调用构造函数
const newObj = constructor.call(obj, ...args)
// 修改obj的___proto___, 指向构造函数的prototype
Object.setPrototypeOf(obj, constructor.prototype)
return newObj || obj
}
手写 call
js
Function.prototype.myCall = function (thisArg, ...args) {
if (thisArg === undefined || thisArg === null) {
thisArg = window
}
if (typeof thisArg === 'string') {
thisArg = new String(thisArg)
}
if (typeof thisArg === 'number') {
thisArg = new Number(thisArg)
}
if (typeof thisArg === 'boolean') {
thisArg = new Boolean(thisArg)
}
const key = Symbol()
// this 是调用 call 的函数
thisArg[key] = this
const result = thisArg[key](...args)
delete thisArg[key]
return result
}
手写 apply
js
Function.prototype.myApply = function (thisArg, argsArray = []) {
// 如果第二参数不是对象抛出错误
if (typeof argsArray !== 'object') {
throw new Error('Uncaught TypeError: CreateListFromArrayLike called on non-object')
}
if (thisArg === undefined || thisArg === null) {
thisArg = window
}
if (typeof thisArg === 'string') {
thisArg = new String(thisArg)
}
if (typeof thisArg === 'number') {
thisArg = new Number(thisArg)
}
if (typeof thisArg === 'boolean') {
thisArg = new Boolean(thisArg)
}
const key = Symbol()
thisArg[key] = this
const result = thisArg[key](...args)
delete thisArg[key]
return result
}
手写 bind
js
Function.prototype.myBind = function (thisArg, ...initArgs) {
if (thisArg === undefined || thisArg === null) {
thisArg = window
}
if (typeof thisArg === 'string') {
thisArg = new String(thisArg)
}
if (typeof thisArg === 'number') {
thisArg = new Number(thisArg)
}
if (typeof thisArg === 'boolean') {
thisArg = new Boolean(thisArg)
}
// 调用 bind 的函数
const that = this
return function (...args) {
const key = Symbol()
thisArg[key] = that
const result = thisArg[key](...initArgs, ...args)
delete thisArg[key]
return result
}
}
手写 instanceof
A instanceof B, 自下往上查找A的原型(A.proto)是否等于B.prototype, 直到向上查找到null
js
/**
* @param {any} obj
* @param {target} constructor
* @return {boolean}
*/
function myInstanceOf(obj, constructor) {
if (typeof obj !== 'object' || obj === null) {
return false
}
// 或者使用 Object.getPrototypeOf(obj) 获取 __proto__
const proto = obj.__proto__
if (proto === null) {
return false
}
if (proto === constructor?.prototype) {
return true
} else {
return myInstanceOf(proto, constructor)
}
}
手写 Object.create
js
/**
* @param {any} proto
* @return {object}
*/
function myObjectCreate(proto) {
if (!(proto instanceof Object)) {
throw new Error('Uncaught TypeError: Object prototype may only be an Object or null')
}
const obj = {}
// Object.setPrototypeOf(obj, proto)
obj.__proto__ = proto
return obj
}
🚀 手写 Object.is
Object.is 不会转换两个值的类型,和 === 相似,但是他们之间也存在一些区别,区别如下:
NaN 使用 === 是不相等的,使用 Object.is 返回 true
+0 和 -0 使用 === 是相等的,使用 Object.is 返回 false
js
/**
* @param {any} a
* @param {any} b
* @return {boolean}
*/
function is(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
if (isNaN(a) && isNaN(b)) {
return true
}
if (a === 0 && b === 0) {
// Infinity === -Infinity false
return 1 / a === 1 / b
}
}
return a === b
}
手写 Array.prototype.reduce
- Array.prototype.reduce(callbackFn, initialValue)
- callbackFn 参数:
*- accumulator 上一次调用 callbackFn 的结果。在第一次调用时,如果指定了 initialValue 则为指定的值,否则为 array[0] 的值
-
- currentValue 当前元素的值。在第一次调用时,如果指定了 initialValue,则为 array[0] 的值,否则为 array[1]
-
- currentIndex currentValue 在数组中的索引位置。在第一次调用时,如果指定了 initialValue 则为 0,否则为 1
-
- array 调用了 reduce() 的数组本身
- initialValue 参数:
- 第一次调用回调时初始化 accumulator 的值。
- 如果指定了 initialValue,则 callbackFn 从数组中的第一个值作为 currentValue 开始执行。
- 如果没有指定 initialValue,则 accumulator 初始化为数组中的第一个值,并且 callbackFn 从数组中的第二个值作为 currentValue 开始执行。在这种情况下,如果数组为空(没有第一个值可以作为 accumulator 返回),则会抛出错误
- callbackFn 参数:
js
Array.prototype.myReduce = function (callback, initialValue) {
// myReduce 参数的长度,判断是否传入了 initialValue
const argsLength = arguments.length
// this.length 是调用 myReduce 方法的数组的长度
// 如果 myReduce 参数为 1 个,并且数组长度为 0,抛出错误
if (argsLength === 1 && this.length === 0) {
throw new Error('Uncaught TypeError: Reduce of empty array with no initial value')
}
// 判断是否指定了 initialValue
let index = argsLength === 1 ? 1 : 0
let resultValue = argsLength === 1 ? this[0] : initialValue
for (let i = index; i < this.length; i += 1) {
resultValue = callback(resultValue, this[i], i, this)
}
return resultValue
}
手写 Array.prototype.map
- map(callbackFn, thisArg)
- callbackFn 参数:
- element 数组中当前正在处理的元素
- index 正在处理的元素在数组中的索引
- array 调用了 map() 的数组本身
- thisArg 参数:
- 执行 callbackFn 时用作 this 的值
- callbackFn 参数:
js
Array.prototype.myMap = function(callback, thisArg) {
const result = []
const that = thisArg || this
this.forEach(function (item, i) {
result[i] = callback.call(that, item, i, that)
})
return result
}
手写 ajax
js
const xhr = new XMLHttpRequest()
xhr.open("GET", url, false)
xhr.onreadystatechange = function () {
// 接收到返回的处理
if (xhr.status === 200) {
// ...
}
}
xhr.send()
🍉 手写 once
js
/**
* @param {Function} func
* @return {Function}
*/
function once(func) {
let result = undefined
let isActive = false
return function (...params) {
if (!isActive) {
result = func.call(this, ...params)
isActive = true
}
return result
}
}
手写简单的双向绑定
js
// element 的 value 与 state 的 value 相互绑定
/**
* @param {{value: string}} state
* @param {HTMLInputElement} element
*/
function model(state, element) {
element.value = state.value
Object.defineProperty(state, 'value', {
get: () => element.value,
set: (value) => element.value = value,
})
}
手写 promisify
js
// const callback = (error, data) => {
// if (error) {
// // 出错的时候
// } else {
// // 成功的时候
// }
// }
// const func = (arg1, arg2, callback) => {
// // 一些异步逻辑
// if (hasError) {
// callback(someError)
// } else {
// callback(null, someData)
// }
// }
/**
* @param {(...args) => void} func
* @returns {(...args) => Promise<any}
*/
function promisify(func) {
return function (...params) {
const that = this
return new Promise(function(resolve, reject) {
func.apply(that, [...params], function(error, data) {
if (error) {
reject(error)
} else {
resolve(data)
}
})
})
}
}
手写 trim
js
/**
* @param {string} str
* @return {string}
*/
function trim(str) {
return str.replace(/^\s+|\s+$/g, '')
}
🏆 手写 Promise 节流
假设你需要调用100个API获取数据,如果使用Promise.all(),100个请求会同时到达你的服务器,如果你的服务器性能很低的话,这就会是个负担。节流API请求,使得任何时刻最多只有5个请求正在进行中。
js
/**
* @param {() => Promise<any>} func
* @param {number} max
* @return {Promise}
*/
function throttlePromises(funcs, max){
return new Promise((resolve, reject) => {
const result = []
const len = funcs.length
// 当前正在执行的任务数
let jobs = 0
// 当前已完成的任务数
let count = 0
// 处理请求列表
const handleJobs = () => {
while (jobs < max && funcs.length) {
let promise = funcs.shift()()
// 当前任务的索引
let index = len - funcs.length - 1
promise = promise instanceof Promise ? promise : Promise.resolve(promise)
promise.then((res) => {
result[index] = res
count += 1
jobs -= 1
if (count === len) {
resolve(result)
}
// 当任务执行完成后,主动判断最大队列是否还有空闲
handleJobs()
}).catch(() => {
reject(err)
})
jobs += 1
}
}
handleJobs()
})
}
手写 Event Emitter
- 需求:
- 支持订阅功能
const sub1 = emitter.subscribe('event1', callback1)
, 并且同一个 callback 可以重复订阅同一个事件
- 支持取消订阅
sub1.release()
- 支持触发订阅
emitter.emit('event1', 1, 2)
- 支持订阅功能
js
class EventEmitter {
constructor() {
this.map = {}
}
subscribe(eventName, callback) {
const sub = {
eventName,
callback,
};
const that = this
if ((this.map[eventName]) {
this.map[eventName].push(sub)
} else {
this.map[[eventName]] = [sub]
}
return {
release () {
this.map = {
...this.map,
[eventName]: that.map[eventName].filter((s) => s !== sub)
}
}
}
}
emit(eventName, ...args) {
if (this.map[eventName]) {
this.map[eventName].forEach((sub) => {
sub.callback(...args)
})
}
}
}
手写 Math.sqrt
Math.sqrt 可以用来取得平方根
使用二分查找法
js
/**
* @param {any} x
* @return {number}
*/
function mySqrt(x) {
if (isNaN(x)) {
return NaN
}
if (typeof x !== 'number') {
return NaN
}
if (x < 0) {
return NaN
}
if (x === 0) {
return 0
}
if (x === 1) {
return 1
}
let result
const _Sqrt = (start, end) => {
if (start <= end) {
const mid = Math.floor((end + start) / 2)
if (mid ** 2 < x) {
result = mid
sqrt(mid + 1, end)
} else if (mid ** 2 > x) {
sqrt(start, mid - 1)
} else {
result = mid
}
}
}
_Sqrt(0, x)
return result
}
手写 Promise.prototype.finally
Promise.prototype.finally() 无论 promise 是成功还是失败都会执行一个callback
js
/**
* @param {Promise<any>} promise
* @param {() => void} onFinally
* @returns {Promise<any>}
*/
function myFinally(promise, onFinally) {
return promise.then(res => {
return Promise.resolve(onFinally()).then(() => {
return res
})
}).catch(error => {
return Promise.resolve(onFinally()).then(() => {
throw err
})
})
}
手写 chunk
js
// chunk([1,2,3,4,5], 1)
// [[1], [2], [3], [4], [5]]
// chunk([1,2,3,4,5], 3)
// [[1, 2, 3], [4, 5]]
/**
* @param {any[]} items
* @param {number} size
* @returns {any[][]}
*/
function chunk(items, size) {
if (size === 0 || items?.length === 0) {
return []
}
const result = []
while (items?.length) {
result.push(items.splice(0, size))
}
return result
}
🍰 手现 curry
js
// const join = (a, b, c) => {
// return `${a}_${b}_${c}`
// }
// const curriedJoin = curry(join)
// '1_2_3'
// curriedJoin(1, 2, 3)
// '1_2_3'
// curriedJoin(1)(2, 3)
// '1_2_3'
// curriedJoin(1, 2)(3)
function curry(func, ...initArgs) {
return function (...restArgs) {
return ((params) => {
return params.length >= func.length ? func.call(this, ...params) : curry(func, ...restArgs)
})([...initArgs, ...restArgs])
}
}
手写 LRU 缓存
利用 js Map 特性实现
js
class LRU {
constructor(max) {
this.map = new Map()
this.max = max
}
get(key) {
if (this.map.has(key)) {
let value = this.map.get(key)
// 先删除,再设置, 相当于更新到map的最后一位(最新的一位)
this.map.delete(key)
this.map.set(key, value)
return value
} else {
return -1
}
}
put(key, value) {
// 如果已经有了这个key,先删除,再设置, 相当于更新到map的最后一位(最新的一位)
if (this.map.has(key)){
this.map.delete(key)
}
this.map.set(key, value)
// 判断是否超过 LRU 的最大限制
if (this.map.size > this.max) {
// 删除map的第一位(最老的一位)
const lastKey = this.map.keys().next().value
this.map.delete(lastKey)
}
}
}
手写 cloneDeep 深拷贝
如果需要写出一个完善的深拷贝,需要考虑到的点还是很多的
js
// hash 用来规避循环
const hash = new WeakMap()
function isObject(value) {
return value !== null && (typeof value === "object" || typeof value === "function")
}
function getSymbolKeys(value) {
let keys = Object.getOwnPropertySymbols(value)
keys = keys.filter((key) => value.propertyIsEnumerable(key))
return keys
}
function getAllKeys(value) {
let keys = Object.keys(value)
keys = [...keys, ...getSymbolKeys(value)]
return keys
}
function cloneDeep(data) {
let result = null
if (!isObject(data)) {
return data
}
const isArray = Array.isArray(data)
if (isArray) {
result = []
} else {
result = Object.create(Object.getPrototypeOf(data))
}
// 规避循环引用
if (hash.has(data)) {
return hash.get(data)
} else {
hash.set(data, result)
}
const keys = getAllKeys(data)
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i]
const val = data[key]
result[key] = cloneDeep(val)
}
return result
}