javascript
javascript

call, apply, bind 实现与区别

call, apply, bind 它们是javascript语言中很常见的函数属性,我们从通常用它们来改变一个函数的this指向,并且传递参数很有用处。下面我们来看一下怎样去实现一个这样能改变函数this指向的函数。

this

this是Javascript语言中的一个很常用的关键字,当一个函数被当作构造函数使用时其this指向其实例,当函数被一个对象调用并执行时候this指向调用者如:

1
2
3
4
5
6
7
let obj = {
value:'hello world',
showValue:()=>{
console.log(this.value)
}
}
obj.showValue();// this指向obj

在这里我们不过多的讲this的指向问题,接下来我们来看在现实撸代码过程中是不是有一种这样的需求:

1
2
3
4
5
6
7
8
9
10
let obj = {
value:'hello',
showValue:function(){// 注意此处不能用箭头函数会修改this指向
console.log(this.value)
}
}

let openObj = {
value:'world'
}

openObj也想能像obj对象那样调用某个方法展示value,有的小伙伴就会想,那我们为openObj添加一个showValue的方法不就好啦,没错可以这样做完全没问题。

不过我们还有更好的方法:

1
obj.showValue.call(openObj); // world

call

我们来思考一下当我们没使用call对openObj进行处理时候他是这样的:

1
2
3
let openObj = {
value:'world'
}

经过我们将call方法指行之后,openObj就可以使用showValue方法啦,试着这样设想一下,当我们使用call方法时候实际上是在openObj对象上添加啦一个showValue方法,用过方法之后我们在将方法删除,根据这样的思路我先来构造一下openObj在传入call方法之后它变成了什么样子:

1
2
3
4
5
6
7
openObj = {
value:'world',
// 多出一个showValue方法
showValue(){
console.log(this.value)
}
}

然后openObjd调用完成方法之后,我们将showValue方法删除,我们一起来设计一下这个call函数

  1. 因为call方法是函数的属性方法所以一定在Function构造函数对prototype(圆形上),我们取名为call2:
1
2
3
Function.prototype.call2 = function(target){
// 实现.....
}
  1. call方法会在传入的对象身上生成一个,调用call函数的方法,小伙伴们还记得谁调用了call方法吗?是它(showValue),是openObj没有却想要使用的那个方法。

    1
    obj.showValue.call(openObj); // world
  2. 现在我们在call2函数内部为传入的对像生成一个和调用call2方法一摸一样的方法。

1
2
3
Function.prototype.call2 = function(target){
target.fn = this //此时的this指向调用call方法的方法
}

这时候我们来执行一下这段代码:

1
obj.showValue.call2(openObj); // 注意我们此时改成了call2
image
image
  1. 到现在我们已经离成功不远了,因为我们在调用一下这个新增的方法就ok,最后的任务是删除:
1
2
3
4
5
Function.prototype.call2 = function(target){
target.fn = this;//此时的this指向调用call方法的方法
target.fn();
delete obj.fn;
}

此时我们在调用一下:

1
obj.showValue.call2(openObj); // 注意我们此时改成了call2

image
image

这样我们就完成啦call方法吗,还远远不够,因为call方法还可以传入一系列参数,最主要我们还不知道要传入的参数个数。所以要实现这样的call2方法以达到可以n个传参数的需求,我们引出一个函数环境量:arguments 实际参数列表:

image
image

如图我们在使用call2方法时候我们传入包括openObj一共4个参数都储存在arguments列表里面:

1
2
3
4
[].constructor === Array
// true
arguments.constructor === Array
// false

注意:arguments不是一个数组

1
2
3
4
5
6
7
8
9
Function.prototype.call2 = function(target){
target.fn = this;//此时的this指向调用call方法的方法
let args = [];
for(let i = 1; i < arguments.length; i++){
args.push(arguments[i])
}
target.fn(...args);
delete target.fn;
}

我们创建一个数组args来存放参数,然后用join方法将其转化为字符串然后用eval()将字符串转化为js语法并执行,最后将其删除。

apply

我们来回想一下apply与call的功能差不多只是apply传入的参数是一个数组,下面我们对call进行修改让它成为apply。

