初、中級前端應該要掌握的手寫代碼實現

過完年立刻又要到金三銀四面試季了,想必不少同窗已經躍躍欲試,提早開始準備面試了,本文就列舉了面試過程當中一些常見的手寫代碼實現供參考。或許不少人會問,這些手寫代碼實現意義何在,社區已經有不少poly-fill或者函數庫供選擇,何須要本身費力去折騰呢?個人理解是,在真實業務開發場景中,咱們真的用不上這些本身寫的方法,一個lodash庫徹底能夠知足咱們的需求,但此時你僅僅只是一個API Caller ,你常用到它,但對它實現原理卻一無所知,哪怕它實現起來是很是簡單的。因此親自動手寫出它的實現過程,對你理解其中原理是頗有幫助的。另外,不要以爲用ES6語法,或者最新的語法去實現ES5甚至是ES3的方法是件好笑的事情,相反,它更能體現出你對ES6語法的掌握程度以及對JS發展的關注度,在面試中說不定會成爲你的一個亮點。javascript

模擬call

  • 第一個參數爲null或者undefined時,this指向全局對象window,值爲原始值的指向該原始值的自動包裝對象,如 StringNumberBoolean
  • 爲了不函數名與上下文(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 入門vue

模擬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
}
複製代碼

注:代碼實現存在缺陷,當第二個參數爲類數組時,未做判斷(有興趣可查閱一下如何判斷類數組)java

模擬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
}
複製代碼

缺點:沒法拷貝函數、MapSet、正則等其餘類型git

深拷貝(尤雨溪版)

vuex源碼es6

  • 原理與上一版相似
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
}
複製代碼

深拷貝(複雜版)

如何寫出一個驚豔面試官的深拷貝?github

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];


function forEach(array, iteratee) {
    let index = -1;
    const length = array.length;
    while (++index < length) {
        iteratee(array[index], index);
    }
    return array;
}

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

function getType(target) {
    return Object.prototype.toString.call(target);
}

function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

function cloneOtherType(targe, type) {
    const Ctor = targe.constructor;
    switch (type) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(targe);
        case regexpTag:
            return cloneReg(targe);
        case symbolTag:
            return cloneSymbol(targe);
        case funcTag:
            return cloneFunction(targe);
        default:
            return null;
    }
}

function clone(target, map = new WeakMap()) {

    // 克隆原始類型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    } else {
        return cloneOtherType(target, type);
    }

    // 防止循環引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

    // 克隆set
    if (type === setTag) {
        target.forEach(value => {
            cloneTarget.add(clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆map
    if (type === mapTag) {
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value, map));
        });
        return cloneTarget;
    }

    // 克隆對象和數組
    const keys = type === arrayTag ? undefined : Object.keys(target);
    forEach(keys || target, (value, key) => {
        if (keys) {
            key = value;
        }
        cloneTarget[key] = clone(target[key], map);
    });

    return cloneTarget;
}
複製代碼

深拷貝(高性能版)

頭條面試官:你知道如何實現高性能版本的深拷貝嘛?面試

const MY_IMMER = Symbol('my-immer1')

const isPlainObject = value => {
  if (
    !value ||
    typeof value !== 'object' ||
    {}.toString.call(value) != '[object Object]'
  ) {
    return false
  }
  var proto = Object.getPrototypeOf(value)
  if (proto === null) {
    return true
  }
  var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor
  return (
    typeof Ctor == 'function' &&
    Ctor instanceof Ctor &&
    Function.prototype.toString.call(Ctor) ===
      Function.prototype.toString.call(Object)
  )
}

const isProxy = value => !!value && !!value[MY_IMMER]

function produce(baseState, fn) {
  const proxies = new Map()
  const copies = new Map()

  const objectTraps = {
    get(target, key) {
      if (key === MY_IMMER) return target
      const data = copies.get(target) || target
      return getProxy(data[key])
    },
    set(target, key, val) {
      const copy = getCopy(target)
      const newValue = getProxy(val)
      // 這裏的判斷用於拿 proxy 的 target
      // 不然直接 copy[key] = newValue 的話外部拿到的對象是個 proxy
      copy[key] = isProxy(newValue) ? newValue[MY_IMMER] : newValue
      return true
    }
  }

  const getProxy = data => {
    if (isProxy(data)) {
      return data
    }
    if (isPlainObject(data) || Array.isArray(data)) {
      if (proxies.has(data)) {
        return proxies.get(data)
      }
      const proxy = new Proxy(data, objectTraps)
      proxies.set(data, proxy)
      return proxy
    }
    return data
  }

  const getCopy = data => {
    if (copies.has(data)) {
      return copies.get(data)
    }
    const copy = Array.isArray(data) ? data.slice() : { ...data }
    copies.set(data, copy)
    return copy
  }

  const isChange = data => {
    if (proxies.has(data) || copies.has(data)) return true
  }

  const finalize = data => {
    if (isPlainObject(data) || Array.isArray(data)) {
      if (!isChange(data)) {
        return data
      }
      const copy = getCopy(data)
      Object.keys(copy).forEach(key => {
        copy[key] = finalize(copy[key])
      })
      return copy
    }
    return data
  }

  const proxy = getProxy(baseState)
  fn(proxy)
  return finalize(baseState)
}
複製代碼

函數防抖

函數防抖是在事件被觸發n秒後再執行回調,若是在n秒內又被觸發,則從新計時。 函數防抖多用於input輸入框vuex

  • 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
    }
}
複製代碼

函數節流(定時器)

函數節流是指連續觸發事件,可是在 n 秒中只執行一次函數,適合應用於動畫相關的場景數組

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] = []
      }
    }
}
複製代碼

subsEventEmitter私有屬性(最新特性參考阮一峯老師的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

參考

js基礎-面試官想知道你有多理解call,apply,bind?[不看後悔系列]

【進階 6-2 期】深刻高階函數應用之柯里化

JavaScript專題之跟着 underscore 學節流

如何寫出一個驚豔面試官的深拷貝?

頭條面試官:你知道如何實現高性能版本的深拷貝嘛?

相關文章
相關標籤/搜索