【重学JS之路】call、apply和bind

358 阅读6分钟

call、apply和bind都是对this值得改变,那三者有什么不同呢?本篇通过模拟三者代码的形式来讲解。

1、call

W3school中对call()方法的定义是:它可以用来调用所有者对象作为参数的方法。通过 call(),能够使用属于另一个对象的方法。 我们举个例子:

var foo = {
  name: 'Jack'
}
function bar() {
  console.log(this.name);
}
bar.call(foo);

由上可以看出call()函数主要做了两件事,第一个是改变this的指向,将bar中的this指向了foo,第二个是执行了bar方法。 接下来我们来模拟call方法: 第一步:实现简单的call方法: 上面的例子可以改造为:

var foo = {
  name: 'Jack',
  bar: function() {
    console.log(this.name);
  }
}
foo.bar();

输出的结果和上述的一模一样,唯一的不同就是foo中多了一个bar的属性,那我们执行完后删除这个属性就可以了。

Function.prototype.myCall = function(Context) {
  Context.fn = this; // 此时的this指向的是Function,即调用者
  Context.fn();
  delete Context.fn;
}
var foo = {
  name: 'Jack'
}
function bar() {
  console.log(this.name);
}
bar.myCall(foo);

结果和call一致。 第二步:因为call可以带参数,所以我们接下来实现这个 看一下原版的:

var foo = {
  name: 'Jack'
}
function bar(age, goods) {
  console.log(this.name);
  console.log(age); // 10
  console.log(goods); // 自行车
}
bar.call(foo, 10, '自行车');

因为参数是不固定的,所以可以想到arguments。怎么将arguments传入对象中,这就是个难题,高兴的是ES6给了我们方法:解构赋值

Function.prototype.myCall = function(Context) {
  Context.fn = this; // 此时的this指向的是Function,即调用者
  var args = [...arguments].slice(1);
  Context.fn(...args);
  delete Context.fn;
}
var foo = {
  name: 'Jack'
}
function bar(age, goods) {
  console.log(this.name);
  console.log(age); // 10
  console.log(goods); // 自行车
}
bar.myCall(foo, 10, '自行车');

是不是感觉特别简单,但是call是ES3的方法,解构赋值是ES6的方法,感觉有点欺负它,那我们就需要重新想一个。这个方法也是查看了资料后才找到的,自己能力还是有限啊! 可以运用eval(),eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

Function.prototype.myCall = function(Context) {
  Context.fn = this; // 此时的this指向的是Function,即调用者
  var args = [];
  for (var i = 1; i < arguments.length; i++>) {
    args.push('arguments[+' i '+]');
  }
  eval('Context.fn(+' args '+)');
  delete Context.fn;
}
var foo = {
  name: 'Jack'
}
function bar(age, goods) {
  console.log(this.name);
  console.log(age); // 10
  console.log(goods); // 自行车
}
bar.myCall(foo, 10, '自行车');

感觉很完美了,但是依旧有两小点需要注意:

  1. 当this为null时
  2. 当函数有返回值时 我们先来看第一点:
var name = 'Jack';
function bar() {
  console.log(this.name); // Jack
}
bar.call(null)

当this为null时,默认走向window 第二点:

var foo = {
  name: 'Jack'
}
function bar() {
  return {
    age: 1,
    goods: '自行车'
  }
}
bar.call(foo); // {age: 1, goods: "自行车"}

当函数有返回值时,结果就是这个返回值。 所以我们对自己模拟的方法进行最后的优化:

Function.prototype.myCall = function(Context) {
  var Context = Context ? Context : window;
  Context.fn = this; // 此时的this指向的是Function,即调用者
  var args = [];
  for (var i = 1; i < arguments.length; i++) {
    args.push('arguments['+ i +']');
  }
  var result = eval('Context.fn('+ args +')');
  delete Context.fn;
  return result;
}

2、apply

call() 和 apply() 之间的区别,不同之处是: call() 方法分别接受参数。 apply() 方法接受数组形式的参数。 所以call和apply只是接受参数的不同,思路还是和call一样,这次就不重复了直接贴代码: 运用解构赋值:

Function.prototype.myApply = function(Context) {
  var Context = Context ? Context : window;
  Context.fn = this; // 此时的this指向的是Function,即调用者
  var args = arguments[1]?arguments[1]:[];
  var result = Context.fn(...args);
  delete Context.fn;
  return result;
}

运用eval:

Function.prototype.myApply = function(Context) {
  var Context = Context ? Context : window;
  Context.fn = this; // 此时的this指向的是Function,即调用者
  var args = [];
  var arguments = arguments[1]?arguments[1]:[];
  for (var i = 0; i < arguments.length; i++) {
    args.push('arguments['+ i +']');
  }
  var result = eval('Context.fn('+ args +')');
  delete Context.fn;
  return result;
}
var foo = {
  name: 'Jack'
}
function bar() {
  console.log(this.name);
}
bar.myApply(foo);

