Feng erdong's Blog

Life is beautiful

Call vs Apply vs Bind

| Comments

在JavaScript中, 函数是一等公民, 可以作为参数传入另一个函数, 也可以作为返回值从另一个函数中返回, 不仅如此, 在JavaScript这门动态语言中, 函数可以被任意对象调用, 比如说function1是定义在obj1上的, 但是它还可以被obj2所调用, 怎么做到的? 有下面的三种办法:

  • Call

基本语法: fnc.call(thisArg, arg1, arg2... argN)

  • thisArg将作为this对象传入到函数fnc
  • arg1, arg2... argN作为参数列表传入到函数中

通过call方法可以让一个函数被任何的对象所使用, call()会立即返回使用thisArg作为this对象, 余下部分作为参数列表来调用原函数fnc的结果, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
  toString: function(delimiter) {
    return "height=" + this.height + delimiter + " weight=" + this.weight;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(","));
//"height=180, weight=80"

var dog = {};
dog.height = "40";
dog.weight = "30";

console.log(person.toString.call(dog, ":"));
//"height=40: weight=30"
  • Apply

上面的例子里toString只是返回了身高与体重的数值, 但是只有数值是没有意义的, 我们还需要单位, 出于演示的目的我们打算将单位传为参数传入toString方法, 修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
  toString: function(delimiter, heightUnit, weightUnit) {
    return "height=" + this.height + heightUnit + delimiter + " weight=" + this.weight + weightUnit;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(",", "cm", "kg"));
//"height=180cm, weight=80kg"

var dog = {};
dog.height = "40";
dog.weight = "30";

console.log(person.toString.call(dog, ":", "cm", "kg"));
//"height=40cm: weight=30kg"

现在在调用call的时候需要传入三个参数(除去thisArg不算), 还不错, 不过接下来我们打算将更进一步, 我们希望在dog对象上添加一个方法justDoIt, 不管这个justDoIt接收什么参数, 都要将对justDoIt函数的调用代理到toString方法上去, 先试试看call能不能帮我们解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var person = {
  toString: function(delimiter, heightUnit, weightUnit) {
    return "height=" + this.height + heightUnit + delimiter + " weight=" + this.weight + weightUnit;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(",", "cm", "kg"));
//"height=180cm, weight=80kg"

var dog = {
  justDoIt: function() {
    person.toString.call(this, arguments);
  }
};
dog.height = "40";
dog.weight = "30";

console.log(dog.justDoIt(":", "cm", "kg"));
//"height=40undefined[object Arguments] weight=30undefined"

可以看到arguments这个数组被赋值给了第一个参数delimiter, 而heightUnit, weightUnit都没有被正确的赋值, 所以他们都是undefined.

要解决上面的问题, 就需要apply出场了, applycall非常的相似, 唯一的区别就是call需要明确地为每一个参数传值, 而apply只需要一个参数数组就可以了, 当原函数被调用的时候, apply会将这个参数数组unpack并给对应位置上的参数赋值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var person = {
  toString: function(delimiter, heightUnit, weightUnit) {
    return "height=" + this.height + heightUnit + delimiter + " weight=" + this.weight + weightUnit;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(",", "cm", "kg"));
//"height=180cm, weight=80kg"

var dog = {
  justDoIt: function() {
    return person.toString.apply(this, arguments);
  }
};
dog.height = "40";
dog.weight = "30";

console.log(dog.justDoIt(":", "cm", "kg"));
//"height=40cm: weight=30kg"
  • Bind

如果你对函数式编程有所了解, 那么你应该知道柯里化(currying function)偏函数(partial function), 它们有个共同之处就是都是从一个函数上产生出的另一个新的函数.

偏函数可以视为是针对某种情况简化之后的函数, 比如说现在有一个函数awesomeFunction(arg1, arg2, arg3), 而对这个函数的调用过程中, 第一个参数几乎都是相同的, 作为眼睛里容不得重复的程序员, 我们应该有一种办法可以消除这种重复, 这个时候偏函数就伴着掌声入场了, 我们在偏函数的声明中已经预置了第一个参数arg1=mostFrequentlyUsedValue, 这样我们调用偏参数的时候只需要传入(arg2, arg3)就可以了.

那么现在问题来了, JavaScript中怎么实现偏函数呢?

这时候就需要使用bind了, bind的基本语法与call一致: fnc.bind(thisArg, arg1, arg2... argN)

但是它们有一个重要的区别, 那就是bind不会立即执行原函数, 而是返回一个新的函数, 并且这个新的函数在被调用的时候会自动将已经bind上去的那些参数值赋给对应位置的上的参数, 新的函数只需要关心余下的那部分参数列表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function greet(timeFrame, msg) {
  return "Good " + timeFrame + ", " + msg;
}

console.log(greet("Morning", "What can I do for you?"));
console.log(greet("Morning", "This is your ticket"));
console.log(greet("Morning", "welcome back"));
console.log(greet("Morning", "How was the trip"));
console.log(greet("Afternoon", "Can I borrow you some time?"));
// "Good Morning, What can I do for you?"
// "Good Morning, This is your ticket"
// "Good Morning, welcome back"
// "Good Morning, How was the trip"
// "Good Afternoon, Can I borrow you some time?"

上面的例子中greet这个函数多数情况下都是使用”Morning”作为第一个参数, 何不创建一个更简短的函数来专门处理这种场景呢:

1
2
3
4
5
6
7
8
9
10
11
function greet(timeFrame, msg) {
  return "Good " + timeFrame + ", " + msg;
}

var morningGreet = greet.bind(undefined, "Morning");

console.log(morningGreet("What can I do for you?"));
console.log(morningGreet("This is your ticket"));
console.log(morningGreet("welcome back"));
console.log(morningGreet("How was the trip"));
console.log(greet("Afternoon", "Can I borrow you some time?"));

了解了bind其实是返回一个新的函数之后, 那么其实想通过它来实现上面call/apply的功能其实就非常简单了, 只需要调用一下生成的新的函数就可以了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
  toString: function(delimiter) {
    return "height=" + this.height + delimiter + " weight=" + this.weight;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(","));
//"height=180, weight=80"

var dog = {};
dog.height = "40";
dog.weight = "30";

console.log(person.toString.bind(dog, ":")());
//"height=40: weight=30"

上面只是从函数可以被任意对象调用的角度来分析了call, apply, bind的用法, 其实实际使用的过程中, 这些方法还有另一个非常重要的用途, 那就是给回调函数绑定正确的上下文环境(this对象).

Comments