call 和 apply

callapplyFunction 构造函数原型对象上的方法,所有的函数都可以调用 callapply,作用是可以改变调用 callapply 函数内部的 this 指向,并执行函数。

call 的使用方法

/* 不指定 this */
function fn() {
  console.log(this, arguments);
}

fn.call();

// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
// Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]

不指定替换的 this,则调用 call 的函数在运行时决定 this 指向,当前案例中在浏览器中运行,则指向 window 对象。

/* 一次调用 call */
function fn() {
  console.log(this, arguments);
}

fn.call('hello', '1', '2');

// String { "hello" }
// Arguments(2) ["1", "2", callee: ƒ, Symbol(Symbol.iterator): ƒ]

在上面案例中,callfn 内部的 this 更改为 hello 的基本包装类(对象),而 12 作为 fn 的参数,以 arguments 的形式被打印出来。

/* 多次调用 call */
function fn1() {
  console.log(this, arguments);
}

function fn2() {
  console.log(this, arguments);
}

fn1.call.call(fn2, '1', '2');

// String { "1" }
// Arguments ["2", callee: ƒ, Symbol(Symbol.iterator): ƒ]

由于 call 是函数原型的方法,当然也可以被 call 自己调用,在上面的案例中,第一个 call 内部的 this 为调用者 fn1,通过第二个 call 将第一个 call 内部的 thisfn1 更改为 fn212 作为参数传递给第一个 call,而 1 又作为第一个 call 内部 this 指向的 fn2 内部的 this2 作为 fn2 的参数,最后执行 fn2,固有上面执行结果。

call 的实现原理

根据 call 方法的特点,来模拟实现一个自己封装的 call 方法,代码如下。

/* call 的实现原理 */
// context 参数为要替换的 this
Function.prototype.call = function (context) {
  // 将传入的 this 转换成对象,若没传则使用 window 作为 this
  context = context ? Object(context) : window;

  // 将调用 call 的函数作为属性赋值给传入的 this
  context.fn = this;

  var args = [];

  // 将传递给调用 call 函数的参数转化成字符串取值的形式
  for (var i = 1; i < arguments.length; i++) {
    // args ['arguments[1]', 'arguments[2]']
    args.push('arguments[' + i + ']');
  }

  // 利用 eval 执行 context.fn,并利用数组转换字符串的 toString 去掉 [ ]
  var result = eval('context.fn(' + args + ')');

  // 删除 context 上多余的 fn 属性
  delete context.fn;

  return result;
}

上面的实现方式重点解决两个问题:

  • 如何让调用 call 函数内部的 this 指向传入的 this,我们通过将传入 this 上加一个属性 fn,值为调用 call 的函数,在执行函数时并不直接调用 this,而是执行 context.fn,这样内部的 this 指向了调用者 context,即指向了传入要替换的 this
  • 如何将 call 调用时除第一个参数以外的参数列表作为调用 call 函数的参数依次传入,我们这里借用了 eval 提供执行环境,将要执行的代码拼接成字符串,这样就可以容易的将 argument 第二项后面的所有项通过循环的方式拼接。

context 添加的多余属性 fn,要在函数 context.fn 执行后删除。

apply 的使用方法

applycall 的作用相同,基本用法如下。

/* apply 用法 */
function fn() {
  console.log(this, arguments);
}

fn.apply('hello', ['1', '2']);

// String { "hello" }
// Arguments(2) ["1", "2", callee: ƒ, Symbol(Symbol.iterator): ƒ]

apply 的实现原理

通过用法可以看出 applycall 唯一不同的就是传参方式,call 传递给调用它的函数传参靠调用时使用参数列表的方式依次传入,而 apply 是通过数组的方式传入,只需要将 call 的代码稍加改造就可以实现 apply

/* apply 的实现原理 */
// context 参数为要替换的 this,args 为调用 apply 函数执行的参数
Function.prototype.apply = function (context, args) {
  context = context ? Object(context) : window;
  context.fn = this;
  var result;

  // 判断是否传入参数列表,如果没传则直接执行
  if (!args) {
    result = context.fn();
  } else {
    result = eval('context.fn(' + args + ')');
  }

  delete context.fn;

  return result;
}

bind

bind 的使用方法

bind 函数是 Function 原型对象上的方法,bind 的作用是可以将调用它的函数内部的 this 绑定成所指定的 this,第一个参数为指定的 this,与 callapply 不同的是,调用 bind 的函数并不会执行,而是返回一个新的函数,新的函数调用时传入的参数会和 bind 调用时传入的除第一个以外的参数进行合并,并作为调用 bind 的函数执行的参数,下面是 bind 的基本用法。

/* 返回的函数当做普通函数调用 */
var obj = {
  name: 'Shen'
};

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

var bindFn = sayName.bind(obj);
bindFn();
// Shen
/* 调用 bind 和执行返回函数分开传参 */
var obj = {
  name: 'Shen'
};

function animal(name, age) {
  console.log(this.name + ' have a ' + name + ' is ' + age + ' years old.');
}

var bindFn = animal.bind(null, 'cat');
bindFn(2);
// Shen have a cat is 2 years old.
/* 返回的函数当做构造函数执行 */
var obj = {
  name: 'Shen'
};

function Animal(name, age) {
  this.name = name;
  this.age = age;
}

// 动物类别为哺乳类
Animal.prototype.category = 'mammalia';

var BindFn = Animal.bind(obj, 'cat');
var cat = new BindFn(2);

console.log(cat); // Animal {name: "cat", age: 2}
console.log(cat.category); // mammalia

bind 的原理

从上面的例子已经可以看出 bind 不但能绑定 this,收集参数,返回的函数既可以直接调用,又可以作为构造函数实例化对象,而实例化的对象的方式,bind 绑定的 this 不生效,this 指向被创建的实例,实例依然可以找到原来函数原型上的属性和方法,根据 bind 的特性,模拟实现的代码如下。

/* bind 的实现原理 */
// context 参数为要绑定的 this
Function.prototype.bind = function (context) {
  // this 为调用 bind 的函数
  var self = this;

  // 收集除了 context 以外所有的参数
  var bindArgs = Array.prototype.slice.call(arguments, 1);

  // 返回的新函数 fBound
  function fBound() {
    // 收集 fBound 的参数
    var args = Array.prototype.slice.call(arguments);

    // 执行调用 bind 的函数
    // 若是普通函数调用,this 为 context,若是作为构造函数则 this 为实例
    self.apply(this instanceof fBound ? this : context, bindArgs.concat(args));
  }

  // 用来继承的中间函数
  function fNOP() {}

  // 作为构造函数调用 fBound 时,为了实例可以找到调用 bind 函数的原型对象,进行继承
  if (this.prototype) {
    // Function.prototype 为函数,可以调用 bind,当时没有原型对象,所以要判断
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
  }

  return fBound;
}

在上面代码中,如果调用 bind 返回函数作为构造函数使用,则需要通过继承找回原函数的属性和方法,但是有一个特例,就是 Function.prototype,类型为函数,却没有 prototype 属性,所以需要判断。

最后

上面就是 callapplybind 的基本用法、实现原理以及区别,希望读者可以通过这篇文章加深对 callapplybind 的印象,运用自如。