手写代码(1) call apply bind 数据类型判断 Object.create new 防抖节流 柯里化

手写代码(0) 开始手写代码之前,需要知道什么

call,apply和bind

介绍

call,apply,bind都是为了改变函数中this的指向,可以让任意对象调用任意函数,等同于将函数作为了对象的属性,可以通过对象.属性的方式调用函数,第一个参数都是this要指向的对象,如果没有这个参数或参数为undefined或null,默认this指向全局window

区别:

  • call接受第一个参数是this的指向,后面传入的是参数列表;apply接受两个参数,第一个是this的指向,第二个参数是函数接受的参数,以数组的形式传入;bind接受第一个参数是this的指向,后面传入的是参数列表(可以分多次传入)
  • call和apply在改变this指向的同时会立即执行函数返回结果,是一次性的;bind不会立即执行函数,而是返回一个永久改变this指向的函数,并且此后this的指向无法再通过call,apply,bind改变

call 函数

实现流程
  1. 判断调用对象是否为函数,即使是定义在函数的原型上,也可能出现用call等方式调用的情况
  2. 判断传入上下文对象是否存在,不存在则设置为window
  3. 处理传入的参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
代码
Function.prototype.myCall = function(context) {
  if (typeof this !== "function") {
    console.error("type error");
  }
  let args = [...arguments].slice(1);//将arguments每个元素取出来再放到数组里,截取参数
  let result = null;
  context = context || window;
  context.fn = this;//普通函数中this指向函数调用者
  result = context.fn(...args);//将需要调用的函数作为对象上的一个属性,通过context.fn()的方式来调用
  delete context.fn;
  return result;
};

在这里插入图片描述

apply 函数

实现流程
  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性
  7. 返回结果
代码
Function.prototype.myApply = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  context = context || window;
  context.fn = this;
  if (arguments[1]) {//存在函数接受的参数数组
    result = context.fn(...arguments[1]);//从类数组中取出,传参
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
};

在这里插入图片描述

bind 函数

实现流程
  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的this给apply 调用,其余情况都传入指定的上下文对象。
代码
Function.prototype.myBind = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  var args = [...arguments].slice(1),
      fn = this;
  return function Fn() {
    //因为bind返回的是函数,如果它被new Fn(),即Fn作为了构造函数,Fn的实例就是当前函数的this
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

在这里插入图片描述

this从objNum指向了test:

在这里插入图片描述

instanceof 方法

介绍

判断对象类型(引用数据类型),内部机制是判断构造函数的prototype属性是否出现在对象的原型链中的任何位置

基本数据类型在刚创建时会创建对应的包装类型对象(声明周期只有一瞬),基本数据类型不是对象 ,所以instanceof无法给出正确结果

typeof null返回object,因为js在底层存储变量时,会在变量的机器码的低位1-3位存储其类型信息:

机器码类型信息
000对象
1整数
010浮点数
100字符串
110布尔

null的所有机器码均为0,所以被当成对象。但null不具有任何对象的特性,也没有__proto__属性,所以instanceof无法正确判断null

实现流程

  1. 检验需要判断的是否是对象
  2. 获取类型的原型
  3. 获取需要判断的对象的原型
  4. 循环判断对象的原型是否和类型的原型指向一个地方,直到对象原型到达原型链终点null

代码

function myInstanceof(left, right) {//left是需要判断的对象,right是构造函数
  if(typeof left !== 'object' || left === null) return false;//判断left是不是对象
  let proto = Object.getPrototypeOf(left), // 获取对象的原型,相当于left. __proto__
      prototype = right.prototype; // 获取构造函数的 prototype 对象
  while (true) {// 判断构造函数的 prototype 对象是否在对象的原型链上
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);//沿着原型链找
  }
}

在这里插入图片描述

类型判断函数

介绍

  1. 数组、对象、null都会被typeof判断为object
  2. Object.prototype.toString.call()可以正确判断基本类型和原生引用类型,不能准确判断自定义类型
  3. toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法

实现流程

  1. 判断是否等于null
  2. 用typeof判断是否是基本类型,是则返回typeof判断的结果
  3. 引用类型用Object原型上的toString方法判断,截取类型再返回

代码

function getType(value) {
  if (value === null) {
    return value + "";
  }
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split(" ")[1].split("");//分割字符串,返回数组
    type.pop();
    return type.join("").toLowerCase();
  } else {
    return typeof value;
  }
}

在这里插入图片描述

new 操作符

介绍

new的作用是通过构造函数来创建一个实例对象,这个实例对象可以访问到构造函数原型上的属性

实现流程

  1. 创建一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象
  3. 让函数的this指向这个对象,执行构造函数的代码(给这个新对象添加属性)
  4. 判断函数返回值类型,如果是值类型,返回创建的对象。如果是引用类型,返回这个引用类型的对象

代码

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);//删除第一个参数并返回这个参数
  let result = null;
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  //新建原型为构造函数的prototype对象的空对象
  newObject = Object.create(constructor.prototype);
  //给newObject增加constructor"属性"并调用,相当于newObject.constructor(arguments)
  //arguments在上面已经被删除了第一个参数,剩下传参部分
  result = constructor.apply(newObject, arguments);
  //如果构造函数没有显式返回一个对象,则使用新建的空对象,否则返回指定对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  return flag ? result : newObject;
}

在这里插入图片描述

Object.create

介绍

Object.create(proto,[propertiesObject])

