天天看點

【JS】528- 前端常見手寫代碼實作

【JS】528- 前端常見手寫代碼實作

模拟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
}      

深拷貝(尤雨溪版)

vuex源碼

  • 原理與上一版類似
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​