JS |手写 call / apply / bind (简易版)

308 阅读2分钟

手写 call

原生 call

我们首先来看看 call 方法实现了什么

var foo = {
  name: 'chou',
};

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

sayName.call(foo); // chou

不难看出

  • call 改变了 this 的指向,指向到 foo 对象
  • sayName 方法执行了
  • call 是可以被所有的函数继承的,所以 call 方法应该被定义在 Function.prototype 上

实现

var foo = {
  name: 'chou',
}

function sayName() {
  console.log(this.name)
}

// ------------------------
// 改造foo对象
var foo = {
  name: 'chou',
  sayName() {
    console.log(this.name)
  }
}

foo.sayName() // chou

所以 如果我们

  1. 将函数设置为对象的属性
  2. 执行该函数
  3. 删除该函数

是不是就可以模拟 call 了呢

好啦 动手实现吧

Function.prototype._call = function (context) {
  // this 就是 sayName 函数
  context.fn = this;
  // 执行函数
  context.fn();
  // 删除删除
  delete context.fn;
};

// ------------------------
// test

var foo = {
  name: 'chou',
};

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

sayName._call(foo); // chou

但是 call 的第二个参数可以接收一个变量

同时 如果不传递参数 默认会绑定 window

好啦 我们继续模拟

Function.prototype._call = function (context) {
  var args = [];
  // 从第二个参数开始 因为第一个存放的是我们的函数
  for (let i = 1; i < arguments.length; i++) {
    args.push(arguments[i]);
  }
  // 不传递参数的时候 默认是window
  context = context || window;
  context.fn = this;
  context.fn(...args);
  delete context.fn;
};

// ------------------------
// test

var foo = {
  name: 'chou',
};

function sayName(...msg) {
  console.log(this.name, ...msg);
}

sayName._call(foo, 'yep', '🤨'); // chou yep 🤨

手写 apply

思路

apply 和 call 的区别在于 apply 的参数接收一个数组参数 而 call 则是参数列表

我们可以取 argument 的第一个元素

因为如果正确的传递 则 argument 的第一个元素应该是我们想要的 那个作为参数的数组

我们可以对这个值进行校验

  • 如果不是数组 则抛出异常
  • 如果不传 则默认只调用该方法
  • 如果是数组 则按正确的处理

实现

Function.prototype._apply = function (context) {
  // 获取参数
  var args = arguments[1];
  // 如果不传参数 默认是window调用
  context = context || window;
  // 绑定方法
  context.fn = this;
  if (!args) {
    // 没有参数 就直接调用该方法
    context.fn();
  } else {
    if (args instanceof Array) {
      // 有参数 调用方法 并传递参数
      context.fn(...args);
    } else {
      try {
        // 参数不是数组 报错
        throw new Error('CreateListFromArrayLike called on non-object');
      } catch (e) {
        console.log(e);
      }
    }
  }
  // 删除方法
  delete context.fn;
};

手写 bind

第一版

我们借用 apply 或 call 返回一个新的函数即可

Function.prototype._bind = function (context) {
  var self = this;
  return function () {
    return self.apply(context);
  };
};

第二版

我们模拟 bind 调用中可以传递参数的形式

Function.prototype._bind = function (context) {
  var self = this;
  // 获取第二个参数到最后一个参数 第一个为函数 我们需要传入的变量
  var args = Array.prototype.slice.call(arguments, 1);

  return function () {
    // 这个时候的arguments是指bind返回的函数传入的参数
    var bindArgs = Array.prototype.slice.call(arguments);
    // 用concat合并两次的参数
    return self.apply(context, args.concat(bindArgs));
  };
};