前言
寫個快排吧
、能手寫一個Promise嗎?
、來一個深拷貝
...相信大家已經不止一次在面試或者日常業務中遇到這樣的題目了,每當現場寫程式碼時感覺似曾相識,但就是寫不出來,期望的offer也離我們遠去o(╥﹏╥)o。來,兄弟們捲起來,日計不足,歲計有餘
,我們們每天學一個,看那些面試官還怎麼難倒我們!!!哼哼哼
1. 實現instanceOf的3種方式
instanceof
運算子用於檢測建構函式的prototype
屬性是否出現在某個例項物件的原型鏈上。MDN上
關鍵點: 建構函式Fn的prototype
,例項物件的原型鏈。
所以只要遍歷例項物件的原型鏈,挨個往上查詢看是否有與Fn的prototype
相等的原型,直到最頂層Object
還找不到,那麼就返回false。
遞迴實現(方式1)
/**
*
* @param {*} obj 例項物件
* @param {*} func 建構函式
* @returns true false
*/
const instanceOf1 = (obj, func) => {
if (obj === null || typeof obj !== 'object') {
return false
}
let proto = Object.getPrototypeOf(obj)
if (proto === func.prototype) {
return true
} else if (proto === null) {
return false
} else {
return instanceOf1(proto, func)
}
}
// 測試
let Fn = function () { }
let p1 = new Fn()
console.log(instanceOf1({}, Object)) // true
console.log(instanceOf1(p1, Fn)) // true
console.log(instanceOf1({}, Fn)) // false
console.log(instanceOf1(null, Fn)) // false
console.log(instanceOf1(1, Fn)) // false
遍歷實現(方式2)
/**
*
* @param {*} obj 例項物件
* @param {*} func 建構函式
* @returns true false
*/
const instanceOf2 = (obj, func) => {
if (obj === null || typeof obj !== 'object') {
return false
}
let proto = obj
while (proto = Object.getPrototypeOf(proto)) {
if (proto === null) {
return false
} else if (proto === func.prototype) {
return true
}
}
return false
}
// 測試
let Fn = function () { }
let p1 = new Fn()
console.log(instanceOf2({}, Object)) // true
console.log(instanceOf2(p1, Fn)) // true
console.log(instanceOf2({}, Fn)) // false
console.log(instanceOf2(null, Fn)) // false
console.log(instanceOf2(1, Fn)) // false
遍歷實現(方式3)
/**
*
* @param {*} obj 例項物件
* @param {*} func 建構函式
* @returns true false
*/
const instanceOf3 = (obj, func) => {
if (obj === null || typeof obj !== 'object') {
return false
}
let proto = obj
// 因為一定會有結束的時候(最頂層Object),所以不會是死迴圈
while (true) {
if (proto === null) {
return false
} else if (proto === func.prototype) {
return true
} else {
proto = Object.getPrototypeOf(proto)
}
}
}
// 測試
let Fn = function () { }
let p1 = new Fn()
console.log(instanceOf3({}, Object)) // true
console.log(instanceOf3(p1, Fn)) // true
console.log(instanceOf3({}, Fn)) // false
console.log(instanceOf3(null, Fn)) // false
console.log(instanceOf3(1, Fn)) // false
2. 實現JSON.stringify(超詳細)
看程式碼實現前,可以先看看前幾天寫的一篇悲傷的故事就因為JSON.stringify,我的年終獎差點打水漂了
JSON.stringify()
方法將一個 JavaScript 物件或值轉換為 JSON 字串,如果指定了一個 replacer 函式,則可以選擇性地替換值,或者指定的 replacer 是陣列,則可選擇性地僅包含陣列指定的屬性。MDN
JSON.stringify
本身有非常多的轉換規則和特性(詳情請檢視MDN),要完整實現還是挺麻煩的(為了實現這個函式胖頭魚
快不會動了o(╥﹏╥)o)
const jsonstringify = (data) => {
// 確認一個物件是否存在迴圈引用
const isCyclic = (obj) => {
// 使用Set資料型別來儲存已經檢測過的物件
let stackSet = new Set()
let detected = false
const detect = (obj) => {
// 不是物件型別的話,可以直接跳過
if (obj && typeof obj != 'object') {
return
}
// 當要檢查的物件已經存在於stackSet中時,表示存在迴圈引用
if (stackSet.has(obj)) {
return detected = true
}
// 將當前obj存如stackSet
stackSet.add(obj)
for (let key in obj) {
// 對obj下的屬性進行挨個檢測
if (obj.hasOwnProperty(key)) {
detect(obj[key])
}
}
// 平級檢測完成之後,將當前物件刪除,防止誤判
/*
例如:物件的屬性指向同一引用,如果不刪除的話,會被認為是迴圈引用
let tempObj = {
name: '前端胖頭魚'
}
let obj4 = {
obj1: tempObj,
obj2: tempObj
}
*/
stackSet.delete(obj)
}
detect(obj)
return detected
}
// 特性七:
// 對包含迴圈引用的物件(物件之間相互引用,形成無限迴圈)執行此方法,會丟擲錯誤。
if (isCyclic(data)) {
throw new TypeError('Converting circular structure to JSON')
}
// 特性九:
// 當嘗試去轉換 BigInt 型別的值會丟擲錯誤
if (typeof data === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt')
}
const type = typeof data
const commonKeys1 = ['undefined', 'function', 'symbol']
const getType = (s) => {
return Object.prototype.toString.call(s).replace(/\[object (.*?)\]/, '$1').toLowerCase()
}
// 非物件
if (type !== 'object' || data === null) {
let result = data
// 特性四:
// NaN 和 Infinity 格式的數值及 null 都會被當做 null。
if ([NaN, Infinity, null].includes(data)) {
result = 'null'
// 特性一:
// `undefined`、`任意的函式`以及`symbol值`被`單獨轉換`時,會返回 undefined
} else if (commonKeys1.includes(type)) {
// 直接得到undefined,並不是一個字串'undefined'
return undefined
} else if (type === 'string') {
result = '"' + data + '"'
}
return String(result)
} else if (type === 'object') {
// 特性五:
// 轉換值如果有 toJSON() 方法,該方法定義什麼值將被序列化
// 特性六:
// Date 日期呼叫了 toJSON() 將其轉換為了 string 字串(同Date.toISOString()),因此會被當做字串處理。
if (typeof data.toJSON === 'function') {
return jsonstringify(data.toJSON())
} else if (Array.isArray(data)) {
let result = data.map((it) => {
// 特性一:
// `undefined`、`任意的函式`以及`symbol值`出現在`陣列`中時會被轉換成 `null`
return commonKeys1.includes(typeof it) ? 'null' : jsonstringify(it)
})
return `[${result}]`.replace(/'/g, '"')
} else {
// 特性二:
// 布林值、數字、字串的包裝物件在序列化過程中會自動轉換成對應的原始值。
if (['boolean', 'number'].includes(getType(data))) {
return String(data)
} else if (getType(data) === 'string') {
return '"' + data + '"'
} else {
let result = []
// 特性八
// 其他型別的物件,包括 Map/Set/WeakMap/WeakSet,僅會序列化可列舉的屬性
Object.keys(data).forEach((key) => {
// 特性三:
// 所有以symbol為屬性鍵的屬性都會被完全忽略掉,即便 replacer 引數中強制指定包含了它們。
if (typeof key !== 'symbol') {
const value = data[key]
// 特性一
// `undefined`、`任意的函式`以及`symbol值`,出現在`非陣列物件`的屬性值中時在序列化過程中會被忽略
if (!commonKeys1.includes(typeof value)) {
result.push(`"${key}":${jsonstringify(value)}`)
}
}
})
return `{${result}}`.replace(/'/, '"')
}
}
}
}
// 各種測試
// 1. 測試一下基本輸出
console.log(jsonstringify(undefined)) // undefined
console.log(jsonstringify(() => { })) // undefined
console.log(jsonstringify(Symbol('前端胖頭魚'))) // undefined
console.log(jsonstringify((NaN))) // null
console.log(jsonstringify((Infinity))) // null
console.log(jsonstringify((null))) // null
console.log(jsonstringify({
name: '前端胖頭魚',
toJSON() {
return {
name: '前端胖頭魚2',
sex: 'boy'
}
}
}))
// {"name":"前端胖頭魚2","sex":"boy"}
// 2. 和原生的JSON.stringify轉換進行比較
console.log(jsonstringify(null) === JSON.stringify(null));
// true
console.log(jsonstringify(undefined) === JSON.stringify(undefined));
// true
console.log(jsonstringify(false) === JSON.stringify(false));
// true
console.log(jsonstringify(NaN) === JSON.stringify(NaN));
// true
console.log(jsonstringify(Infinity) === JSON.stringify(Infinity));
// true
let str = "前端胖頭魚";
console.log(jsonstringify(str) === JSON.stringify(str));
// true
let reg = new RegExp("\w");
console.log(jsonstringify(reg) === JSON.stringify(reg));
// true
let date = new Date();
console.log(jsonstringify(date) === JSON.stringify(date));
// true
let sym = Symbol('前端胖頭魚');
console.log(jsonstringify(sym) === JSON.stringify(sym));
// true
let array = [1, 2, 3];
console.log(jsonstringify(array) === JSON.stringify(array));
// true
let obj = {
name: '前端胖頭魚',
age: 18,
attr: ['coding', 123],
date: new Date(),
uni: Symbol(2),
sayHi: function () {
console.log("hello world")
},
info: {
age: 16,
intro: {
money: undefined,
job: null
}
},
pakingObj: {
boolean: new Boolean(false),
string: new String('前端胖頭魚'),
number: new Number(1),
}
}
console.log(jsonstringify(obj) === JSON.stringify(obj))
// true
console.log((jsonstringify(obj)))
// {"name":"前端胖頭魚","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖頭魚","number":1}}
console.log(JSON.stringify(obj))
// {"name":"前端胖頭魚","age":18,"attr":["coding",123],"date":"2021-10-06T14:59:58.306Z","info":{"age":16,"intro":{"job":null}},"pakingObj":{"boolean":false,"string":"前端胖頭魚","number":1}}
// 3. 測試可遍歷物件
let enumerableObj = {}
Object.defineProperties(enumerableObj, {
name: {
value: '前端胖頭魚',
enumerable: true
},
sex: {
value: 'boy',
enumerable: false
},
})
console.log(jsonstringify(enumerableObj))
// {"name":"前端胖頭魚"}
// 4. 測試迴圈引用和Bigint
let obj1 = { a: 'aa' }
let obj2 = { name: '前端胖頭魚', a: obj1, b: obj1 }
obj2.obj = obj2
console.log(jsonstringify(obj2))
// TypeError: Converting circular structure to JSON
console.log(jsonStringify(BigInt(1)))
// TypeError: Do not know how to serialize a BigInt
3. 實現一個Promise
篇幅原因,這裡就不介紹Promise A+規範以及then
函式之外的其他詳細實現了,下面這個版本我一般在面試中常用,基本直接通過。
class MyPromise {
constructor (exe) {
// 最後的值,Promise .then或者.catch接收的值
this.value = undefined
// 狀態:三種狀態 pending success failure
this.status = 'pending'
// 成功的函式佇列
this.successQueue = []
// 失敗的函式佇列
this.failureQueue = []
const resolve = () => {
const doResolve = (value) => {
// 將快取的函式佇列挨個執行,並且將狀態和值設定好
if (this.status === 'pending') {
this.status = 'success'
this.value = value
while (this.successQueue.length) {
const cb = this.successQueue.shift()
cb && cb(this.value)
}
}
}
setTimeout(doResolve, 0)
}
const reject = () => {
// 基本同resolve
const doReject = (value) => {
if (this.status === 'pending') {
this.status = 'failure'
this.value = value
while (this.failureQueue.length) {
const cb = this.failureQueue.shift()
cb && cb(this.value)
}
}
}
setTimeout(doReject, 0)
}
exe(resolve, reject)
}
then (success = (value) => value, failure = (value) => value) {
// .then返回的是一個新的Promise
return new MyPromise((resolve, reject) => {
// 包裝回到函式
const successFn = (value) => {
try {
const result = success(value)
// 如果結果值是一個Promise,那麼需要將這個Promise的值繼續往下傳遞,否則直接resolve即可
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 基本筒成功回撥函式的封裝
const failureFn = (value) => {
try {
const result = failure(value)
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
} catch (err) {
reject(err)
}
}
// 如果Promise的狀態還未結束,則將成功和失敗的函式快取到佇列裡
if (this.status === 'pending') {
this.successQueue.push(successFn)
this.failureQueue.push(failureFn)
// 如果已經成功結束,直接執行成功回撥
} else if (this.status === 'success') {
success(this.value)
} else {
// 如果已經失敗,直接執行失敗回撥
failure(this.value)
}
})
}
// 其他函式就不一一實現了
catch () {
}
}
// 以下舉個例子,驗證一下以上實現的結果
const pro = new MyPromise((resolve, reject) => {
setTimeout(resolve, 1000)
setTimeout(reject, 2000)
})
pro
.then(() => {
console.log('2_1')
const newPro = new MyPromise((resolve, reject) => {
console.log('2_2')
setTimeout(reject, 2000)
})
console.log('2_3')
return newPro
})
.then(
() => {
console.log('2_4')
},
() => {
console.log('2_5')
}
)
pro
.then(
data => {
console.log('3_1')
throw new Error()
},
data => {
console.log('3_2')
}
)
.then(
() => {
console.log('3_3')
},
e => {
console.log('3_4')
}
)
// 2_1
// 2_2
// 2_3
// 3_1
// 3_4
// 2_5
4. 實現多維陣列扁平化的3種方式
業務和麵試中都經常會遇到,將多維陣列扁平化是必備的技能
遞迴實現(方式1)
/**
*
* @param {*} array 深層巢狀的資料
* @returns array 新陣列
*/
const flat1 = (array) => {
return array.reduce((result, it) => {
return result.concat(Array.isArray(it) ? flat1(it) : it)
}, [])
}
// 測試
let arr1 = [
1,
[ 2, 3, 4 ],
[ 5, [ 6, [ 7, [ 8 ] ] ] ]
]
console.log(flat1(arr1)) // [1, 2, 3, 4, 5, 6, 7, 8]
遍歷實現(方式2)
/**
*
* @param {*} array 深層巢狀的資料
* @returns array 新陣列
*/
const flat2 = (array) => {
const result = []
// 展開一層
const stack = [ ...array ]
while (stack.length !== 0) {
// 取出最後一個元素
const val = stack.pop()
if (Array.isArray(val)) {
// 遇到是陣列的情況,往stack後面推入
stack.push(...val)
} else {
// 往陣列前面推入
result.unshift(val)
}
}
return result
}
// 測試
let arr2 = [
1,
[ 2, 3, 4 ],
[ 5, [ 6, [ 7, [ 8 ] ] ] ]
]
console.log(flat2(arr2)) // [1, 2, 3, 4, 5, 6, 7, 8]
逗比版本(方式3)
藉助原生flat函式,將需要展開的層,指定為Infinity即無限層,也就可以拍平了,算是一個思路,不過面試官估計覺得我們們是個逗比?,也不知道寫出這樣的程式碼,讓不讓過。
/**
*
* @param {*} array 深層巢狀的資料
* @returns 新陣列
*/
const flat3 = (array) => {
return array.flat(Infinity)
}
// 測試
let arr3 = [
1,
[ 2, 3, 4 ],
[ 5, [ 6, [ 7, [ 8 ] ] ] ]
]
console.log(flat3(arr3)) // [1, 2, 3, 4, 5, 6, 7, 8]
5. 實現深拷貝
const deepClone = (target, cache = new Map()) => {
const isObject = (obj) => typeof obj === 'object' && obj !== null
if (isObject(target)) {
// 解決迴圈引用
const cacheTarget = cache.get(target)
// 已經存在直接返回,無需再次解析
if (cacheTarget) {
return cacheTarget
}
let cloneTarget = Array.isArray(target) ? [] : {}
cache.set(target, cloneTarget)
for (const key in target) {
if (target.hasOwnProperty(key)) {
const value = target[ key ]
cloneTarget[ key ] = isObject(value) ? deepClone(value, cache) : value
}
}
return cloneTarget
} else {
return target
}
}
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: { f: {} } } } } } } } } } } },
};
target.target = target;
const result1 = deepClone(target);
console.log(result1)
6. 實現new操作符
思路: 在實現new之前,我們先了解一下new的執行過程
new
關鍵字會進行如下的操作:
- 建立一個空的簡單JavaScript物件(即
{}
); - 為步驟1新建立的物件新增屬性 proto ,將該屬性連結至建構函式的原型物件
- 將步驟1新建立的物件作為
this
的上下文,執行該函式 ; - 如果該函式沒有返回物件,則返回
this
。
const _new = function (func, ...args) {
// 步驟1和步驟2
let obj = Object.create(func.prototype)
// 也可以通過下面的程式碼進行模擬
/**
let Ctor = function () {}
Ctor.prototype = func.prototype
Ctor.prototype.constructor = func
let obj = new Ctor()
*/
// 步驟3
let result = func.apply(obj, args)
// 步驟4
if (typeof result === 'object' && result !== null || typeof result === 'function') {
return result
} else {
return obj
}
}
// 測試
let Person = function (name, sex) {
this.name = name
this.sex = sex
}
Person.prototype.showInfo = function () {
console.log(this.name, this.sex)
}
let p1 = _new(Person, 'qianlongo', 'sex')
console.log(p1)
// Person { name: '前端胖頭魚', sex: 'sex' }
7. 實現釋出訂閱(EventEmitter)
釋出訂閱相信大家一定不會陌生,實際工作也經常會遇到,比如Vue的EventBus
,$on
,$emit
等。接下來我們們實現一把試試
class EventEmitter {
constructor () {
this.events = {}
}
// 事件監聽
on (evt, callback, ctx) {
if (!this.events[ evt ]) {
this.events[ evt ] = []
}
this.events[ evt ].push(callback)
return this
}
// 釋出事件
emit (evt, ...payload) {
const callbacks = this.events[ evt ]
if (callbacks) {
callbacks.forEach((cb) => cb.apply(this, payload))
}
return this
}
// 刪除訂閱
off (evt, callback) {
// 啥都沒傳,所有的事件都取消
if (typeof evt === 'undefined') {
delete this.events
} else if (typeof evt === 'string') {
// 刪除指定事件的回撥
if (typeof callback === 'function') {
this.events[ evt ] = this.events[ evt ].filter((cb) => cb !== callback)
} else {
// 刪除整個事件
delete this.events[ evt ]
}
}
return this
}
// 只進行一次的事件訂閱
once (evt, callback, ctx) {
const proxyCallback = (...payload) => {
callback.apply(ctx, payload)
// 回撥函式執行完成之後就刪除事件訂閱
this.off(evt, proxyCallback)
}
this.on(evt, proxyCallback, ctx)
}
}
// 測試
const e1 = new EventEmitter()
const e1Callback1 = (name, sex) => {
console.log(name, sex, 'evt1---callback1')
}
const e1Callback2 = (name, sex) => {
console.log(name, sex, 'evt1---callback2')
}
const e1Callback3 = (name, sex) => {
console.log(name, sex, 'evt1---callback3')
}
e1.on('evt1', e1Callback1)
e1.on('evt1', e1Callback2)
// 只執行一次回撥
e1.once('evt1', e1Callback3)
e1.emit('evt1', '前端胖頭魚', 'boy')
// 前端胖頭魚 boy evt1---callback1
// 前端胖頭魚 boy evt1---callback2
// 前端胖頭魚 boy evt1---callback3
console.log('------嘗試刪除e1Callback1------')
// 移除e1Callback1
e1.off('evt1', e1Callback1)
e1.emit('evt1', '前端胖頭魚', 'boy')
// 前端胖頭魚 boy evt1---callback2
8. 實現有並行限制的Promise
這是一道廣大網友真實遇到題目,我們先看一下題意
/*
JS實現一個帶併發限制的非同步排程器Scheduler,保證同時執行的任務最多有兩個。
完善下面程式碼的Scheduler類,使以下程式能夠正常輸出:
class Scheduler {
add(promiseCreator) { ... }
// ...
}
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
}
})
const scheduler = new Scheduler()
const addTask = (time,order) => {
scheduler.add(() => timeout(time).then(()=>console.log(order)))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// output: 2 3 1 4
整個的完整執行流程:
起始1、2兩個任務開始執行
500ms時,2任務執行完畢,輸出2,任務3開始執行
800ms時,3任務執行完畢,輸出3,任務4開始執行
1000ms時,1任務執行完畢,輸出1,此時只剩下4任務在執行
1200ms時,4任務執行完畢,輸出4
*/
解析
看完題目之後,大概會這幾個問題存在
- 如何才能保證同時只有2個任務在處於執行中?
- 當某個任務執行結束之後,下一步如何知道應該執行哪個任務?
問題1:只需要用一個計數器來控制即可,每開始一個任務計數器+1,結束之後計數器-1,保證計數器一定<=2。
問題2:按照題目要求,任務的執行是有順序的,只是任務的結束時間是不確定的,所以下一個任務一定是按照這樣的順序來
任務1 => 任務2 => 任務3 => 任務4
利用陣列佇列的性質,將任務挨個推入佇列,前面的任務執行結束之後,將隊首的任務取出來執行即可。
class Scheduler {
constructor () {
this.queue = []
this.maxCount = 2
this.runCount = 0
}
// promiseCreator執行後返回的是一個Promise
add(promiseCreator) {
// 小於等於2,直接執行
this.queue.push(promiseCreator)
// 每次新增的時候都會嘗試去執行任務
this.runQueue()
}
runQueue () {
// 佇列中還有任務才會被執行
if (this.queue.length && this.runCount < this.maxCount) {
// 執行先加入佇列的函式
const promiseCreator = this.queue.shift()
// 開始執行任務 計數+1
this.runCount += 1
// 假設任務都執行成功,當然也可以做一下catch
promiseCreator().then(() => {
// 任務執行完畢,計數-1
this.runCount -= 1
// 嘗試進行下一次任務
this.runQueue()
})
}
}
}
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
const scheduler = new Scheduler()
const addTask = (time,order) => {
scheduler.add(() => timeout(time).then(()=>console.log(order)))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// 2
// 3
// 1
// 4
9. 手寫LRU演算法(螞蟻金服曾遇到過)
這道演算法題我記得以前在螞蟻金服的面試中遇到過,大家也有可能會遇到噢。
大致題意
運用你所掌握的資料結構,設計和實現一個 LRU (最近最少使用) 快取機制 。
實現 LRUCache 類:
- LRUCache(int capacity) 以
正整數
作為容量 capacity 初始化 LRU 快取 - int get(int key)
如果關鍵字 key 存在於快取中,則返回關鍵字的值,否則返回 -1
。 - void put(int key, int value)
如果關鍵字已經存在,則變更其資料值
;如果關鍵字不存在,則插入該組「關鍵字-值」
。當快取容量達到上限時,它應該在寫入新資料之前刪除最久未使用的資料值
,從而為新的資料值留出空間。
題目要求的1和2相對簡單,主要是條件3,當快取容量達到上限時,它應該在寫入新資料之前刪除最久未使用的資料值
。容量和條件1相呼應,關鍵是怎麼理解最久未使用呢?
- 讀和寫都是在使用資料
- 假設不管是讀還是寫,我們都把對應的
key
值放到陣列的末尾,那麼是不是意味著陣列的頭部就是最久未使用的了呢?
陣列&&物件實現方式
var LRUCache = function (capacity) {
// 用陣列記錄讀和寫的順序
this.keys = []
// 用物件來儲存key value值
this.cache = {}
// 容量
this.capacity = capacity
}
LRUCache.prototype.get = function (key) {
// 如果存在
if (this.cache[key]) {
// 先刪除原來的位置
remove(this.keys, key)
// 再移動到最後一個,以保持最新訪問
this.keys.push(key)
// 返回值
return this.cache[key]
}
return -1
}
LRUCache.prototype.put = function (key, value) {
if (this.cache[key]) {
// 存在的時候先更新值
this.cache[key] = value
// 再更新位置到最後一個
remove(this.keys, key)
this.keys.push(key)
} else {
// 不存在的時候加入
this.keys.push(key)
this.cache[key] = value
// 容量如果超過了最大值,則刪除最久未使用的(也就是陣列中的第一個key)
if (this.keys.length > this.capacity) {
removeCache(this.cache, this.keys, this.keys[0])
}
}
}
// 移出陣列中的key
function remove(arr, key) {
if (arr.length) {
const index = arr.indexOf(key)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
// 移除快取中 key
function removeCache(cache, keys, key) {
cache[key] = null
remove(keys, key)
}
const lRUCache = new LRUCache(2)
console.log(lRUCache.put(1, 1)) // 快取是 {1=1}
console.log(lRUCache.put(2, 2)) // 快取是 {1=1, 2=2}
console.log(lRUCache.get(1)) // 返回 1
console.log(lRUCache.put(3, 3)) // 該操作會使得關鍵字 2 作廢,快取是 {1=1, 3=3}
console.log(lRUCache.get(2)) // 返回 -1 (未找到)
console.log(lRUCache.put(4, 4)) // 該操作會使得關鍵字 1 作廢,快取是 {4=4, 3=3}
console.log(lRUCache.get(1) ) // 返回 -1 (未找到)
console.log(lRUCache.get(3)) // 返回 3
console.log(lRUCache.get(4) ) // 返回 4
Map實現方式
第一種實現方式,我們藉助了陣列來儲存每次key被訪問(get、set)的順序,這樣實現比較麻煩一些,有沒有其他方案,讓我們更加便捷一些,不需要額外維護陣列呢?藉助Map
設定值時可以保持順序性,處理LRU演算法將會及其方便
/**
* @param {number} capacity
*/
var LRUCache = function (capacity) {
this.cache = new Map()
this.capacity = capacity
};
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function (key) {
if (this.cache.has(key)) {
const value = this.cache.get(key)
// 更新位置
this.cache.delete(key)
this.cache.set(key, value)
return value
}
return -1
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function (key, value) {
// 已經存在的情況下,更新其位置到”最新“即可
// 先刪除,後插入
if (this.cache.has(key)) {
this.cache.delete(key)
} else {
// 插入資料前先判斷,size是否符合capacity
// 已經>=capacity,需要把最開始插入的資料刪除掉
// keys()方法得到一個可遍歷物件,執行next()拿到一個形如{ value: 'xxx', done: false }的物件
if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value)
}
}
this.cache.set(key, value)
};
const lRUCache = new LRUCache(2)
console.log(lRUCache.put(1, 1)) // 快取是 {1=1}
console.log(lRUCache.put(2, 2)) // 快取是 {1=1, 2=2}
console.log(lRUCache.get(1)) // 返回 1
console.log(lRUCache.put(3, 3)) // 該操作會使得關鍵字 2 作廢,快取是 {1=1, 3=3}
console.log(lRUCache.get(2)) // 返回 -1 (未找到)
console.log(lRUCache.put(4, 4)) // 該操作會使得關鍵字 1 作廢,快取是 {4=4, 3=3}
console.log(lRUCache.get(1) ) // 返回 -1 (未找到)
console.log(lRUCache.get(3)) // 返回 3
console.log(lRUCache.get(4) ) // 返回 4
10. call
mdn call上是這樣描述call的,call
方法使用一個指定的this
值和單獨給出的一個或多個引數來呼叫一個函式。 所以關鍵點是指定的this
和一個或者多個引數
,只要瞭解了this的基本用法,實現起來就簡單多了。
/**
*
* @param {*} ctx 函式執行上下文this
* @param {...any} args 引數列表
* @returns 函式執行的結果
*/
Function.prototype.myCall = function (ctx, ...args) {
// 簡單處理未傳ctx上下文,或者傳的是null和undefined等場景
if (!ctx) {
ctx = typeof window !== 'undefined' ? window : global
}
// 暴力處理 ctx有可能傳非物件
ctx = Object(ctx)
// 用Symbol生成唯一的key
const fnName = Symbol()
// 這裡的this,即要呼叫的函式
ctx[ fnName ] = this
// 將args展開,並且呼叫fnName函式,此時fnName函式內部的this也就是ctx了
const result = ctx[ fnName ](...args)
// 用完之後,將fnName從上下文ctx中刪除
delete ctx[ fnName ]
return result
}
// 測試
let fn = function (name, sex) {
console.log(this, name, sex)
}
fn.myCall('', '前端胖頭魚')
// window 前端胖頭魚 boy
fn.myCall({ name: '前端胖頭魚', sex: 'boy' }, '前端胖頭魚')
// { name: '前端胖頭魚', sex: 'boy' } 前端胖頭魚 boy
11. apply
該方法的語法和作用與call
方法類似,只有一個區別,就是call
方法接受的是一個引數列表,而apply
方法接受的是一個包含多個引數的陣列。
/**
*
* @param {*} ctx 函式執行上下文this
* @param {*} args 引數列表
* @returns 函式執行的結果
*/
// 唯一的區別在這裡,不需要...args變成陣列
Function.prototype.myApply = function (ctx, args) {
if (!ctx) {
ctx = typeof window !== 'undefined' ? window : global
}
ctx = Object(ctx)
const fnName = Symbol()
ctx[ fnName ] = this
// 將args引數陣列,展開為多個引數,供函式呼叫
const result = ctx[ fnName ](...args)
delete ctx[ fnName ]
return result
}
// 測試
let fn = function (name, sex) {
console.log(this, name, sex)
}
fn.myApply('', ['前端胖頭魚', 'boy'])
// window 前端胖頭魚 boy
fn.myApply({ name: '前端胖頭魚', sex: 'boy' }, ['前端胖頭魚', 'boy'])
// { name: '前端胖頭魚', sex: 'boy' } 前端胖頭魚 boy
12. 實現trim方法的兩種方式
trim
方法會從一個字串的兩端刪除空白字元。在這個上下文中的空白字元是所有的空白字元 (space, tab, no-break space 等) 以及所有行終止符字元(如 LF,CR等)
思路:
初看題目我們腦海中閃過的做法是把空格部分刪除掉,保留非空格的部分
,但是也可以換一種思路,也可以把非空格的部分提取出來,不管空格的部分
。接下來我們來寫一下兩種trim方法的實現
去除空格法(方式1)
const trim = (str) => {
return str.replace(/^\s*|\s*$/g, '')
}
console.log(trim(' 前端胖頭魚')) // 前端胖頭魚
console.log(trim('前端胖頭魚 ')) // 前端胖頭魚
console.log(trim(' 前端胖頭魚 ')) // 前端胖頭魚
console.log(trim(' 前端 胖頭魚 ')) // 前端 胖頭魚
字元提取法(方式2)
const trim = (str) => {
return str.replace(/^\s*(.*?)\s*$/g, '$1')
}
console.log(trim(' 前端胖頭魚')) // 前端胖頭魚
console.log(trim('前端胖頭魚 ')) // 前端胖頭魚
console.log(trim(' 前端胖頭魚 ')) // 前端胖頭魚
console.log(trim(' 前端 胖頭魚 ')) // 前端 胖頭魚
13. 實現Promise.all
Promise.all() 方法接收一個promise的iterable型別(注:Array,Map,Set都屬於ES6的iterable型別)的輸入,並且只返回一個Promise
例項, 那個輸入的所有promise的resolve回撥的結果是一個陣列。這個Promise
的resolve回撥執行是在所有輸入的promise的resolve回撥都結束,或者輸入的iterable裡沒有promise了的時候。它的reject回撥執行是,只要任何一個輸入的promise的reject回撥執行或者輸入不合法的promise就會立即丟擲錯誤,並且reject的是第一個丟擲的錯誤資訊。
上面是MDN上關於Promise.all
的描述,咋一看有點懵逼,我們一起總結一下關鍵點
Promise.all
接收一個陣列,陣列裡面可以是Promise例項也可以不是Promise.all
等待所有都完成(或第一個失敗)Promise.all
執行的結果也是一個Promise
Promise.myAll = (promises) => {
// 符合條件3,返回一個Promise
return new Promise((rs, rj) => {
let count = 0
let result = []
const len = promises.length
promises.forEach((p, i) => {
// 符合條件1,將陣列裡的項通過Promise.resolve進行包裝
Promise.resolve(p).then((res) => {
count += 1
result[ i ] = res
// 符合條件2 等待所有都完成
if (count === len) {
rs(result)
}
// 符合條件2 只要一個失敗就都失敗
}).catch(rj)
})
})
}
let p1 = Promise.resolve(1)
let p2 = 2
let p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 3)
})
let p4 = Promise.reject('出錯啦')
Promise.myAll([p1, p2, p3]).then((res) => {
console.log(res); // [ 1, 2, 3 ]
});
Promise.myAll([ p1, p2, 3 ]).then((res) => {
console.log(res) // [ 1, 2, 3 ]
}).catch((err) => {
console.log('err', err)
})
Promise.myAll([ p1, p2, p4 ]).then((res) => {
console.log(res)
}).catch((err) => {
console.log('err', err) // err 出錯啦
})
14. 實現Promise.race
Promise.race(iterable)
方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。
Promise.myRace = (promises) => {
// 返回一個新的Promise
return new Promise((rs, rj) => {
promises.forEach((p) => {
// 包裝一下promises中的項,防止非Promise .then出錯
// 只要有任意一個完成了或者拒絕了,race也就結束了
Promise.resolve(p).then(rs).catch(rj)
})
})
}
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 1);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 2);
});
Promise.myRace([promise1, promise2]).then((value) => {
// 因為promise2更快所以列印出2
console.log(value) // 2
});
Promise.myRace([promise1, promise2, 3]).then((value) => {
// 3比其他兩個更快
console.log(value) // 3
});
15. Object.create
Object.create()
方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__。
先看看如何使用
- 常規使用
// Object.create使用
const person = {
showName () {
console.log(this.name)
}
}
const me = Object.create(person)
me.name = '前端胖頭魚'
me.showName() // 前端胖頭魚
可以看到person
作為me
例項的原型存在,原型上有showName
方法
- 建立原型為null的物件
const emptyObj = Object.create(null)
console.log(emptyObj)
- 第二個 propertiesObject引數
可選。需要傳入一個物件,該物件的屬性型別參照Object.defineProperties()
的第二個引數。如果該引數被指定且不為undefined
,該傳入物件的自有可列舉屬性(即其自身定義的屬性,而不是其原型鏈上的列舉屬性)將為新建立的物件新增指定的屬性值和對應的屬性描述符。
let o = Object.create(Object.prototype, {
// foo會成為所建立物件的資料屬性
foo: {
writable:true, // 可以修改
configurable:true, // 可以配置
enumerable: true, // 可以遍歷
value: "hello"
},
// bar會成為所建立物件的訪問器屬性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
})
// 無法進行修改
o.bar = '前端胖頭魚'
console.log(o.foo) // hello
console.log(o.bar) // 10
// 遍歷測試
for (let key in o) {
console.log(key, o[key]) // foo hello
}
程式碼實現
const create = (prop, props) => {
if (![ 'object', 'function' ].includes(typeof prop)) {
throw new TypeError(`Object prototype may only be an Object or null: ${prop}`)
}
// 建立建構函式
const Ctor = function () {}
// 賦值原型
Ctor.prototype = prop
// 建立例項
const obj = new Ctor()
// 支援第二個引數
if (props) {
Object.defineProperties(obj, props)
}
// 支援空原型
if (prop === null) {
obj.__proto__ = null
}
return obj
}
// 用前面的例子做測試
const person = {
showName () {
console.log(this.name)
}
}
const me2 = create(person)
me2.name = '前端胖頭魚'
me2.showName() // 前端胖頭魚
const emptyObj2 = create(null)
console.log(emptyObj2)
const props = {
// foo會成為所建立物件的資料屬性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar會成為所建立物件的訪問器屬性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
}
let o2 = create(Object.prototype, props) // 請看下面的截圖
// 無法修改
o2.bar = '前端胖頭魚'
console.log(o2.foo) // hello
console.log(o2.bar) // 10
16.快速排序
const quickSort = (array) => {
const length = array.length
if (length <= 1) {
return array
}
const midIndex = Math.floor(length / 2)
const midValue = array.splice(midIndex, 1)[ 0 ]
let leftArray = []
let rightArray = []
let index = 0
while (index < length - 1) {
const curValue = array[ index ]
if (curValue <= midValue) {
leftArray.push(curValue)
} else {
rightArray.push(curValue)
}
index++
}
return quickSort(leftArray).concat([ midValue ], quickSort(rightArray))
}
const arr = [ -10, 10, 1, 34, 5, 1 ]
console.log(quickSort(arr)) // [-10, 1, 1, 5, 10, 34]
17.氣泡排序
/**
* 1. 從第一個元素開始,比較相鄰的兩個元素,前者大就交換位置
* 2. 每次遍歷結束,都能找到一個最大值
* 3. 如果還有沒排序的元素繼續1
*
*/
const swap = (array, a, b) => [ array[ b ], array[ a ] ] = [ array[ a ], array[ b ] ]
const bubbleSort = (array) => {
const length = array.length
for (let i = 0; i < length - 1; i++) {
for (let j = 0; j < length - 1 - i; j++) {
if (array[ j ] > array[ j + 1 ]) {
swap(array, j, j + 1)
}
}
}
return array
}
console.log(bubbleSort([ -1, 10, 10, 2 ])) // [-1, 2, 10, 10]
18. 選擇排序
/**
* 1. 取出未排序的第一個元素,遍歷該元素之後的部分並進行比較。第一次就是取第一個元素
* 2. 如果有更小的就交換位置
*/
const swap = (array, a, b) => [ array[ b ], array[ a ] ] = [ array[ a ], array[ b ] ]
const selectSort = (array) => {
const length = array.length
for (let i = 0; i < length; i++) {
let minIndex = i
for (let j = i + 1; j < length; j++) {
if (array[ j ] < array[ minIndex ]) {
minIndex = j
}
}
if (minIndex !== i) {
swap(array, i, minIndex)
}
}
return array
}
console.log(selectSort([ -1, 10, 10, 2 ])) // [-1, 2, 10, 10]
19. 插入排序
// 插入排序
/**
* 記住你是怎麼打牌的就知道插入排序怎麼實現了
* 1. 首先有一個有序的序列,可以認為第一個元素就是已排序的序列
* 2. 從未排序序列中取一個元素出來,往有序序列中找到合適的位置,如果該位置比元素大,則後移動, 否則繼續往前找
*/
const insertSort = (array) => {
for (let i = 1, length = array.length; i < length; i++) {
let j = i - 1
const curValue = array[ i ]
while (j >= 0 && array[ j ] > curValue) {
array[ j + 1 ] = array[ j ]
j--
}
array[ j + 1 ] = curValue
}
return array
}
console.log(insertSort([ -1, 10, 10, 2 ])) // [-1, 2, 10, 10]
20. setTimeout模擬setInterval
描述: 使用setTimeout
模擬實現setInterval
的功能
思路: 當然這裡不是完全的實現,比如setInterval
執行之後得到的是一個數字id,這一點我們就不模擬了,關閉定時器的方法則通過返回一個函式來進行
const simulateSetInterval = (func, timeout) => {
let timer = null
const interval = () => {
timer = setTimeout(() => {
// timeout時間之後會執行真正的函式func
func()
// 同時再次呼叫interval本身,是不是有點setInterval的感覺啦
interval()
}, timeout)
}
// 開始執行
interval()
// 返回用於關閉定時器的函式
return () => clearTimeout(timer)
}
const cancel = simulateSetInterval(() => {
console.log(1)
}, 300)
setTimeout(() => {
cancel()
console.log('一秒之後關閉定時器')
}, 1000)
可以看到1被列印出了3次,第1000毫秒的時候定時器被關閉,1也就沒有繼續列印了。
21. setInterval模擬setTimeout
描述: 使用setInterval
模擬實現setTimeout
的功能
思路: setTimeout
的特性是在指定的時間內只執行一次,我們只要在setInterval
內部執行callback之後,把定時器關掉即可
const simulateSetTimeout = (fn, timeout) => {
let timer = null
timer = setInterval(() => {
// 關閉定時器,保證只執行一次fn,也就達到了setTimeout的效果了
clearInterval(timer)
fn()
}, timeout)
// 返回用於關閉定時器的方法
return () => clearInterval(timer)
}
const cancel = simulateSetTimeout(() => {
console.log(1)
}, 1000)
// 一秒後列印出1
22.陣列去重的4種方式
業務和麵試中都經常會遇到,將陣列進行去重是必備的基本技能
利用Set實現(方式1)
const uniqueArray1 = (array) => {
return [ ...new Set(array) ]
}
// 測試
let testArray = [ 1, 2, 3, 1, 2, 3, 4 ]
console.log(uniqueArray1(testArray)) // [1, 2, 3, 4]
indexOf去重(方式2)
const uniqueArray2 = (array) => {
let result = []
array.forEach((it, i) => {
if (result.indexOf(it) === -1) {
result.push(it)
}
})
return result
}
// 測試
console.log(uniqueArray2(testArray)) // [1, 2, 3, 4]
indexOf去重(方式3)
const uniqueArray3 = (array) => {
return array.filter((it, i) => array.indexOf(it) === i)
}
// 測試
console.log(uniqueArray3(testArray)) // [1, 2, 3, 4]
Array.from去重
const uniqueArray4 = (array) => {
return Array.from(new Set(array))
}
// 測試
console.log(uniqueArray4(testArray)) // [1, 2, 3, 4]
23. 手機號3-3-4分割
手機號按照例如183-7980-2267
進行分割處理
// 適合純11位手機
const splitMobile = (mobile, format = '-') => {
return String(mobile).replace(/(?=(\d{4})+$)/g, format)
}
// 適合11位以內的分割
const splitMobile2 = (mobile, format = '-') => {
return String(mobile).replace(/(?<=(\d{3}))/, format).replace(/(?<=([\d\-]{8}))/, format)
}
console.log(splitMobile(18379802267)) // 183-7980-2267
console.log(splitMobile2(18379876545)) // 183-7987-6545
24. 千分位格式化數字
將123456789變成123,456,789 且要支援小數
// 金額轉千分位
const formatPrice = (number) => {
number = '' + number
const [ integer, decimal = '' ] = number.split('.')
return integer.replace(/\B(?=(\d{3})+$)/g, ',') + (decimal ? '.' + decimal : '')
}
console.log(formatPrice(123456789.3343)) // 123,456,789.3343
25. 二分查詢
// 704. 二分查詢
/**
*
給定一個 n 個元素有序的(升序)整型陣列 nums 和一個目標值 target ,寫一個函式搜尋 nums 中的 target,如果目標值存在返回下標,否則返回 -1。
示例 1:
輸入: nums = [-1,0,3,5,9,12], target = 9
輸出: 4
解釋: 9 出現在 nums 中並且下標為 4
示例 2:
輸入: nums = [-1,0,3,5,9,12], target = 2
輸出: -1
解釋: 2 不存在 nums 中因此返回 -1
提示:
你可以假設 nums 中的所有元素是不重複的。
n 將在 [1, 10000]之間。
nums 的每個元素都將在 [-9999, 9999]之間。
*/
const search = (nums, target) => {
let i = 0
let j = nums.length - 1
let midIndex = 0
while (i <= j) {
midIndex = Math.floor((i + j) / 2)
const midValue = nums[ midIndex ]
if (midValue === target) {
return midIndex
} else if (midValue < target) {
i = midIndex + 1
} else {
j = midIndex - 1
}
}
return -1
}
console.log(search([-1,0,3,5,9,12], 9)) // 4
26. 版本比較的兩種方式
客戶端估計遇到比較版本號的情況會比較多,但是胖頭魚在業務中也遇到過該需求
詳細規則
給你兩個版本號 version1 和 version2 ,請你比較它們。
版本號由一個或多個修訂號組成,各修訂號由一個 '.' 連線。每個修訂號由 多位數字 組成,可能包含 前導零 。每個版本號至少包含一個字元。修訂號從左到右編號,下標從 0 開始,最左邊的修訂號下標為 0 ,下一個修訂號下標為 1 ,以此類推。例如,2.5.33 和 0.1 都是有效的版本號。
比較版本號時,請按從左到右的順序依次比較它們的修訂號。比較修訂號時,只需比較 忽略任何前導零後的整數值 。也就是說,修訂號 1 和修訂號 001 相等 。如果版本號沒有指定某個下標處的修訂號,則該修訂號視為 0 。例如,版本 1.0 小於版本 1.1 ,因為它們下標為 0 的修訂號相同,而下標為 1 的修訂號分別為 0 和 1 ,0 < 1 。
返回規則如下:
如果 version1 > version2 返回 1,
如果 version1 < version2 返回 -1,
除此之外返回 0。
原始碼實現
// 比較版本號
const compareVersion = function(version1, version2) {
version1 = version1.split('.')
version2 = version2.split('.')
const len1 = version1.length
const len2 = version2.length
let maxLen = len1
const fillZero = (array, len) => {
while (len--) {
array.push(0)
}
}
if (len1 < len2) {
fillZero(version1, len2 - len1)
maxLen = len2
} else if (len1 > len2) {
fillZero(version2, len1 - len2)
maxLen = len1
}
for (let i = 0; i < maxLen; i++) {
const a = parseInt(version1[i])
const b = parseInt(version2[i])
if ( a === b) {
// i++
} else if (a > b) {
return 1
} else {
return -1
}
}
return 0
}
// 也可以不補零
const compareVersion = function(version1, version2) {
version1 = version1.split('.')
version2 = version2.split('.')
const maxLen = Math.max(version1.length, version2.length)
for (let i = 0; i < maxLen; i++) {
const a = parseInt(version1[i]??0)
const b = parseInt(version2[i]??0)
if ( a === b) {
// i++
} else if (a > b) {
return 1
} else {
return -1
}
}
return 0
}
console.log(compareVersion('1.0', '1.0.0'))
// 擴充套件1比較多個版本號並排序
const compareMoreVersion = (versions) => {
return versions.sort((a, b) => compareVersion(a, b))
}
console.log(compareMoreVersion(['1.0', '3.1', '1.01']))
27. 解析 url 引數
根據name獲取url上的search引數值
const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// 一般都會通過decodeURIComponent解碼處理
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}
// https://www.baidu.com/?name=%E5%89%8D%E7%AB%AF%E8%83%96%E5%A4%B4%E9%B1%BC&sex=boy
console.log(getQueryByName('name'), getQueryByName('sex')) // 前端胖頭魚 boy
28. 實現獲取js資料型別的通用函式
實現一個通用函式判斷資料型別
const getType = (s) => {
const r = Object.prototype.toString.call(s)
return r.replace(/\[object (.*?)\]/, '$1').toLowerCase()
}
// 測試
console.log(getType()) // undefined
console.log(getType(null)) // null
console.log(getType(1)) // number
console.log(getType('前端胖頭魚')) // string
console.log(getType(true)) // boolean
console.log(getType(Symbol('前端胖頭魚'))) // symbol
console.log(getType({})) // object
console.log(getType([])) // array
29. 字串轉化為駝峰
如下規則,將對應字串變成駝峰寫法
1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar
const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}
// 測試
console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar
30. 實現reduce
reduce
方法對陣列中的每個元素執行一個由您提供的reducer函式(升序執行),將其結果彙總為單個返回值 mdn
這個函式稍微複雜一些,我們用一個例子來看一下他是怎麼用的。
const sum = [1, 2, 3, 4].reduce((prev, cur) => {
return prev + cur;
})
console.log(sum) // 10
// 初始設定
prev = initialValue = 1, cur = 2
// 第一次迭代
prev = (1 + 2) = 3, cur = 3
// 第二次迭代
prev = (3 + 3) = 6, cur = 4
// 第三次迭代
prev = (6 + 4) = 10, cur = undefined (退出)
程式碼實現
Array.prototype.reduce2 = function (callback, initValue) {
if (typeof callback !== 'function') {
throw `${callback} is not a function`
}
let pre = initValue
let i = 0
const length = this.length
// 當沒有傳遞初始值時,取第一個作為初始值
if (typeof pre === 'undefined') {
pre = this[0]
i = 1
}
while (i < length) {
if (i in this) {
pre = callback(pre, this[ i ], i, this)
}
i++
}
return pre
}
複製程式碼
測試一把
const sum = [1, 2, 3, 4].reduce2((prev, cur) => {
return prev + cur;
})
console.log(sum) // 10