proto:新创建对象的原型对象,如果不是null或非原始包装对象,则抛出异常

prop:可选,也是一个对象,用来设置新创建对象的自有可枚举属性

返回一个新对象,带着指定的原型对象和属性

实现流程

  1. 判断第一个参数的类型是否是object或null
  2. 创建新函数,新函数的原型指向第一个参数
  3. 创建新函数的实例
  4. 如果第二个参数不是undefined,将属性加入实例

代码

function create(proto, propertiesObject = undefined){ 
  if(!(obj instanceof Object) && proto !== null) {
      throw Error('Error');
  }  
  function F(){}
  F.prototype = proto;
  let obj = new F();
  if(propertiesObject !== undefined) {
      Object.defineProperties(obj, propertiesObject); 
  } 
  return obj;
}

在这里插入图片描述

在这里插入图片描述

防抖函数

介绍

防抖是指当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定时间到来之前,又触发了事件,就重新开始计时。可以通俗理解为:防止用户频繁触发一个事件,只执行该事件的最后一次

乘公交车,一直有人陆陆续续上车(事件触发),司机心想30s(时间间隔) 内没人继续上,再开车(函数执行)。

实现流程

  1. 闭包内设置定时器(指向定时器的变量不被垃圾回收机制回收)
  2. 在规定时间内再次触发,清除上次调用的计时器,重新设置定时器计时

代码

function debounce(fn, wait) {
  let timer = null;
  return function() {//存在闭包,实际上debounce只会执行一次,之后执行的都是这个返回函数
    let context = this,
        args = arguments;
    if (timer) {//在规定时间内再次触发,重新计时
      clearTimeout(timer);
      timer = null;
    }
    //设置定时器,使事件间隔指定时间后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

在这里插入图片描述

节流函数

介绍

节流是指动作绑定事件后,动作触发事件,在这段事件内,如果动作又发生,则无视该动作,直到事件执行完后才能重新触发。可以通俗理解为:在指定的事件间隔内,只允许该函数执行一次

在公交车总站乘坐公交车,不管车上是否有人,不管是否有人上车,若规定了10分钟发一次车(时间间隔),那么只有在这辆车发车之后下辆车才会发车。

实现流程

  1. 设置时间戳(闭包会保存curTime时间戳,因为变量被返回的函数引用,所以无法被垃圾回收机制回收)
  2. 当触发事件的时候,取出当前的时间戳nowTime,减去之前的时间戳curTime
  3. 结果大于设置的时间周期,则执行函数,然后更新时间戳为当前时间戳:curTime = Date.now();
  4. 结果小于设置的时间周期,则不执行函数

代码

function throttle(fn, delay) {
  let curTime = Date.now();
  return function() {//存在闭包,实际上throttle只会执行一次,之后执行的都是这个返回函数
    let context = this,
        args = arguments,
        nowTime = Date.now();
    //如果两次时间间隔超过了指定时间,则执行函数,否则忽略
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

在这里插入图片描述

柯里化

介绍

柯里化又称部分求值。一个柯里化的函数会首先接收一些参数,但不会立即求值,而是继续返回另一个函数,之前传入的参数在函数形成的闭包中被保存起来,等到函数真正被需要求值时,之前传入的所有参数都会被一次性用于求值

实现流程

接收函数和参数,判断当前传入的参数数量是否已经满足函数所需要参数数量,如果满足,执行函数,否则递归返回柯里化的函数等待参数的传入

代码

function curry(fn, ...args) {
    return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

在这里插入图片描述

可以注意到,当参数数量满足函数所需数量时它就自动执行了,但如果我们想控制它的执行时间怎么办

实现可控制的执行时间的柯里化函数:参数足够时并不会执行,只有再调用一次函数它才会执行并返回结果

let newCurry = (fn,...params)=> {
	let args = params || [];
	let fnLen = fn.length;//所需参数数量
	return (...res)=> {
		let allArgs = args.slice(0);
		allArgs.push(...res);
                //如果输入了参数(所以即使传入参数数量够,也不会执行函数,只有()才会执行函数)或当前参数数量还不够,递归返回柯里化的函数,等待下次参数传入
		if(res.length > 0 || allArgs.length < fnLen){
		   return newCurry.call(this,fn,...allArgs);
		}else{
		  return fn.apply(this,allArgs);
		}
	}
}

在这里插入图片描述

应用场景

  • 参数复用,减少重复参数的传递
    在这里插入图片描述

比如说求四个数字之和,已经确定一个数字是1,那么只要再传入其它三个数字就行了,不用每次都再传入一次1

  • 延时执行
    像上面写的一样,参数数量满足要求,但并不想立即执行
  • 提前确认
//兼容性检测
const whichEvent = ( function () {
    // 优先判断addEventListener
    if(window.addEventListener){
        return function(element,type,listener,useCapture){
            element.addEventListener(type,function(e){
                listener.call(element,e);
            },useCapture);
        }
    // 判断IE 
    }else if(window.attchEvent){
        return function(element,type,handler){
            element.attchEvent('on'+element,function(e){
                handler.call(element,e);
            });
        }
    }
} )();

总结

学习手写代码的过程中,用到了很多平常不怎么接触的知识,也对底层原理有了一些认识,更理解到了:要想学好一个知识点,就去用它,这个过程会产生很多光是看代码想不到的问题,解决了这些问题会对这个知识点了解得更加透彻。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值