解析call、apply、bind

1,112 阅读5分钟

1. 前言

callapplybind在日常开发中时常用到,它们的作用都是改变this指向,或者说是借用方法。而本篇将重点介绍它们的用法、区别以及适用场景。

2. 基本语法

func.call(thisArg, param1, param2 ... )
func.apply(thisArg, [param1, param2, ...])
func.bind(thisArg, param1, param2)

返回值:

call/apply:返回func的执行结果;

bind:返回func的拷贝(即返回一个新的函数),同时拥有指定的this和参数;

参数:

  • thisArg(可选):

    1. functhis将指向thisArg
    2. 非严格模式下若thisArg指定为null或是undefined,则functhis指向window对象
    3. 严格模式下若thisArg指定为null或是undefined,则functhis指向undefined
  • param(可选):

    1. 如果不传该参数或是传入nullundefined,则表示不传入任何参数;
    2. apply的第二个参数为类数组对象,传入func的参数是数组内各项的值;

3. 三者的区别

3.1 call与apply

callapply本质上来说并没有什么区别,作用都是一致的,它们的差别体现在传入给func的参数不同call是多次传入要使用的参数apply传入的是一个包含要使用参数的类数组。这两个方法极易弄混,可使用双A记忆法进行记忆,即Apply传入Array,都以A作为开头。当参数数量或顺序确定时建议使用call,参数数量或顺序不确定的情况建议使用apply。另外当参数数量特别多时为了代码可读行还是建议将这些参数整合成一个数组来使用apply

3.2 call/apply与bind

call/applybind的区别体现在它们的返回值不同callapply返回值是func的执行结果,在改变this指向后立即执行函数

bind返回值是func的拷贝,在改变this指向后不会立即执行函数,即需要自行调用得到的这个新函数。

4. 使用场景

4.1 判断数据类型

// typeof(value) 在碰到诸如object、array、null、new String、new Boolean等情况时返回的都是object,这并不利于数据类型的判断,所以可以通过Object.prototype.toString.call(value)对其进行重写
function typeof(value){
    const typeObj = {
        '[Object object]':'object',
        '[Object Array]':'array'.
        '[Object String]':'string',
        '[Object Number]':'number',
        '[Object Boolean]':"boolean",
        '[Object null]':'null',
        '[Object undefined]':'undefined',
        "[object Function]": "function",
    	"[object Date]": "date", 
    	"[object RegExp]": "regExp",
    	"[object Map]": "map",
    	"[object Set]": "set",
    	"[object HTMLDivElement]": "dom", 
    	"[object WeakMap]": "weakMap",
        "[object Window]": "window", 
    	"[object Error]": "error", 
    	"[object Arguments]": "arguments"
    }
    return typeObj[Object.prototype.toString.call(value)];
}

4.2 类数组对象借用数组方法

let domNodes = Array.prototype.slice.call(document.getElementByTagName('div'));
// 这样类数组就可以借用数组类型的slice方法了!

5. 手写call、apply、bind

手写callapplybind一直都是做为面试的高频题目,接下来我们一起来将这个三个函数的实现过程过一遍

5.1 实现call

/**
 * 用原生JavaScript实现call
 */
Function.prototype.myCall = function(thisArg, ...arr) {

  //1.判断参数合法性
  if (thisArg === null || thisArg === undefined) {
    //指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    thisArg = window;
  } else {
    thisArg = Object(thisArg);//创建一个可包含数字/字符串/布尔值的对象,
                              //thisArg 会指向一个包含该原始值的对象。
  }

  //2.搞定this的指向
  const specialMethod = Symbol("anything"); //创建一个不重复的常量
  //如果调用myCall的函数名是func,也即以func.myCall()形式调用;
  //根据上篇文章介绍,则myCall函数体内的this指向func
  thisArg[specialMethod] = this; //给thisArg对象建一个临时属性来储存this(也即func函数)
  //进一步地,根据上篇文章介绍,func作为thisArg对象的一个方法被调用,那么func中的this便
  //指向thisArg对象。由此,巧妙地完成将this隐式地指向到thisArg!
  let result = thisArg[specialMethod](...arr);

  //3.收尾
  delete thisArg[specialMethod]; //删除临时方法
  return result; //返回临时方法的执行结果
};

let obj = {
  name: "coffe1891"
};

function func() {
  console.log(this.name);
}

func.myCall(obj);//>> coffe1891

5.2 实现apply

/**
 * 用原生JavaScript实现apply
 */
Function.prototype.myApply = function(thisArg) {
  if (thisArg === null || thisArg === undefined) {
    thisArg = window;
  } else {
    thisArg = Object(thisArg);
  }

  //判断是否为【类数组对象】
  function isArrayLike(o) {
    if (
      o && // o不是null、undefined等
      typeof o === "object" && // o是对象
      isFinite(o.length) && // o.length是有限数值
      o.length >= 0 && // o.length为非负值
      o.length === Math.floor(o.length) && // o.length是整数
      o.length < 4294967296
    )
      // o.length < 2^32
      return true;
    else return false;
  }

  const specialMethod = Symbol("anything");
  thisArg[specialMethod] = this;

  let args = arguments[1]; // 获取参数数组
  let result;

  // 处理传进来的第二个参数
  if (args) {
    // 是否传递第二个参数
    if (!Array.isArray(args) && !isArrayLike(args)) {
      throw new TypeError(
        "第二个参数既不为数组,也不为类数组对象。抛出错误"
      );
    } else {
      args = Array.from(args); // 转为数组
      result = thisArg[specialMethod](...args); // 执行函数并展开数组,传递函数参数
    }
  } else {
    result = thisArg[specialMethod]();
  }

  delete thisArg[specialMethod];
  return result; // 返回函数执行结果
};

5.3 实现bind

/**
 * 用原生JavaScript实现bind
 */
Function.prototype.myBind = function(objThis, ...params) {
  const thisFn = this;//存储调用函数,以及上方的params(函数参数)
  //对返回的函数 secondParams 二次传参
  let funcForBind = function(...secondParams) {
    //检查this是否是funcForBind的实例?也就是检查funcForBind是否通过new调用
    const isNew = this instanceof funcForBind;

    //new调用就绑定到this上,否则就绑定到传入的objThis上
    const thisArg = isNew ? this : Object(objThis);

    //用call执行调用函数,绑定this的指向,并传递参数。返回执行结果
    return thisFn.call(thisArg, ...params, ...secondParams);
  };

  //复制调用函数的prototype给funcForBind
  funcForBind.prototype = Object.create(thisFn.prototype);
  return funcForBind;//返回拷贝的函数
};

写在最后:本篇是笔者对于call、bind和apply的一次归纳总结,这三个函数在JS开发过程中使用频率很高,希望小伙伴们能够熟练掌握。

如果这一篇文章你觉得不错或是对你有所帮助的话,可以给笔者一个赞吗?你小小的一个赞便是对笔者莫大的鼓励😊,同时欢迎各位朋友们在评论区评论留言,如有错漏之处敬请指正,互勉💪


参考文献:

[1] MDN Function.prototype.bind

[2] MDN Function.prototype.apply

[3] MDN Function.prototype.call