模拟实现js中的 call、apply、bind实现

107 阅读3分钟

工作中我们常常会用到call、apply、bind,所以抽了些时间在网上查查它们的基本实现原理,然后按自己理解手写了一版,方便随着日后知识的积累来查阅对照和修正。

  • call(ctx, arg1, arg2, ...argn)
    function A() {}
    A.call(ctx, 1, 2);
    描述:call是挂在Function.prototype原型上的一个属性,调用call方法时函数A会立即被执行
    特性:
        1. 修改函数A中的上下文this的指向
        Function.prototype.myCall = function(ctx) {
          if (typeof this !== 'function') {
            throw new TypeError(this + 'must be function!');
          }
          function toArr(data) {
            var list = [];
            for (var i = 0; i < data.length; i++) {
              list.push(data[i]);
            }
            return list;
          }
          var context = ctx || {};
          var fn = this;
          var fnName = ['fn', Date.now(), Math.random().toString().slice(2)].join('_');
          var args = toArr(arguments).slice(1);
          let origin;
          // 如果fn上存在属性[fnName],则先暂存下来
          if (context[fnName]) {
            origin = context[fnName];
          }
          context[fnName] = fn;
          var result = eval('context.' + fnName + '(' + args.join(',') + ')');
          // 如果fn上之前存在过[fnName]属性值,则复原
          if (origin) {
            context[fnName] = origin;
          } else {
            delete context[fnName];
          }
          return result;
        }
  • apply(ctx, args)
    function A() {}
    A.apply(ctx, [1, 2]);
描述:apply是挂在Function.prototype原型上的一个属性,调用apply方法时函数A会立即被执行
特性:
     1. 修改函数A中的上下文this的指向
     2. 若要传参,则用数组包裹放在第二个参数,第二个参数类型只能是nullundefined、数组或者类数组中的一个
    Function.prototype.myApply = function(ctx) {
      if (typeof this !== 'function') {
        throw new TypeError(this + 'must be function!');
      }
      var args = arguments[1];
      if (args !== null && args !== undefined && !(args instanceof Array || 'length' in args)) {
        throw new TypeError('apply 方法调用第二个参数只能是null、undefined、数组')
      }
      function toArr(data) {
        var list = [];
        for (var i = 0; i < data.length; i++) {
          list.push(data[i]);
        }
        return list;
      }
      var context = ctx || {};
      var fn = this;
      var fnName = ['fn', Date.now(), Math.random().toString().slice(2)].join('_');
      var origin;
      // 如果fn上存在属性[fnName],则先暂存下来
      if (context[fnName]) {
        origin = context[fnName];
      }
      context[fnName] = fn;
      var result;
      if (args) {
        result = eval('context.' + fnName + '(' + toArr(args).join(',') + ')');
      } else {
        result = eval('context.' + fnName + '()');
      }
      // 如果fn上之前存在过[fnName]属性值,则复原
      if (origin) {
        context[fnName] = origin;
      } else {
        delete context[fnName];
      }
      return result;
    }
  • bind(ctx, arg1, arg2, ...argn)
     function A() {}
     var B = A.bind(ctx, 1,2);
     B();
     // new B();
    描述:bind是挂在Function.prototype原型上的一个属性,调用bind方法会创建一个新的绑定函数,它包装了原函数对象。调用绑定函数即是调用包装函数。
    特性:
        1. 函数A调用bind方法不会立即执行函数A,只会返回一个新的包装函数
        2. 返回的包装函数中的this上下文指向调用bind时传入的第一个参数,且之后再调用这个包装函数它的this上下文不会再变更(除了用 new 的方式调用包装函数)
        3. 
    Function.prototype.myBind = function(ctx) {
      // 因为bind是挂在Function.prototype上的,所以调用bind方法的对象不是函数则抛出错误提示
      if (typeof this !== 'function') {
        throw new TypeError(this + 'must be function!');
      }
      var fn = this;
      // 调用bind函数除了ctx上下文之外的参数
      var args1 = [].slice.call(arguments, 1);
      var fnBound = function() {
        // 调用包装函数(bind之后返回的函数)的参数
        var args2 = [].slice.call(arguments);
        // 两次函数调用参数合并在一起
        var finalArgs = args1.concat(args2);
        if (this instanceof fnBound) { // 通过new方式调用bind后的函数
          return fn.apply(this, finalArgs);
        }
        return fn.apply(ctx, finalArgs);
      }
      // 调用bind方法的对象必须是一个普通函数(箭头函数没有prototype属性)
      // 修改包装函数的this上下文,让它指向原函数
      if (fn.prototype) {
        var empty = function() {};
        empty.prototype = fn.prototype;
        fnBound.prototype = new empty();
        empty.prototype = null;
      }
      return fnBound;
    }