call方法是javascript中很常用的一个方法,其定义是:

TIP

call方法调用一个函数,其具有一个指定的this值和分别地提供的参数

简单来说,call方法可以在指定函数this的情况下调用这个函数,其余的参数和返回值部分不受影响。举个栗子:

var a = 1;
var obj = {
  a: 2
}
function fun(name, age){
  console.log(this.a);
  return {
    name: name,
    age: age
  }
}
fun("zhangsan",18);  //打印1,返回 {name:"zhangsan", age:18}
fun.call(obj, "zhangsan", 18); //打印2,返回 {name:"zhangsan", age:18}

总结一下:

  • 函数执行了
  • this指向第一个参数
  • 其他的参数照常传递
  • 能有返回值
  • 补充一点,第一个参数为null时,this默认执行window

下面我们就根据上面的特点,一步一步来模拟实现call

还是以上面的函数 fun 和对象 obj 为例

# 第一版

先来实现前两个功能,想指定函数的this,我们可以通过对象调用的方式,也就是obj.fun(),这时候fun中的this就指向obj了,我们来实现第一版代码:

Function.prototype.myCall = function(context) {
  //首先获取函数fun,这里可以通过this来获取,obj也就是参数context啦
  context.fn = this;
  //将fn设为obj的一个方法
  context.fn();
  //莫名其妙给obj添加了一个方法总归是不好的,调用完成之后记得删掉这个方法
  delete context.fn;
}

想通过obj.fun()的方式调用,把fun设置为obj的一个方法就好啦,记得调用之后删除。下面我们来测试一下

fun.myCall(context);  //打印2

看到打印出2的时候还是很高兴的,终于走出第一步了,但是别高兴的太早了,下面还有好几步。

# 第二版

再来解决传参数和返回值的问题,参数是不固定的,但是我们有arguments对象,可以通过arguments获取参数,然后依次传给fun,来看代码

arguments对象不熟悉的可以另外查资料学习,这里就不展开啦。

Function.prototype.myCall=function(context){
  context.fn = this;
  var args = [];  //用来存储参数
  for(var i = 1,length = arguments.length;i < length;i++){
    args.push(arguments[i]);
  }
  //注意arguments的第一个参数是指定this的对象,从第二个参数开始才是传给fun的函数,所以从1开始循环
  var result = context.fn(...args); //接收返回值
  delete context.fn;
  return result;
}

调用试试:

var result = fun.myCall(obj, "zhangsan", 18);  //打印2
console.log(result);  //打印{name:"zhangsan", age:18}

又实现了,但是这里有个问题,在myCall中调用fn的时候,我们给函数穿参数的方式使用的是es6的展开运算符... ,这里考虑到兼容性,我们尽量使用老一点的方式,这里推荐使用eval函数,此时代码是这样的

Function.prototype.myCall = function(context) {
  context.fn = this;
  var args = [];
  for(var i = 1,length = arguments.length;i < length;i++){
    args.push('arguments[' + i + ']');
  }
  var result = eval('context.fn('+args+')');
  delete context.fn;
  return result;
}

# 最终版

还剩最后一个个功能,第一个参数为null时,this默认指向window,这个就很好实现啦,只需要判断一下context的值,为null时,让它默认为window,来看代码

Function.prototype.myCall = function(context) {
  var context = context || window; 
  context.fn = this;
  var args = [];
  for(var i = 1,length = arguments.length;i < length;i++){
    args.push(arguments[i]);
  }
  var result = context.fn(...args);
  delete context.fn;
  return result;
}

调用一下:

var result = fun.myCall(context,"zhangsan", 18);  //打印2
console.log(result);  //打印{name:"zhangsan",age:18}

var result = fun.myCall(null,"zhangsan", 18);  //打印1
console.log(result);  //打印{name:"zhangsan",age:18}

搞定!

# apply

apply和call功能一样,只是call传给函数的参数使用的是列表的形式,使用逗号隔开。而apply的第二个参数是参数数组,直接把参数放在数组里。

//call
fun.call(obj,arg1,arg2,arg3......)
//apply
fun.apply(obj,[arg1,arg2,arg3......])

模拟实现apply的思路和call一样,这里就不再分析一遍了,大家可以仿照思路自己试试实现,我直接贴出代码

Function.prototype.apply = function (context, arr) {
  var context = context || window;
  context.fn = this;
  var args = [];
  for (var i = 0, len = arr.length; i < len; i++) {
    args.push('arr[' + i + ']');
  }
  var result = eval('context.fn(' + args + ')')
  delete context.fn
  return result;
}