3、bind

bind() 方法会创建一个新函数,当这个新函数被调用时,它的this值是传递给bind()的第一个参数, 它的参数是bind()的其他参数和其原本的参数。 所以bind执行两个步骤:

  1. 返回一个新函数
  2. 可以传入参数 我们按照分析call()函数一样的思路来分析bind(),首先看一下原先的bind:
var foo = {
  name: 'Jack'
}
function bar() {
  console.log(this.name);
}
var bindFoo = bar.bind(foo);
bindFoo(); // Jack

所以可以看出bar.bind(foo);返回一个新的函数,当这个函数执行时,才返回其中的结果,那我们先模拟一下这个:

Function.prototype.myBind = function(Context) {
  var self = this;
  return function() {
    return self.apply(Context);
  }
}
var foo = {
  name: 'Jack'
}
function bar() {
  console.log(this.name);
}
var bindFoo = bar.myBind(foo);
bindFoo(); // Jack

结果一样,说明这一步模拟的没有问题。那我们进行下一步的模拟,因为bind也是可以携带参数的,携带参数的方式和call相同:

var foo = {
  name: 'Jack'
}
function bar(age, goods) {
  console.log(this.name);
  console.log(age);
  console.log(goods);
}
var bindFoo = bar.bind(foo, 1, '1');
bindFoo(); // Jack 1 1

模拟板升级:

Function.prototype.myBind = function(Context) {
  var self = this;
  var args = [].slice.call(arguments, 1);
  return function() {
    return self.apply(Context, args);
  }
}

很完美,但是bind有个特点被忽视了,因为bind返回一个新的函数,那我们将返回的函数里面传参,会有什么效果,我们看一下:

var foo = {
  name: 'Jack'
}
function bar(age, goods) {
  console.log(this.name);
  console.log(age);
  console.log(goods);
}
var bindFoo = bar.bind(foo, 1);
bindFoo('1'); // Jack 1 1

可以看出bind可以只传入age,然后再从返回的新函数中传入goods,那我们需要把上述模拟进行升级:

Function.prototype.myBind = function(Context) {
  var self = this;
  var args = [].slice.call(arguments, 1);
  return function() {
    var bindArgs = [].slice.call(arguments);
    return self.apply(Context, args.concat(bindArgs));
  }
}

思路其实很简单,就是将两个arguments进行合并。 本以为这样就结束了,但是MDN提到了bind的另外一个特点:绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。什么意思呢?用代码演示一下:

var foo = {
  name: 'Jack'
}
function bar(age, goods) {
  console.log(this.name);
  console.log(age);
  console.log(goods);
}
bar.prototype.friends = 'a';
var BindFoo = bar.bind(foo, 1);
var bindFoo = new BindFoo('1'); // undefined 1 1
console.log(bindFoo.friends); // a

this.name竟然输出undefined,那是因为new后,BindFoo中this的指向改变了,指向了bindFoo,而BindFoo实际是bar函数,并且bindFoo没有value属性,所以就输出了undefined,通过instanceof就可以看出来,bindFoo是BindFoo的实例,也是bar的实例。

Function.prototype.myBind = function(Context) {
  var self = this;
  var args = [].slice.call(arguments, 1);
  var cacheFn = function() {};
  var bindFun = function() {
    var bindArgs = [].slice.call(arguments);
    return self.apply(this instanceof cacheFn? this : Context, args.concat(bindArgs));
  }
  cacheFn.prototype = this.prototype;
  bindFun.prototype = new cacheFn();
  return bindFun;
}

我们进行分步讲解: 1、为什么要判断this instanceof bindFun? 之前也说到,当将bind返回后函数当做构造函数时,bindFoo即是BindFoo的实例也是bar的实例,BindFoo即为返回来的函数,在我们模拟的代码中就是bindFun这个函数,并且当new之后this指向的是实例,所以用this instanceof bindFun判断的实际就是函数前有没有new这个关键词。 2、为什么要继承this的原型? 这是为了继承bar原型上的属性。 最后一步,健壮模拟的bind,判断传过来的this是否为函数,也是最终版:

Function.prototype.myBind = function(Context) {
  if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }
  var self = this;
  var args = [].slice.call(arguments, 1);
  var cacheFn = function() {};
  var bindFun = function() {
    var bindArgs = [].slice.call(arguments);
    return self.apply(this instanceof cacheFn? this : Context, args.concat(bindArgs));
  }
  cacheFn.prototype = this.prototype;
  bindFun.prototype = new cacheFn();
  return bindFun;
}