JS 常见工具函数的实现原理

120 阅读3分钟

JS 常见工具函数的实现原理

防抖函数

防抖是 N 秒内函数只会被执行一次,如果 N 秒内再次被触发,则重新计算延迟时间(该方法会在最后一次操作的一段时间后才会执行,如 window.keyup、搜索提示等)

类似于狙击手的瞄准过程,狙击手在瞄准目标之后再过一段稳定时间(防止手抖)后才会射击目标

function debounce(fn, delay = 500, immediate = false) {
  let timer;

  return function() {
    const args = arguments;
    const context = this;

    if (immediate && !timer) {
      fn.apply(context, args);
    }

    if (timer) {
      clearTimeout(timer);
    }

    timer = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  }
};

节流函数

节流是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行(对于频繁操作,任意两次操作只会在一定时间间隔内才会执行,如 window.resize、window.scroll 等事件)

类似于水龙头上的水滴,水滴总会在积累到一定的大小之后才会滴下

// throttle 节流 
function throttle(fn, delay) { 
  let timeout; 
  let start = new Date(); 
  let threshhold = delay || 200;

  return function () { 
    let context = this, 
      args = arguments, 
      curr = new Date(); 

    clearTimeout(timeout); //总是干掉事件回调 
    if (curr - start >= threshhold) { 
      fn.apply(context, args); //只执行一部分方法,这些方法是在某个时间段内执行一次 
      start = curr; 
    } else { 
      //让方法在脱离事件后也能执行一次 
      timeout = setTimeout(function () { 
          fn.apply(context, args); 
      }, threshhold); 
    } 
  }; 
}; 


function throttle(fn, delay = 500, immediate = false) {
  let timer;

  return function() {
    const args = arguments;
    const context = this;

    if (immediate) {
      fn.apply(context, args);
      immediate = false;
    }

    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(context, args);
        timer = null;
      }, delay);
    }
  }
};

call、apply 和 bind 函数

Function.prototype.$call = function(context) {
  context = context || window;
  const fn = +new Date() + '' + Math.random(); // 防止同名的 key
  context[fn] = this;

  const args = [...arguments].slice(1);
  const result = eval('context[fn](' + args.toString() +')')

  delete context[fn];

  return result;
}

Function.prototype.$apply = function(context, args = []) {
  context = context || window;
  const fn = +new Date() + '' + Math.random(); // 防止同名的 key
  context[fn] = this;

  const result = context[fn](...args);

  delete context[fn];

  return result;
}

// Function.prototype.bind 方法的实现
Function.prototype.$bind = function (context) {
    let self = this;
    let args = Array.prototype.slice.call(arguments, 1);
    let fBind = function () {
        // 说明:一个绑定函数也能使用 new 操作符创建对象,并需要继承原函数的原型链方法,这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数
        // 通过 new 方法使用该绑定函数时,this 指向新创建的对象,则 this instanceof fBind 成立
        return this.apply(this instanceof fBind ? this : context, args.concat(arguments));
    };
    // 用于把原型链传递下去
    fBind.prototype = Object.create(this.prototype);
    
    // ES5 中才加入 Object.create() 和 Function.prototype.bind,所以 Function.prototype.bind 不能使用时,Object.create() 也不能使用,所以使用中转函数将原型链传递下去
    // function F () {}
    // F.prototype = this.prototype;
    // fBind.prototype = new F();

    return fBind;
}

实现 new 操作符

使用 new 操作符创建一个对象时,会进行以下步骤:

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this 。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function myNew(fn, ...args) {
  // let obj = {};
  // obj.__proto__ fn.prototype;
  let obj = Object.create(fn.prototype);
  let result = fn.apply(obj, ...args);

  if ((result && typeof result === 'object') || typeof result === 'function')) {
    return result;
  }

  return obj;
}

实现 instanceof 操作符

instanceof 的原理是右侧对象的原型对象(prototype )是否在左侧对象的原型链上面

function myInstanceof(left, right) {
  if (typeof obj !== 'object' || obj === null) return false
  
  let leftProp = left.__proto__;
  let rightProp = right.prototype;

  // 一直会执行循环  直到函数return
  while (true) {
    // 遍历到了原型链最顶层
    if (leftProp === null) return false;
    if (leftProp === rightProp) return true;
    
    // 遍历赋值__proto__做对比
    leftProp = leftProp.__proto__;
  }
}

实现 Promise.retry 函数

// 函数执行失败后可重试
Promise.retry = (fn, times) => {
    return new Promise(async (resolve, reject) => {
        while (times--) {
            try {
                const res = await fn();
                resolve(res);
                break;
            } catch (err) {
                if (!times) reject(err)
            }
        }
    })
}

实现深度拷贝

// 简易版实现
function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
    if (typeof obj !== "object") return obj;
    // 是对象的话就要进行深拷贝
    if (hash.get(obj)) return hash.get(obj);
    let cloneObj = new obj.constructor();
    // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
    hash.set(obj, cloneObj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 实现一个递归拷贝
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }
    return cloneObj;
}