目的
本文主要用于理解和掌握call,apply和bind的使用和原理,本文适用于对它们的用法不是很熟悉,或者想搞清楚它们原理的童鞋。好,那我们开始!
在JavaScript中有三种方式来改变this的作用域call,apply和bind。我们先来看看它们是怎么用的,只有知道怎么用的,我们才能来模拟它😸。
Function.prototype.call()
首先是Function.prototype.call(),不熟的童鞋请猛戳MDN,它是这么说的:call()允许为不同的对象分配和调用属于一个对象的函数/方法。也就是说:一个函数,只要调用call()方法,就可以把它分配给不同的对象。
如果还是不明白,不急!跟我往下看,我们先来写一个call()函数最简单的用法:
1 | function source(){ |
上述代码会打印出destination的name属性,也就是说source()函数通过调用call(),source()函数中的this对象可以分配到destination对象中。类似于实现destination.source()的效果,当然前提是destination要有一个source属性
好,现在大家应该明白call()的基本用法,我们再来看下面的例子:
1 | function source(age,gender){ |
打印效果如下:

我们可以看到可以call()也可以传参,而且是以参数,参数,...的形式传入。
上述我们知道call()的两个作用:
1.改变this的指向
2.支持对函数传参
我们看到最后还还输出一个undefined,说明现在调用source.call(…args)没有返回值。
我们给source函数添加一个返回值试一下:
1 | function source(age,gender){ |
打印结果:

果不其然!call()函数的返回值就是source函数的返回值,那么call()函数的作用已经很明显了。
这边再总结一下:
- 改变this的指向
- 支持对函数传参
- 函数返回什么,call就返回什么。
模拟Function.prototype.call()
根据call()函数的作用,我们下面一步一步的进行模拟。我们先把上面的部分代码摘抄下来:
1 | function source(age,gender){ |
上面的这部分代码我们先不变。现在只要实现一个函数call1()并使用下面方式
1 | console.log(source.call1(destination)); |
如果得出的结果和call()函数一样,那就没问题了。
现在我们来模拟第一步:改变this的指向。
假设我们destination的结构是这样的:
1 | let destination = { |
我们执行destination.source(18,"male");就可以在source()函数中把正确的结果打印出来并且返回我们想要的值。
现在我们的目的更明确了:给destination对象添加一个source属性,然后添加参数执行它。
所以我们定义如下:
1 | Function.prototype.call1 = function(ctx){ |
打印效果如下:

我们发现this的指向已经改变了,但是我们传入的参数还没有处理。
第二步:支持对函数传参。
我们使用ES6语法修改如下:
1 | Function.prototype.call1 =function(ctx,...args){ |
打印效果如下:

参数出现了,现在就剩下返回值了,很简单,我们再修改一下:
1 | Function.prototype.call1 =function(ctx,...args){ |
打印效果如下:

现在我们实现了call的效果!
模拟Function.prototype.apply()
apply()函数的作用和call()函数一样,只是传参的方式不一样。apply的用法可以查看MDN,MDN这么说的:apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。
apply()函数的第二个参数是一个数组,数组是调用apply()的函数的参数。
1 | function source(age,gender){ |

效果和call()是一样的。既然只是传参不一样,我们把模拟call()函数的代码稍微改改:
1 | Function.prototype.apply1 =function(ctx,args=[]){ |
执行效果如下:

apply()函数的模拟完成。
Function.prototype.bind()
对于bind()函数的作用,我们引用MDN,bind()方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this对象,之后的一序列参数将会在传递的实参前传入作为它的参数。我们看一下代码:
1 | function source(age,gender){ |
打印效果如下:

我们发现bind函数跟apply和call有两个区别:
1.bind返回的是函数,虽然也有call和apply的作用,但是需要在调用bind()时生效
2.bind中也可以添加参数
明白了区别,下面我们来模拟bind函数。
模拟Function.prototype.bind()
和模拟call一样,现摘抄下面的代码:
1 | function source(age,gender){ |
然后我们定义一个函数bind1,如果执行下面的代码能够返回和bind函数一样的值,就达到我们的目的。
1 | var res = source.bind1(destination,18); |
首先我们定义一个bind1函数,因为返回值是一个函数,所以我们可以这么写:
1 | Function.prototype.bind1 = function(ctx,...args){ |
打印效果如下:

这里我们利用闭包,把外层函数的ctx和参数args传到内层函数,再将内外传递的参数合并,然后使用apply()或call()函数,将其返回。
当我们调用res("male")时,因为外层ctx和args还是会存在内存当中,所以调用时,前面的ctx也就是source,args也就是18,再将传入的”male”跟18合并[18,’male’],执行source.apply(destination,[18,'male']);返回函数结果即可。bind()的模拟完成!
但是bind除了上述用法,还可以有如下用法:
1 | function source(age,gender){ |
打印效果如下:

我们发现bind函数支持new关键字,调用的时候this的绑定失效了,那么new之后,this指向哪里呢?我们来试一下,代码如下:
1 | function source(age,gender){ |

执行new的时候,我们发现虽然bind的第一个参数是destination,但是this是指向source的。

不用new的话,this指向destination。
好,现在再来回顾一下我们的bind1实现:
1 | Function.prototype.bind1 = function(ctx,...args){ |
如果我们使用:
1 | var res = source.bind(destination,18); |
如果执行上述代码,我们的ctx还是destination,也就是说这个时候下面的source函数中的ctx还是指向destination。而根据Function.prototype.bind的用法,这时this应该是指向source自身。
我们先把部分代码抄下来:
1 | function source(age,gender){ |
我们改一下bind1函数:
1 | Function.prototype.bind1 = function (ctx, ...args) { |
我们执行
1 | var res = source.bind1(destination,18); |
效果如下:

已经达到我们的效果!
现在分析一下上述实现的代码:
1 | //调用var res = source.bind1(destination,18)时的代码分析 |
f()函数的内部实现分析:
1 | //new res("male")相当于运行new f("male");下面进行函数的运行态分析 |
至此bind()函数的模拟实现完毕!如有不对之处,欢迎拍砖!您的宝贵意见是我写作的动力,谢谢大家。