来一波手撕代码(节流、防抖、call、apply、instanceof)

286 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

防抖

什么是防抖?

(在某段时间(eg:1000ms)里再次点击同一事件,那么时间又从0开始计时(即使前一事件时间已经倒计时一半了,也会清除计时,从新开始),触发的事件永远是固定时间段(eg:1000)里面的最后一个点击事件. 现象: 设置的防抖时间为1000ms,1000ms里多次点击的话, 最后时间触发的时间有可能大于1000ms)

手撕防抖

// 防抖
const debounce = (fun, delay) => {
    let time = null;
    return (...args) => {
        clearTimeout(time);

        time = setTimeout(function() {
            fun.apply(this, args);
        }, delay)
    }
}  

节流

什么是节流?

(在某段时间(eg:1000ms)里再次多次点击同一事件,只执行此时间段内(eg:1000ms)以内的第一次点击,此时间段内后面的点击不触发. 此时间段后, 再次重复执行此逻辑. 现象: 设置的节流时间为1000ms,1000ms内多次点击, 1000ms里只执行一次事件. 区别:同一时间段内多次点击:防抖=>最后的执行事有可能大于此时间段时间,并且只执行时间段内最后一次点击.节流: 同一时间段内,只执行第一次点击, 其他点击不触发).

手撕节流

//节流
const throttle = (fun, delay = 500) => {
    let flag = true;
    return (...args) => {
        if (!flag) return;
        flag = false;
        setTimeout(() => {
            fun.apply(this, args);
            flag = true;
        }, delay)
    }
}  

call

做了什么?

将函数设为对象的属性,执⾏&删除这个函数,指定this到函数并传⼊给定参数执⾏函数,如果不传⼊参数,默认指向为 window。

手撕call

//实现一个call 
Function.prototype.myCall = function (context) {
    context.fn = this;
    console.log('context.fn', context.fn);
    let args = [];
    for (var i = 1; i < arguments.length; i++) {
        args.push(arguments[i]);
    }
    const result = context.fn(...args);
    //这里删除开始window添加的对象方法,使用完删除
    delete context.fn;
    return result;

}

//eg:
// let m = (a, b) => {
let m = function(a, b) {
    console.log('a,b', a, b);
    console.log(this.name);
}

let n = {
    name: 'baby',
    age: '15',
}
m.myCall(n, 47, '拉拉');

image.png
上面截图是之前理解的,感觉有错误,下面是重新理解;

传的第一个参数就相当于this,第一个传的除过this之外的值,传的是谁,谁就是那个要被添加函数属性的对象,传的n,对象就是n。call里面的this是调用call点前的对象,就是m,而m添加到n里面的一个函数,所以m里的this就是n(外层)。call里的参数content的值是n。
上面call里的arguments循环时从第二个开始,原因是第一个函数n而并非真正要的参数

为什么不用箭头函数:因为下面会用到arguments以及this, 箭头函数里没有this的绑定 并且没有arguments,里面的context指的是window,这的this指的是this里 ".前的对象" 也就是m,给window里的fn添加object方法对象,最后,会删除开始给window添加的对象, 最后还要返回这个对象, 所以这要重新赋一个位置。

eg中:上面是箭头函数的话 , 这里的this是window,那么this.name就是undefined,因为箭头函数里没有this绑定, 里面的this 指的是上面一层this的指向,如果是普通函数的话, this指的就是n了;把m对象应用到n里面,如果下面是:w.o.myCall指的是w里面的o对象应用到n里面。

image.png

函数m改成箭头函数后, m里面的this指向window;
上面说的,参数里只写一个content的话, 而传的参数是一个以上, 那么这个接受函数参数的content只接第一个参数,也就是n; image.png
时隔一段时间后再看的, 重新理解:

m.myCall(n, 47, '拉拉');
m的原型上的myCall方法里面的this指的是m,
这个应该是用了this指向里面的'.前的对象', 这里m是myCall点前的对象,所以myCall里的this指向m,
又是因为,myCall是m父对象原型上的方法,原型方法里的this指向的是引用的子对象;
而被添加到n对象里的m对象方法, m是个普通函数的话, 那m里面的this指向是n(this指向外层),
而m是个箭头函数的话, m里的this指向的是window(箭头函数没有this绑定,它的this和外部的this保持一致);

