背景

今天浏览网站时看到一个问题,如何手动实现一个 call 方法?,看了下源码,似懂非懂的理解了一下,心中不免大呼,就这几行难道就是 javascript 中大名鼎鼎的 call 方法,这也太简单了吧。
敷衍了事之后,关闭了网站,继续在 🕸️⬆️ 开始了 🏄‍。
可是下来回味的时候,想自己手写一遍,才猛然发现,似懂非懂的真正含义其实是不懂。
又看了一下原理,才发现自己真的有点不明白。
于是我自己在本地跑了一个程序,仔仔细细的把每一个点 debug 完才发现,掌握完知识的感觉是多么的奇妙。

实现

好,那么我们现在开始进入正题,先揭开谜题,如何手动实现一个 call 方法?,这个方法的每一行代码如果你都能理解什么意思的话,那请您别看了,您比我 🐂🍺 ,ctrl + w or command + w 继续开始您的 🕸️⬆️🏄‍。
暂时叫这个方法叫 mycall,原理和 call一样。

1
2
3
4
5
6
7
Function.prototype.myCall = function (context) {
context.fn = this
const args = [...arguments].slice(1)
const result = context.fn(args)
delete context.fn
return result
}

如果你不能理解每一行代码代表的意思,那么恭喜你,你和我一样笨,哈哈哈。
先笑一会儿。
那我们正式开始进入正题,先来一个小 demo:

1
2
3
4
5
6
7
const obj1 = {
x: 1,
count: function () {
return this.x
}
}
console.log(obj1.count())

👆这个 demo 的输出是 1,如果你答错了,那么你可能是没睡醒,多看看就明白了。

我们继续,

1
2
3
4
5
6
7
8
9
10
11
12
const obj1 = {
x: 1,
count: function () {
return this.x
}
}

const obj2 = { x: 2 }

obj2.count = obj1.count

console.log(obj2.count())

上面这个 demo 的输出应该为 2, 我重新声明了一个 obj2,然后在 obj2 中添加了一个新的属性,它的值就是 obj1count 方法,所以上面那个 demo 和下面这个是一样的意思,但是不同的是上面的 demo 中的 obj1obj2 中的 count 方法指向得的是同一个内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj1 = {
x: 1,
count: function () {
return this.x
}
}

const obj2 = {
x: 2,
count: function () {
return this.x
}
}

console.log(obj2.count())

如果你都能理解的话,那么我们的主题,如何手动实现一个 call 方法?,就能理解三分之一了。

我们先抛出一个完整的例子来,再挨个解析 myCall 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Function.prototype.myCall = function (context) {
context.fn = this

const args = [...arguments].slice(1)

const result = context.fn(args)

delete context.fn

return result
}

const obj1 = {
x: 1,
count: function () {
return this.x
}
}

const obj2 = { x: 2 }

const newCount = obj1.count

console.log(newCount.myCall(obj2))

根据我们学习到知识,这次还是输出 2 ,那么 myCall 中的 this 指向是什么呢,context.fn 又是什么意思呢.

其实 context.fn = this 中的 this 指向的当前的 count 函数名,context 是我们传入的 obj2 对象。
所以这里的意思就是为 obj2 添加一个属性 fn,值为 count 函数,就跟我们之前的小 demo 一样。

然后下一行的意思就是取得 mycall 后面的参数,再下一行就是将参数传入 count 函数并执行,最后返回执行结果。

思考

关于 this 指向,总有数不清的困扰,但是只要我们很多时候牢记一点,this 永远指向它最后调用的一个地方。

如果还不明白为什么context.fn = this 中的 this 指向的当前的 count 函数名的话,可以看下面的例子:

1
2
3
4
5
6
7
function justAEmptyFunc() {}

Function.prototype.sayMe = function () {
console.log(this)
}

justAEmptyFunc.sayMe()

执行当前代码,console 打印出的就是当前的函数名: justAEmptyFunc,因为函数本质也是一个对象(object).

1
Function.__proto__.__proto__ === Object.prototype

上面的代码返回 true。

其次,我们不妨再试试 mycall,(执行环境:nodejs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
global.x = 2

Function.prototype.myCall = function (context) {
context.fn = this

const args = [...arguments].slice(1)

console.log(this(args))

const result = context.fn(args)

delete context.fn

return result
}

const obj1 = {
x: 1,
count: function () {
return this.x
}
}

const obj2 = { x: 2 }

const newCount = obj1.count

console.log(newCount.myCall(obj2))

执行以上代码时,你会发现会额外打印出一个 x 值,这个值就是全局变量 2,代码在 console.log(this(args))这句,因为就像我们上面说的 this 值为 count 函数,而单纯执行 count 函数时没有执行环境,默认执行环境就是全局(nodejs 环境下为 global,浏览器下为 window)。

同理,如果想实现 applybind 等方法也是一个原理,只是 bind 不返回执行结果,只返回执行环境,所以需要返回一个改变 this 过后的执行函数环境。