過完年馬上又要到金三銀四面試季了,想必很多同學已經躍躍欲試,提前開始準備面試了,本文就列舉了面試過程中一些常見的手寫代碼實現供參考。或許很多人會問,這些手寫代碼實現意義何在,社區已經有很多poly-fill
或者函數庫供選擇,何必要自己費力去折騰呢?我的理解是,在真實業務開發場景中,我們真的用不上這些自己寫的方法,一個lodash
庫完全可以滿足我們的需求,但此時你僅僅只是一個API Caller ,你經常使用到它,但對它實現原理卻一無所知,哪怕它實現起來其實是非常簡單。所以親自動手寫出它的實現過程,對理解其中原理是很有幫助的。另外,不要覺得用ES6
語法,或者最新的語法去實現ES5
甚至是ES3
的方法是件可笑的事情,相反,它更能體現出你對ES6
語法的掌握程度以及對JS發展的關注度,在面試中說不定會成爲你的一個亮點。
“我自己是一名從事了6年web前端開發的老程序員(我的微信:web-xxq),今年年初我花了一個月整理了一份最適合2019年自學的web前端全套培訓教程(視頻+源碼+筆記+項目實戰),從最基礎的HTML+CSS+JS到移動端HTML5以及各種框架和新技術都有整理,打包給每一位前端小夥伴,這裏是前端學習者聚集地,歡迎初學和進階中的小夥伴(所有前端教程關注我的微信公衆號:web前端學習圈,關注後回覆“2020”即可領取)。
模擬call
- 第一個參數爲
null
或者undefined
時,this
指向全局對象window
,值爲原始值的指向該原始值的自動包裝對象,如String
、Number
、Boolean
- 爲了避免函數名與上下文(
context
)的屬性發生衝突,使用Symbol
類型作爲唯一值 - 將函數作爲傳入的上下文(
context
)屬性執行 - 函數執行完成後刪除該屬性
- 返回執行結果
Function.prototype.myCall = function(context, ...args) {
context = (context ?? window) || new Object(context)
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
複製代碼
注: 代碼實現使用了ES2020
新特性Null
判斷符 ??
, 詳細參考阮一峯老師的ECMAScript 6 入門
模擬apply
- 前部分與
call
一樣 - 第二個參數可以不傳,但類型必須爲數組或者類數組
Function.prototype.myApply = function(context) {
context = (context ?? window) || new Object(context)
const key = Symbol()
const args = arguments[1]
context[key] = this
let result
if(args) {
result = context[key](...args)
} else {
result = context[key]
}
delete context[key]
return result
}
複製代碼
注:代碼實現存在缺陷,當第二個參數爲類數組時,未作判斷(有興趣可查閱一下如何判斷類數組)
模擬bind
- 使用
call / apply
指定this
- 返回一個綁定函數
- 當返回的綁定函數作爲構造函數被
new
調用,綁定的上下文指向實例對象 - 設置綁定函數的
prototype
爲原函數的prototype
Function.prototype.myBind = function(context, ...args) {
const fn = this
const bindFn = function (...newFnArgs) {
fn.call(
this instanceof bindFn ? this : context,
...args, ...newFnArgs
)
}
bindFn.prototype = Object.create(fn.prototype)
return bindFn
}
複製代碼
模擬new
- 創建一個新的空對象
- 把
this
綁定到空對象 - 使空對象的
__proto__
指向構造函數的原型(prototype
) - 執行構造函數,爲空對象添加屬性
- 判斷構造函數的返回值是否爲對象,如果是對象,就使用構造函數的返回值,否則返回創建的對象
const createNew = (Con, ...args) => {
const obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}
複製代碼
模擬instanceOf
- 遍歷左邊變量的原型鏈,直到找到右邊變量的 prototype,如果沒有找到,返回
false
const myInstanceOf = (left, right) => {
let leftValue = left.__proto__
let rightValue = right.prototype
while(true) {
if(leftValue === null) return false
if(leftValue === rightValue) return true
leftValue = leftValue.__proto__
}
}
複製代碼
深拷貝(簡單版)
- 判斷類型是否爲原始類型,如果是,無需拷貝,直接返回
- 爲避免出現循環引用,拷貝對象時先判斷存儲空間中是否存在當前對象,如果有就直接返回
- 開闢一個存儲空間,來存儲當前對象和拷貝對象的對應關係
- 對引用類型遞歸拷貝直到屬性爲原始類型
const deepClone = (target, cache = new WeakMap()) => {
if(target === null || typeof target !== 'object') {
return target
}
if(cache.get(target)) {
return target
}
const copy = Array.isArray(target) ? [] : {}
cache.set(target, copy)
Object.keys(target).forEach(key => copy[key] = deepClone(obj[key], cache))
return copy
}
複製代碼
深拷貝(尤雨溪版)
- 原理與上一版類似
function find(list, f) {
return list.filter(f)[0]
}
function deepCopy(obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => copy[key] = deepCopy(obj[key], cache))
return copy
}
複製代碼
函數防抖
this
繼承自父級上下文,指向觸發事件的目標元素- 事件被觸發時,傳入
event
對象 - 傳入
leading
參數,判斷是否可以立即執行回調函數,不必要等到事件停止觸發後纔開始執行 - 回調函數可以有返回值,需要返回執行結果
const debounce = (fn, wait = 300, leading = true) => {
let timerId, result
return function(...args) {
timerId && clearTimeout(timerId)
if (leading) {
if (!timerId) result = fn.apply(this, args)
timerId = setTimeout(() => timerId = null, wait)
} else {
timerId = setTimeout(() => result = fn.apply(this, args), wait)
}
return result
}
}
複製代碼
函數節流(定時器)
const throttle = (fn, wait = 300) => {
let timerId
return function(...args) {
if(!timerId) {
timerId = setTimeout(() => {
timerId = null
return result = fn.apply(this, ...args)
}, wait)
}
}
}
複製代碼
函數節流(時間戳)
const throttle = (fn, wait = 300) => {
let prev = 0
let result
return function(...args) {
let now = +new Date()
if(now - prev > wait) {
prev = now
return result = fn.apply(this, ...args)
}
}
}
複製代碼
函數節流實現方法區別
方法 | 使用時間戳 | 使用定時器 |
---|---|---|
開始觸發時 | 立刻執行 | n秒後執行 |
停止觸發後 | 不再執行事件 | 繼續執行一次事件 |
數組去重
const uniqBy = (arr, key) => {
return [...new Map(arr.map(item) => [item[key], item])).values()]
}
const singers = [
{ id: 1, name: 'Leslie Cheung' },
{ id: 1, name: 'Leslie Cheung' },
{ id: 2, name: 'Eason Chan' },
]
console.log(uniqBy(singers, 'id'))
// [
// { id: 1, name: 'Leslie Cheung' },
// { id: 2, name: 'Eason Chan' },
// ]
複製代碼
原理是利用Map
的鍵不可重複
數組扁平化(技巧版)
const flatten = (arr) => arr.toString().split(',').map(item => +item)
複製代碼
數組扁平化
const flatten = (arr, deep = 1) => {
return arr.reduce((cur, next) => {
return Array.isArray(next) && deep > 1 ?
[...cur, ...flatten(next, deep - 1)] :
[...cur, next]
},[])
}
複製代碼
函數柯里化
const currying = (fn) {
_curry = (...args) =>
args.length >= fn.length
? fn(...args)
: (...newArgs) => _curry(...args, ...newArgs)
}
複製代碼
原理是利用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數
發佈訂閱EventEmitter
class EventEmitter {
#subs = {}
emit(event, ...args) {
if (this.#subs[event] && this.#subs[event].length) {
this.#subs[event].forEach(cb => cb(...args))
}
}
on(event, cb) {
(this.#subs[event] || (this.#subs[event] = [])).push(cb)
}
off(event, offCb) {
if (offCb) {
if (this.#subs[event] && this.#subs[event].length)
this.#subs[event] = this.#subs[event].filter(cb => cb !== offCb)
} else {
this.#subs[event] = []
}
}
}
複製代碼
subs
是EventEmitter
私有屬性(最新特性參考阮一峯老師的ECMAScript 6 入門),通過on
註冊事件,off
註銷事件,emit
觸發事件
寄生組合繼承
function Super(foo) {
this.foo = foo
}
Super.prototype.printFoo = function() {
console.log(this.foo)
}
function Sub(bar) {
this.bar = bar
Super.call(this)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
複製代碼
ES6版繼承
class Super {
constructor(foo) {
this.foo = foo
}
printFoo() {
console.log(this.foo)
}
}
class Sub extends Super {
constructor(foo, bar) {
super(foo)
this.bar = bar
}
}
複製代碼
ES5
的繼承,實質是先創造子類的實例對象,然後將再將父類的方法添加到this
上。 ES6
的繼承,先創造父類的實例對象(所以必須先調用super
方法,然後再用子類的構造函數修改this