# 定义
MDN对 call 的定义是:
call()方法使用一个特定的this值和单独给出一个参数或多个参数来调用一个函数。
举个例子:
var foo = {
  value: 1
}
function bar() {
  console.log(this.value)
}
bar.call(foo);  // 1
 1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
注意到两点:
- call 改变了 this 的指向,指向到了foo
 - bar 函数执行了
 
# 第一版
那么怎么来实现这两个效果呢? 假设调用 call 的时候,把 foo 对象改造成如下:
var foo = {
  value: 1,
  bar: function() {
    console.log(this.value)
  }
}
foo.bar();  // 1
 1
2
3
4
5
6
7
2
3
4
5
6
7
这样就可以把 this 指向 foo 对象了。
但是这样就会给 foo 对象添加多了一个属性,可以用 delete 删除掉就好了
思路就是:
- 将函数设为对象的属性
 - 执行该函数
 - 删除该函数
 
foo.fn = bar
foo.fn()
delete foo.fn
 1
2
3
2
3
于是可以尝试实现第一版:
Function.prototype.myCall = function(context) {
  // 用 this 可以获取调用 myCall 的函数
  // context 是传入的参数,this 要指向的对象
  context.fn = this
  context.fn();
  delete context.fn;
}
 1
2
3
4
5
6
7
2
3
4
5
6
7
尝试一下看看能不能实现效果:
var foo = {
  value: 1
}
function bar() {
  console.log(this.value)
}
bar.myCall(foo)  // 1
 1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
是可以实现的。
# 第二版
第一版实现了改变 this 的值和执行调用 call 的函数。 MDN定义中说到,call 还能给定参数执行函数。举例来说:
var foo = {
  value: 1
}
function bar(name, age) {
  console.log(name)
  console.log(age)
  console.log(this.value)
}
bar.call(foo, 'revan', '18'); // revan, 18, 1
 1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
我们知道,函数调用是存在 Arguments 类数组对象
arguments = {
  0: foo,
  1: 'revan',
  2: '18',
  length: 3
}
 1
2
3
4
5
6
2
3
4
5
6
可以取从第二个到最后一个的参数,放到一个数组里。
var args = [];
for (var i = 1, len = arguments.length; i < len; i++) {
  args.push('arguments[' + i + ']');
}
// args 为 ["arguments[1]", "arguments[2]"]
 1
2
3
4
5
2
3
4
5
这样就解决了不定参数的问题,接着还需要把这个参数组放到要执行的函数的参数里面。 我们可以用 eval 方法去执行:
eval('context.fn(' + args + ')')
// => eval('context.fn(arguments[1], arguments[2])')
 1
2
2
这里 args 会自动调用 Array.toString() 这个方法,把数组转成字符串 arguments[1], arguments[2]
尝试写第二版:
Function.prototype.myCall = function (context) {
  context.fn = this
  var args = []
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
  }
  eval('context.fn(' + args + ')')
  delete context.fn
}
 1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
测试一下:
var foo = {
  value: 1
}
function bar(name, age) {
  console.log(name)
  console.log(age)
  console.log(this.value)
}
bar.myCall(foo, 'revan', '18'); // revan, 18, 1
 1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
It works!
# 第三版
到现在为止已经完成了80%的功能,但是有2点需要注意的是:
1.call 可以不指定第一个参数,此时 this 为 null,当为 null 的时候,视为指向 window。
var value = 1;
function bar() {
  console.log(this.value)
}
bar.call(null);  // 1
 1
2
3
4
5
6
2
3
4
5
6
2.函数有返回值的情况下,需要把返回值返回。
修改下我们最后一版的代码:
Function.prototype.myCall = function (context) {
  var context = context || window
  context.fn = this
  var args = []
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
  }
  var result = eval('context.fn(' + args + ')')
  delete context.fn
  return result
}
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
测试一下看看能否实现我们想要的功能:
var value = 2;
var obj = {
  value: 1
}
function bar(name, age) {
  console.log(this.value)
  return {
    value: this.value,
    name: name,
    age: age
  }
}
bar.myCall(null);   // 2 (window.value)
console.log(bar.myCall(obj, 'revan', 18))
// {
//   age: 18,
//   name: "revan",
//   value: 1
// }
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
到此就完成了 call 的模拟实现。
这里顺便给出用 ES6 的方式的实现。
ES6 方式:
Function.prototype.myCall = function(thisArg) {
  // this指向调用call的对象
  if (typeof this !== 'function') {
    // 调用call的若不是函数则报错
    throw new TypeError('Error');
  }
  // 声明一个 Symbol 属性,防止 fn 被占用
  const fn = Symbol('fn')
  const args = [...arguments].slice(1);
  thisArg = thisArg || window;
  // 将调用call函数的对象添加到thisArg的属性中
  thisArg[fn] = this;
  // 执行该属性
  const result = thisArg[fn](...args);
  // 删除该属性
  delete thisArg[fn];
  // 返回函数执行结果
  return result;
}
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# apply 的实现
apply 的实现和 call 的类似,只不过 apply 传入的参数是一个数组。输入参数如果为 null 或者 undefined,则表示不需要传入任何参数。
Function.prototype.myApply = function (context, arr) {
  var context = Object(context) || window;
  context.fn = this;
  var result;
  if (!arr) {
    result = context.fn();
  } else {
    var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push('arr[' + i + ']');
    }
    result = eval('context.fn(' + args + ')')
  }
  delete context.fn
  return result;
}
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ES6 方式:
Function.prototype.myApply = function(thisArg) {
  if (typeof this !== 'function') {
    throw this + ' is not a function';
  }
  const args = arguments[1];
  const fn = Symbol('fn')
  thisArg[fn] = this;
  const result = thisArg[fn](...arg);
  delete thisArg[fn];
  return result;
};
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15