前端基础之手撕代码(一)

3,015 阅读5分钟

前端学到现在,发现有些代码平时看了千八百遍,都快要看吐了的代码关键时刻上居然写不出来了,这真的是有点难受啊。所以就在这里做一些总结,后面忘了可以及时复习。 本系列大致分为四篇:

  1. 第一篇 call、apply、bind、new、柯里化、函数组合、防抖、节流
  2. 第二篇 数组去重、数组扁平化、千分位、深拷贝、promise实现
  3. 第三篇 常用排序算法(正在努力中)
  4. 第四篇 大厂面试中经常出现的算法题(正在努力中)

1. Function.prototype.call

call方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
call方法接受的是一个参数列表。
返回值是函数执行后的结果。

Function.prototype.myCall = function(context, ...args) {
    // 设置第一个参数的默认值为window
    const ctx = context || window;
    // 设置一个唯一变量,防止覆盖原属性
    const func = Symbol('func');
    ctx[func] = this;
    const result = ctx[func](...args);
    // 执行后得到返回值之前先删除本次声明的属性
    delete ctx[func];
    return result;
}

2. Function.prototype.apply

apply方法使用一个指定的this值和单独给出的一个参数来调用一个函数。
apply方法接受的参数是一个数组。
返回值是函数执行后的结果。

// 与call的差异就体现在args这里,使用call和apply之后,原函数接收到的参数都应该是解构后的参数
Function.prototype.myApply = function(context, args) {
    // 设置第一个参数的默认值为window
    const ctx = context || window;
    // 设置一个唯一变量,防止覆盖原属性
    const func = Symbol('func');
    ctx[func] = this;
    const result = ctx[func](...args);
    // 执行后得到返回值之前先删除本次声明的属性
    delete ctx[func];
    return result;
}

3. Function.prototype.bind

bind方法接受参数,原函数也接受参数,两部分参数都需要注意保留。
需要注意的是bind方法转换后的函数可以当做构造函数使用,所以需要注意this的指向。
注意保留原函数在原型链上的属性和方法。 返回值是一个新的函数。

// 自己实现
Function.prototype.myBind = function(context, ...args) {
    const that = this;
    const funcBind = function() {
        // 当bind转换后的函数被实例化了,apply要指向函数的实例
        const target = this instanceof funcBind ? this : context;
        const applyArgs = args.concat([...arguments]);
        that.apply(target, applyArgs);
    }
    // apply不保留函数原型上的方法,这里做一层浅拷贝
    funcBind.prototype = Object.create(that.prototype);
    return funcBind;
}
// [MDN源码实现](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)
// Does not work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    var thatFunc = this, thatArg = arguments[0];
    var args = slice.call(arguments, 1);
    if (typeof thatFunc !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    return function(){
      var funcArgs = args.concat(slice.call(arguments))
      return thatFunc.apply(thatArg, funcArgs);
    };
  };
})();

4. new

创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。MDN释义

  1. 创建一个空对象
  2. 使这个新对象继承其构造函数的原型
  3. 执行构造函数并绑定新的this指向
  4. 如果该函数本身有返回对象则直接返回,否则返回新创建的对象
function myNew(func, ...args) {
    // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
    // 相当于obj.__proto__ = func.prototype
    let obj = Object.create(func.prototype);
    // args是构造方法的入参, 因为这里用myNew模拟, 所以入参从myNew传入
    let result = func.apply(obj, args);
    return Object.prototype.toString.call(result) === '[object Object]' ? result : obj
}

5. 柯里化 curry

柯里化是指将一个接受多个参数的函数转换为固定部分参数,然后返回一个接受剩余参数的函数。
function.length 属性指明函数的形参个数。

function curry(fn) {
    return function recursive(...args) {
        if (fn.length > args.length) {
            return (...rest) => recursive(...args, ...rest);
        } else {
            return fn(...args);
        }
    }
}

6. 函数组合 compose

函数组合是指一个函数执行完之后把返回的结果再作为参数赋给下一个函数来执行。
函数管道是从左往右函数执行,函数组合是从右往左执行。

function compose(...args) {
  return subArgs => {
    return args.reverse().reduce((total, func, index) => {
      return func(total);
    }, subArgs);
  }
}

7. 防抖函数 debounce

防抖是指短时间内大量触发同一事件,只会在最后一次事件完成后延迟执行一次函数。像是在用户输入用户名的过程中,需要对用户名进行重复性的校验,此时应该等待用户停止输入再校验,不然会影响用户体验。
防抖实现的原理是触发事件后设置一个定时器,在定时器延迟过程中,若再次触发事件,则重置定时器,直到没有事件触发后,定时器再触发并执行对应函数。

function debounce(func, delay = 200) {
    if (typeof func !== 'function') {
        throw Error('func必须是一个函数')
    };
    let timer = null;
    return function() {
        let args = arguments;
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(this, args)
        }, delay);
    };
};

8. 节流函数 throttle

节流是指每隔一段时间就执行一次函数。像是未拧紧的水龙头一样每隔一段时间滴一次水,即使在这段时间内水管里的水再多,水龙头也不会多滴一滴水。
节流的实现原理是触发事件后设置一个定时器,在定时器延迟过程中,即使再次触发事件,也不会改变定时器的延迟时间,直到该定时器执行完成函数后,下次触发事件才会重置定时器。

function throttle(func, delay = 200) {
    if (typeof func !== 'function') {
        throw Error('func必须是一个函数')
    };
    let lastTime = 0;
    return function() {
        let args = arguments;
        var nowTime = Date.now(); 
        if(nowTime - lastTime > delay){
            func.apply(this, args);
            lastTime = nowTime;
        };
    };
};