1
2
3
4
5
6
7
8
9
Function.prototype.apply2 = function(target){
target.fn = this;//此时的this指向调用call方法的方法
let args = [];
for(let i = 1; i < arguments.length; i++){
args.push(arguments[i])
}
target.fn([...args]);
delete target.fn;
}

bind

bind()方法和call方法的不同点就是,bind没有规定固定的参数个数,数据类型,在传入参数后返回一个新的函数,必须需要调用才能执行,不会立即执行,而call会立即执行。

1
obj.showValue.bind(openObj)();

我们先来看一下bind方法的一些奇特之处

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
value:10,
showValue:function(args){
console.log(this.value + args[0] + args[1])
}
}

let openObj = {
value:'world'
}
obj.showValue.bind(openObj,[1,2]);// 10+1+2 = 13
obj.showValue.bind(openObj)(8, 9);// 10+8+9 = 27
obj.showValue.bind(openObj,[1,2])(8, 9);// 10+1+2 = 13

通过上面的输出结果我们不难发现bind当首次传入参数时候以首次参数稳准,执行时候再次传入参数则不起作用,如果首次没有传入参数则执行时候传入参数才会起作用,我们咱看一下这种情况下使用bind

1
2
3
4
5
6
7
8
9
10
11
12
13
function introduce() {
console.log(`我叫${this.name}我今年${this.age}岁`);
}

let liYing = {
name : '赵丽颖',
age : 18
}

let liYingIntroduce = introduce.bind(liYing);

let liYingIntroduce();
// 我叫赵丽颖我今年18岁

我们要实现一个这样的bind方法,生成一个固定this指向传入对参数对函数,让我来尝试一下

1
2
3
Function.prototype.bind2 = function(){
return this;
}

如果我们把当前调用bind的函数返回去,我们来试验一下能否实现bind效果,执行:

1
2
3
4
let liYingIntroduce = introduce.bind2(liYing);

let liYingIntroduce();
// 我叫undefined我今年undefined岁

我们要获取的name,age 都变成了undefined,可见我们执行时候liYingIntorduce函数的this指向了window,接下来我们这样来改一下。

1
2
3
4
5
6
7
8
Function.prototype.bind2 = function(){
let target = [].slice.call(arguments, 0, 1);
let self = this;
let fn = function(){
self.call(target);
}
return fn;
}

执行:

1
2
3
4
let liYingIntroduce = introduce.bind2(liYing);

let liYingIntroduce();
// 我叫赵丽颖我今年18岁

我们返回一个函数,函数内部用call将要调用的方法的this指向bind传入的第一次参数,也就是this要指向的对象。就成功实现生成一个this固定指向的bind函数。到这里就结速了吗?还远远不够。我们来看一下这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}

}

class Man {
constructor(options){
this.options = options;
}
}

let man = new Man()

let People = Person.bind(man);
let p = new People('赵丽颖', 18);
// p : Person {name: "赵丽颖", age: 18}

我们发现当People,被创建出来的函数当this并没有被更改,我们来这样修改bind2:

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

当我们这样更改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
2
3
4
5
6
7
8
9
10
11
function Person(n,a){   
this.name = n;
this.age = a;
if(this instanceof Person){
alert('new调用');
}else{
alert('函数调用');
}
}
var p = new Person('jack',30); // --> new调用
Person(); // --> 函数调用

我们知道当一个函数被当做构造函数使用时候,this 指向该构造函数当实例,所以借着这个思路我们来更改一下我们bind2,让其完全实现bind方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(){
if(this instanceof self){
self.call(this, ...args);
}else{
self.call(target, ...args);
}
}
fn.prototype = self.prototype;
return fn;
}

总结

call,apply,bind函数都是函数的方法,他们都可以改变一个函数的this指向,call和apply传参数形势不同,且他们都是立即执行,而bind对所传参数没有要求,只不过它不立即执行,而是生成一个新函数只个函数的原型等于调用bind函数的那个函数的原型,且可以分别从两种情况下生成函数,一种是当最普通函数执行,一种是当作构造函数执行。谢谢!