借尸还魂?apply、call、bind你三搁着闹腾呢

330 阅读5分钟

前言

从来没有想过一个成语——“借尸还魂”,就可以把apply、call、bind原理分析的透透的,不由感叹世间万物如此互通,相互关联,接下来让我们一起进入今日的学习。

apply、call、bind基础知识

语法

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

基本逻辑解释,说白了就是借尸还魂。我是thisArg,一个感情丰富的灵魂,缺乏肉体。想用function老哥你的肉体,但不要你的灵魂,通过借你肉体上身,使用你的肉体,输出我自己灵魂的思想。(我看到自己的这解释,我都惊了。)

调用肉体(function函数)时,涉及this绑定问题,遵循this的指向取决于函数在哪里被调用。

参数

thisArg

  • 可选;
  • function的this指向thisArg对象。特殊情况bind中,如果使用new运算符构造绑定函数,则忽略该值);
  • 非严格模式下,值为 nullundefined 时会自动替换为指向全局对象;
  • 值为原始值(数字,字符串,布尔值),this会指向该原始值的自动包装对象,如 StringNumberBoolean特殊情况(当使用 bindsetTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object)。

param

  • 可选;
  • apply 一个数组或者类数组对象,call / bind 一个指定的参数列表。

返回值

  • call / applyfun执行的结果,若该方法没有返回值,则返回 undefined
  • bind:返回fun的拷贝,并拥有指定的this值和初始参数;

作用

借用方法,改变函数执行时的this指向。

注意事项

call、apply和bind是挂在Function对象上的三个方法,只有函数才有这些方法。

区别

执行:

  • call / apply改变了函数的this上下文后马上执行该函数;
  • bind则是返回改变了上下文后的函数,不执行该函数;

返回值:

  • call / apply 返回function的执行结果;
  • bind返回function的拷贝,并指定了function的this指向,保存了function的参数;

使用选择

call,apply的效果完全一样,但是在应用选择上有一些不一样的地方:

  • 参数数量或者顺序确定时就用call,不确定就用apply;
  • 考虑参数数量,数量多就用合成数组,使用apply,反之使用call;

手写call、apply、bind

手写call

根据 “借尸还魂”理论,以下是手写call的步骤:

  1. 传参正确性判断处理(灵魂校验修正);
  2. 根据call的定义,为context设置临时属性,绑定函数this(灵魂进入肉体);
  3. 通过隐式绑定(待补充)执行函数,并传递参数。(灵魂控制肉体干活);
  4. 删除临时属性,返回函数执行结果(灵魂脱离肉体,把干活的成果交出来)
Function.prototype.myCall = function (context, ...arr) {
  // 第一步
  if(context === null || context === undefined) {
    context = window
  } else {
    // 值为基本数据类型时,this会指向该基础数据类型的实例对象
    context = Object(context)
  }
  
  // 第二步
  const specialPrototype = Symbol('特殊属性Symbol') 
  context[specialPrototype] = this;
  
  // 第三步
  const result = context[specialPrototype](...arr)
  
  // 第四步
  delete context[specialPrototype];
   return result;
}

手写apply

手写apply的思路同call差不多,不在详讲步骤。主要的不同点是处理参数数组问题,需要判断是否为类数组对象。

Function.prototype.myCall = function (context) {
  // 第一步
  if (context === null || context === undefined) {
    context = window;
  } else {
    // 值为基本数据类型时,this会指向该基础数据类型的实例对象
    context = Object(context);
  }

  // 第二步
  const specialPrototype = Symbol("特殊属性Symbol");
  context[specialPrototype] = this;

  // 第三步
  // 获取参数数组 因为默认传入是数组,所以取arguments的索引为1的值就好
  let args = arguments[1];
  const result = undefined;
  // JavaScript权威指南判断是否为类数组对象
  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;
  }
  if (args) {
    // 是否传递第二个参数
    if (!Array.isArray(args) && !isArrayLike(args)) {
      throw new TypeError("myApply 第二个参数不为数组并且不为类数组对象抛出错误");
    } else {
      // 转为数组
      args = Array.from(args); 
      // 执行函数并展开数组,传递函数参数
      result = context[specialPrototype](...args); 
    }
  } else {
    // 执行函数
    result = context[specialPrototype](); 
  }
  // 第四步
  delete context[specialPrototype];
  return result;
};

手写bind

根据bind的基本定义—-bind是返回改变了上下文后的函数,不执行该函数;套用**“借尸还魂”理论**,以下是手写bind的思路步骤:

  1. 拷贝源函数;
  2. 构造绑定函数,运用 “借尸还魂”,新函数内部return 改变了函数的this上下文后的执行结果;
  3. 返回绑定函数;

注意事项

  • 调研bind 绑定的函数时,需要注意绑定函数是否通过 new 调用
  • 如果源函数有原型(prototype),需要将绑定函数的原型指向源函数的原型,这样保证bind拷贝函数的完整性
  • 判断绑定this + 传递参数(bind绑定是传参,以及返回绑定函数后传参)。
Function.prototype.myBind = function (context, ...params) {
  // 第一步
  const that = this;

  // 第二步
  const fnToBind = function (...fnToBindParams) {
    // 判断是否通过new调用,为真则是被new调用
    const isNew = that instanceof fnToBind;
    // new调用就绑定到this上,否则就绑定到传入的context上(使用Object()包裹,可以包装原始值对象)
    const _context = isNew ? this : Object(context);
    // 用call调用源函数绑定this的指向并传递参数,返回执行结果
    return that.call(_context, ...params, ...fnToBindParams);
  };
  if (that.prototype) {
    // 复制源函数的prototype
    fnToBind.prototype = Object.create(that.prototype);
  }

  // 第三步
  return fnToBind;
};

以上就是我们今日的学习,完结撒花✿✿ヽ(°▽°)ノ✿~

参考

js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]