Revan1i

vuePress-theme-reco revan1i    2019 - 2020
Revan1i Revan1i

Choose mode

  • dark
  • auto
  • light
Home
TimeLine
Category
  • javascript
  • css
  • react
  • js
  • how
  • math
  • regexp
  • algorithm
  • feeling
Tag
Contact
  • GitHub
author-avatar

revan1i

25

文章

20

标签

Home
TimeLine
Category
  • javascript
  • css
  • react
  • js
  • how
  • math
  • regexp
  • algorithm
  • feeling
Tag
Contact
  • GitHub

Javascript源码之call和apply的模拟实现

vuePress-theme-reco revan1i    2019 - 2020

Javascript源码之call和apply的模拟实现


revan1i 2019-12-09 09:09:57 javascript source code

# 定义

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

注意到两点:

  1. call 改变了 this 的指向,指向到了foo
  2. bar 函数执行了

# 第一版

那么怎么来实现这两个效果呢? 假设调用 call 的时候,把 foo 对象改造成如下:

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

这样就可以把 this 指向 foo 对象了。

但是这样就会给 foo 对象添加多了一个属性,可以用 delete 删除掉就好了

思路就是:

  1. 将函数设为对象的属性
  2. 执行该函数
  3. 删除该函数
foo.fn = bar
foo.fn()
delete foo.fn
1
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

尝试一下看看能不能实现效果:

var foo = {
  value: 1
}

function bar() {
  console.log(this.value)
}

bar.myCall(foo)  // 1
1
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

我们知道,函数调用是存在 Arguments 类数组对象

arguments = {
  0: foo,
  1: 'revan',
  2: '18',
  length: 3
}
1
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

这样就解决了不定参数的问题,接着还需要把这个参数组放到要执行的函数的参数里面。 我们可以用 eval 方法去执行:

eval('context.fn(' + args + ')')
// => eval('context.fn(arguments[1], arguments[2])')
1
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

测试一下:

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

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.函数有返回值的情况下,需要把返回值返回。

修改下我们最后一版的代码:

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

测试一下看看能否实现我们想要的功能:

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

到此就完成了 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

# 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

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

# 参考

  1. JavaScript深入之call和apply的模拟实现
  2. 各种源码实现,你想要的这里都有