在模拟之前先来看一下call和apply的定义:

:::tips

  • call():调用一个函数,其具有一个指定的this值和分别提供的参数(参数列表)
  • apply():调用一个给定具体的this值,以及一个作为数组(或类数组对象)提供的参数 :::

这两个方法都是用来改变this指向的,第一个参数用来设置this指向的对象,call()方法的其他参数是一个参数列表,而apply()方法只有两个参数,第二个参数是函数的参数所组成的数组。

模拟call()实现

首先看下call()方法的调用实例:

  1. var foo = {
  2. value: 1;
  3. }
  4. function bar() {
  5. console.log(this.value);
  6. }
  7. bar.call(foo); // 1

call()方法将this的指向foo对象。并执行了bar()方法

模拟实现第一步

当bar调用call方法的时候,相当于bar方法是foo的一个局部方法。所以我们可以将foo对象改造为:

  1. var foo = {
  2. value: 1,
  3. bar: function() {
  4. console.log(this.value);
  5. }
  6. }
  7. foo.bar(); // 1

现在this的指向是指向foo了,但是给foo新增了一个属性。显然是不符号我们的需求的。那等这个方法执行完再delete这个属性不就行了。所以我们模拟的步骤分为:
1、将函数设为对象的属性
2、执行该函数
3、删除该函数
所以这个逻辑应该改造为:

  1. // 第一步
  2. foo.fn = bar
  3. // 第二步
  4. foo.fn()
  5. // 第三步
  6. delete foo.fn

fn是用来存放bar的属性名,最后都会删除的,所以这个属性名可以随意取。所以我们可以将上面的实例改造成:

  1. Function.prototype.call2 = function(context) {
  2. // 首先要获取调用call2的函数,用this获取
  3. context.fn = this;
  4. context.fn();
  5. delete context.fn;
  6. }
  7. // 测试一下
  8. var foo = {
  9. value: 1
  10. }
  11. function bar() {
  12. console.log(this.value)
  13. }
  14. bar.call2(foo); // 1

到这里基本就实现了call的模拟了。但是真正的call方法还需要传参数列表,所以请看模拟第二步

模拟实现第二步

首先看下真实的call方法的实现效果:

  1. // 还是同样的实例
  2. var foo = {
  3. value: 1
  4. }
  5. function bar(name, age) {
  6. console.log(name);
  7. console.log(age);
  8. console.log(this.value);
  9. }
  10. bar.call(foo, 'john', 18); // 'john' 18 1

传入的参数是不确定的,我们可以从arguments数组中去出需要的参数值

  1. /*以上个例子为例,此时的arguments为:
  2. arguments: {
  3. 0: foo,
  4. 1: 'john',
  5. 2: 18,
  6. length: 3
  7. }*/
  8. var args = [];
  9. for (var i = 1; len = arguments.length; i < len; i++) {
  10. args.push('arguments[' + i + ']');
  11. }
  12. // 执行后arg为['arguments[1]', 'arguments[2]', 'arguments[3]']

参数的问题解决了,现在只要把这个参数放进等待执行的函数的参数中,那么文问题来了,现在args中保存的是字符串并没有求出每一个参数的值,如果用ES6的方法倒是可以改装一下,不过一个ES3的方法模拟用上了ES6的方法有点不合时宜。所以只能用eval方法去求出参数值。

  1. // ES3方法改造
  2. Function.prototype.call2 = function(context) {
  3. context.fn = this;
  4. var args = [];
  5. for(var i = 1, len = arguments.length; i < len; i++) {
  6. args.push('arguments[' + i + ']');
  7. }
  8. eval('context.fn(' + args + ')'); // 这里args会自动调用Array.toString(),且firefox不能执行eval方法
  9. delete context.fn;
  10. }
  11. var foo = {
  12. value: 1
  13. }
  14. function bar(name, age) {
  15. console.log(name);
  16. console.log(age);
  17. console.log(this.value);
  18. }
  19. bar.call2(foo, 'john', 18); // 'john' 18 1
  20. // ES6方法改造
  21. Function.prototype.call2 = function(context) {
  22. context.fn = this;
  23. var args = [];
  24. for(var i = 1, len = arguments.length; i < len; i++) {
  25. args.push(arguments[i]);
  26. }
  27. context.fn(...args);
  28. delete context.fn;
  29. }

现在就可以传递参数列表了,那这个call模拟到这里算完成了吗?答案是不,call方法还可以传null,这个方法没做处理。所以我们进行第三步模拟

模拟实现第三步

加入给call方法传null,this指向window,举个例子:

  1. var value = 1;
  2. function bar() {
  3. console.log(this.value);
  4. }
  5. bar.call(null) // 1

而且函数是有返回值的,举个例子:

  1. var obj = {
  2. value: 1
  3. }
  4. function bar(name, age) {
  5. return {
  6. value: this.value,
  7. name: name,
  8. age: age
  9. }
  10. }
  11. bar.call(obj, 'john', 18); // { value: 1, name: 'john', age: 18}

我们对第二版的代码稍微改造一下就解决了这个问题

  1. Function.prototype.call2 = function(context) {
  2. var context = context || window;
  3. context.fn = this;
  4. var args = [];
  5. for(var i = 1, len = arguments.length; i < len; i++) {
  6. args.push('arguments[' + i + ']');
  7. }
  8. var result = eval('context.fn(' + args + ')'); // 这里args会自动调用Array.toString(),且firefox不能执行eval方法
  9. delete context.fn;
  10. return result;
  11. }
  12. // 测试一下
  13. var value = 2;
  14. var obj = {
  15. value: 1
  16. }
  17. function bar(name, age) {
  18. console.log(this.value);
  19. return {
  20. value: this.value,
  21. name: name,
  22. age: age
  23. }
  24. }
  25. bar.call2(null); // 2
  26. console.log(bar.call2(obj, 'john', 18)); // 1 { value: 1, name: 'john', age: 18}

到这一步算是大功告成了~~~~

模拟apply()实现

apply与call方法的远离是一样的,只不过apply的参数只有两个,第二个参数是数组,所以只需要对参数进行处理就行,改造一下。实现方法如下:

  1. Function.prototype.apply1 = function(context, arr) {
  2. var context = Object(context) || window;
  3. context.fn = this;
  4. var result;
  5. if (!arr) {
  6. result = context.fn();
  7. } else {
  8. var args = [];
  9. for (var i = 0, len = arr.length;i < len; i++) {
  10. args.push('arr[' + i + ']');
  11. }
  12. result = eval('context.fn('+ args + ')'); // firefox下eval方法会报错
  13. }
  14. delete context.fn;
  15. return result;
  16. }
  17. var foo = {
  18. value: 1
  19. }
  20. function bar(name, age) {
  21. console.log(name);
  22. console.log(age);
  23. console.log(this.value)
  24. }
  25. bar.apply1(foo, ['Tom', 12]); // 'Tom' 12 1
  26. bar.apply(foo, ['Tom', 12]); // 'Tom' 12 1

这样简单的模拟apply方法就实现啦~~

:::info 文档内容参考自:羽大大的博客 :::