myCall里的content其实指的是第一个参数(也就是这里的n)(如果想所有都接受的话也可以myCall参数里面占3个位置,myCall(content,n,m)但是不建议太麻烦, 因为argumens就可以接受到所有的参数),
而arguments指的是所有参数, 因为第一个参数是n不是真正的参数, 所以, 遍历argumens时, i是从1开始的;

如下图: image.png

myCall里如果是箭头函数的话:
那么里面就不能使用arguments, 因为箭头函数没有arguments, 也没有this绑定, myCall里面的this就会和外部的this保持一致,那就是window;
所以使用的是普通函数;

如下:

image.png

apply

apply原理与call很相似,就传参的方式不同。

手撕apply

//手写apply
Function.prototype.myApply=function(context,arr){
  var contexts=Object(context)||window;
  contexts.fn=this;
  let result;
  if(!arr){
      return contexts.fn();
  }else{
      let args=[];
      for(var i=0;i<arr.length;i++){
          args.push(`arr[${i}]`);
      }
      result=eval(`contexts.fn(${args})`);
  }
    delete contexts.fn;
    return result;
}

先判断是否有值,有值的话转为对象, 否则赋值window。
eval: 是允许执行一段代码字符串; 如果你字符串里面是可执行的js 那么他就会执行这一段代码;有执行动态代码的能力\

但是eval要慎用, 因为有危险系数;
具体可以参考这篇文章:blog.csdn.net/weixin_4609…

第一个参数传n进去:

image.png
没有n的情况下,第一个参数不传n传的this,其实就是window;

image.png
问题:
下面这块这样写,虽然也是可行的;但是前提是,第二个参数也就是传递给函数的参数是个数组是可以的,
但是:
apply, 第二个参数(传递给函数的参数)可以是数组也可以是个类数组.
类数组:
结构像数组的对象,有length,但是没有数组的api.

拥有 length 属性:类数组对象通常具有一个 length等属性,表示对象中元素的个数。
按照索引访问元素:可以通过索引值从类数组对象中获取元素,就像访问数组元素一样。 没有数组原型上的方法:类数组对象不具备数组原型上的方法(如 push、pop、forEach 等),因此无法直接使用这些方法。
常见的类数组对象包括:
arguments 对象:在函数内部自动创建的对象,存储了函数调用时传递的参数。 HTMLCollection 和 NodeList:DOM 操作返回的一些对象,如:document.getElementsByTagName() 返回的对象集合。
字符串:字符串可以通过索引访问每个字符,并具有 length 属性。

刚开始我认为下面的写法是可行的 ;

Function.prototype.myApply = function (context, arr) {
    var contexts = Object(context) || window;
    contexts.fn = this;
    let result;
    if (!arr) {
        return contexts.fn();
    } else {
        //刚开始不太理解为什么要这样写,大厂题库里面的写法
        //let args = [];
        //for (var i = 0; i < arr.length; i++) {
        //    args.push(`arr[${i}]`);
        //}
       // result = eval(`contexts.fn(${args})`);


        //刚开始我的想法,觉得跟call除了参数不同,其他的差不多是一致的,所以下面这样写貌似也可行
       
        result=contexts.fn(arr);
       
    }
    delete contexts.fn;
    return result;
}

参数是数组的话,没啥问题,如下: image.png 但参数是类数组的话,就有问题了,函数接受的参数不正常, 如下:

6fdf82c887150492da1e2d157c59877.png
代码改成下面这种, 接受的参数就正常了,如下:

Function.prototype.myApply = function (context, arr) {
    var contexts = Object(context) || window;
    contexts.fn = this;
    let result;
    if (!arr) {
        return contexts.fn();
    } else {
       let args = [];
       for (var i = 0; i < arr.length; i++) {
            args.push(`arr[${i}]`);
        }
       result = eval(`contexts.fn(${args})`);
       
    }
    delete contexts.fn;
    return result;
}

479fed8028bb71cefcd3a7be714f8df.png

instanceof

什么是instanceof?

instanceof用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。

手撕instanceof

实现一个instanceof , 模拟

//实现一个instanceof, 模拟
//L代表左侧, R代表右侧
function instance_of(L,R){
    var L_proto=L.__proto__;
    var RProtoType=R.prototype;
    if(L_proto===null){
        return false;
    }
    if(L_proto===RProtoType){
        return true
    }
}
//eg:
function K(){}
var L=new K();
instance_of(L,K);

L代表左侧, R代表右侧。