call, apply, bind 实现与区别
call, apply, bind 它们是javascript语言中很常见的函数属性,我们从通常用它们来改变一个函数的this指向,并且传递参数很有用处。下面我们来看一下怎样去实现一个这样能改变函数this指向的函数。
this
this是Javascript语言中的一个很常用的关键字,当一个函数被当作构造函数使用时其this指向其实例,当函数被一个对象调用并执行时候this指向调用者如:
1 | let obj = { |
在这里我们不过多的讲this的指向问题,接下来我们来看在现实撸代码过程中是不是有一种这样的需求:
1 | let obj = { |
openObj也想能像obj对象那样调用某个方法展示value,有的小伙伴就会想,那我们为openObj添加一个showValue的方法不就好啦,没错可以这样做完全没问题。
不过我们还有更好的方法:
1 | obj.showValue.call(openObj); // world |
call
我们来思考一下当我们没使用call对openObj进行处理时候他是这样的:
1 | let openObj = { |
经过我们将call方法指行之后,openObj就可以使用showValue方法啦,试着这样设想一下,当我们使用call方法时候实际上是在openObj对象上添加啦一个showValue方法,用过方法之后我们在将方法删除,根据这样的思路我先来构造一下openObj在传入call方法之后它变成了什么样子:
1 | openObj = { |
然后openObjd调用完成方法之后,我们将showValue方法删除,我们一起来设计一下这个call函数
- 因为call方法是函数的属性方法所以一定在Function构造函数对prototype(圆形上),我们取名为call2:
1 | Function.prototype.call2 = function(target){ |
call方法会在传入的对象身上生成一个,调用call函数的方法,小伙伴们还记得谁调用了call方法吗?是它(showValue),是openObj没有却想要使用的那个方法。
1
obj.showValue.call(openObj); // world
现在我们在call2函数内部为传入的对像生成一个和调用call2方法一摸一样的方法。
1 | Function.prototype.call2 = function(target){ |
这时候我们来执行一下这段代码:
1 | obj.showValue.call2(openObj); // 注意我们此时改成了call2 |
- 到现在我们已经离成功不远了,因为我们在调用一下这个新增的方法就ok,最后的任务是删除:
1 | Function.prototype.call2 = function(target){ |
此时我们在调用一下:1
obj.showValue.call2(openObj); // 注意我们此时改成了call2
这样我们就完成啦call方法吗,还远远不够,因为call方法还可以传入一系列参数,最主要我们还不知道要传入的参数个数。所以要实现这样的call2方法以达到可以n个传参数的需求,我们引出一个函数环境量:arguments 实际参数列表:
如图我们在使用call2方法时候我们传入包括openObj一共4个参数都储存在arguments列表里面:
1 | [].constructor === Array |
注意:arguments不是一个数组
1 | Function.prototype.call2 = function(target){ |
我们创建一个数组args来存放参数,然后用join方法将其转化为字符串然后用eval()将字符串转化为js语法并执行,最后将其删除。
apply
我们来回想一下apply与call的功能差不多只是apply传入的参数是一个数组,下面我们对call进行修改让它成为apply。
1 | Function.prototype.apply2 = function(target){ |
bind
bind()方法和call方法的不同点就是,bind没有规定固定的参数个数,数据类型,在传入参数后返回一个新的函数,必须需要调用才能执行,不会立即执行,而call会立即执行。
1 | obj.showValue.bind(openObj)(); |
我们先来看一下bind方法的一些奇特之处
1 | let obj = { |
通过上面的输出结果我们不难发现bind当首次传入参数时候以首次参数稳准,执行时候再次传入参数则不起作用,如果首次没有传入参数则执行时候传入参数才会起作用,我们咱看一下这种情况下使用bind
1 | function introduce() { |
我们要实现一个这样的bind方法,生成一个固定this指向传入对参数对函数,让我来尝试一下
1 | Function.prototype.bind2 = function(){ |
如果我们把当前调用bind的函数返回去,我们来试验一下能否实现bind效果,执行:
1 | let liYingIntroduce = introduce.bind2(liYing); |
我们要获取的name,age 都变成了undefined,可见我们执行时候liYingIntorduce函数的this指向了window,接下来我们这样来改一下。
1 | Function.prototype.bind2 = function(){ |
执行:1
2
3
4let liYingIntroduce = introduce.bind2(liYing);
let liYingIntroduce();
// 我叫赵丽颖我今年18岁
我们返回一个函数,函数内部用call将要调用的方法的this指向bind传入的第一次参数,也就是this要指向的对象。就成功实现生成一个this固定指向的bind函数。到这里就结速了吗?还远远不够。我们来看一下这种情况。
1 | class Person{ |
我们发现当People,被创建出来的函数当this并没有被更改,我们来这样修改bind2:
1 | Function.prototype.bind2 = function(target){ |
当我们这样更改bind2函数时候,可以满足我们实现一个可执行当构造函数,来往让我们对比一下这两个分别满足不同需求的构造函数的不同点。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// 当普通函数进行执行
Function.prototype.bind2 = function(target){
let self = this;
let args = [];
for(let i = 1; i < arguments.length; i++){
args.push(arguments[i])
}
let fn = function(){
self.call(target, ...args);
}
return fn;
}
// 当构造函数使用
Function.prototype.bind2 = function(target){
let self = this;
let args = [];
for(let i = 1; i < arguments.length; i++){
args.push(arguments[i])
}
let fn = function(){
self.call(this, ...args)
}
fn.prototype = self.prototype;
return fn;
}
这时候我们只要判断一下什么时候我们当构造函数和new配合使用,什么时候当普通函数执行就完善了整个bind方法。接下来我们将实现这样的一个判断,让我们来看一下以下代码:
1 | function Person(n,a){ |
我们知道当一个函数被当做构造函数使用时候,this 指向该构造函数当实例,所以借着这个思路我们来更改一下我们bind2,让其完全实现bind方法。
1 | Function.prototype.bind2 = function(target){ |
总结
call,apply,bind函数都是函数的方法,他们都可以改变一个函数的this指向,call和apply传参数形势不同,且他们都是立即执行,而bind对所传参数没有要求,只不过它不立即执行,而是生成一个新函数只个函数的原型等于调用bind函数的那个函数的原型,且可以分别从两种情况下生成函数,一种是当最普通函数执行,一种是当作构造函数执行。谢谢!