apply, call和bind

apply, callbind都是用来动态修改函数执行环境的方法,它们都可以让函数绑定指定的this

this是函数用来获取到它的原型的属性和方法的关键字。当它在函数中使用时,总是指向一个对象——就是调用这个函数的对象。但有时候,this的指向会发生偏差,这时就需要我们人为地为this绑定对象。

举一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
// 当购物车中有东西时,就给这个商品添加一个“删除”按钮
if(this.isEmpty()) {
var deleteBtn = document.createElement("button");
deleteBtn.innerHTML = "Empty Cart";
deleteBtn.className = "delete";

var myCart = this;
deleteBtn.addEventListener("click", function() {
myCart.clearCart();
})
cartDOM.appendChild(deleteBtn);
}

因为在deleteBtn.addEventListener里面,this指向的是deleteBtn这个对象,而不再是myCart了,如果我们需要使用myCart的方法,有一种方法是提前将我们要使用的this保存在一个变量中,需要用到的时候使用变量来替代。

但是相比使用变量,我们还有更好,更优雅的方法。

bind()

MDN的解释:bind()方法创建一个新的函数,当这个函数被调用时,它的this关键字被设置成一个特定的值,新函数被调用时,还可以传递一组序列参数。

我们先看一个简单的示例,它说明了什么情况下我们会需要用到bind来修改this的指向。

1
2
3
4
5
6
7
8
9
10
11
12
var module = {
x: 42,
getX: function() {
return this.x;
}
}

var unboundGetX = module.getX;
console.log(unboundGetX()); //此时,函数在全局范围中被触发,this指向window,所以this.x是undefined

var boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 42

触发函数时的上下文并不是我们想要引用this的上下文,这时就需要给this绑定我们希望执行的上下文。

看一个语义化更明显的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var pokemon = {
firstname: "Pika",
lastname: "Chu",
getPokeName: function() {
var fullname = this.firstname + ' ' + this.lastname;
return fullname;
}
};

var pokemonName = function() {
console.log(this.getPokeName + 'I choose you!');
}

var logPokemon = pokemonName.bind(pokemon);
logPokemon(); //"Pika Chu I choose you!"

bind()函数实际上是创建了一个新的pokomonName的实例,并将pokemon作为this值,这样新的实例就拥有了所有pokemonName函数的方法的副本。

原理

bind()函数创建了一个新的绑定函数(bound function, BF)。这个绑定函数将原来的函数对象包裹起来。所以在调用绑定函数时,实际上是在执行被包裹的函数绑定函数有如下属性:

  • [[BoundTargetFunction]] 即被包裹的函数对象
  • [[BoundThis]] 当调用被包裹的函数时,这个值总是被传递给被包裹函数作为this
  • [[BoundArguments]] 一组值,作为触发被包裹函数时的第一个参数
  • [[Call]] 执行与此对象关联的代码。 通过函数调用的表达式被触发。 内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数列表

当绑定函数被调用时,它调用的是[[BoundTargetFunction]]上的内部方法[[Call]],后跟参数,像这样Call(boundThis, args)。其中,boundThis就是[[BoundThis]],args就是[[BoundArguments]],后跟函数调用传递的参数。

绑定函数也可以用new去构造一个由目标函数创建的新的实例。当一个绑定函数被用来创建新的实例时,原来提供的this值会被忽略,但原来提供的参数仍然被前置到构造函数的调用里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + "," + this.y;
}

var p = new Point(1, 2);
p.toString() //1, 2

var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0);
var YAxisPoint = Point.bind(null, 0);

var axisPoint = new YAxisPoint(5);
axisPoint.toString(); //'0, 5'

axisPoint instanceof Point; //true
axisPoint instanceof YAxisPoint; //true
new Point(17, 42) instanceof YAxisPoint; //true

此时如果我们打印axisPoint__proto__就可以看到它的__proto__原型就是Point函数。

![image-20180916105705225](../../../Library/Application Support/typora-user-images/image-20180916105705225.png)

此外,bind()函数可以插入固定参数

1
2
3
4
5
6
7
8
9
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1,2,3); //[1,2,3]

var leadingThirtysenvenList = list.bind(null, 37);
var list2 = leadingThirtysevenList(); //[37]

var list3 = leadingThirtysevenList(1, 2, 3); //[37,1,2,3]

window.setTimeout()this默认是window对象。当使用需要将this指向类的实例的类方法时,可以直接将this绑定到回调函数上,以此来维护实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function lateBloomer() {
this.petalCount = Math.floor(Math.random() * 12) + 1;
}

LateBloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 1000); //将this绑定到LateBloomer的实例
}

LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with' + this.petalCount + 'petals'); //this会指向window
}

var flower = new LateBloomer();
flower.bloom();

bind()可以用于简化函数。比如Array.prototype.slice可以用来将一组array-like的数组转换成真数组,那么就可以这样写:

1
2
var slice = Array.prototype.slice;
slice.apply(arguments);

使用bind()还能进一步简化。下面的代码中,slice是函数原型的apply()方法的绑定函数,并且把this指向数组原型的slice()方法,这样多余的apply()就可以省略掉了。

1
2
3
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);
slice(arguments);

兼容性问题

低版本的IE不支持bind(),那么可以使用代码将bind()绑定到环境里。

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
if(!Function.prototype.bind) {  //判断环境是否不支持bind
Function.prototype.bind = function(oThis) { //将bind绑定到函数原型上,oThis是传递给它的this值
// 判断目标函数是不是函数,如果不是函数,就无法调用
if(typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is tring to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1), //切割出除了this之外的其他参数
fToBind = this, // 这里的this指的是调用bind的函数
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP //如果this是fNOP的实例,即是在创建新实例
? this //忽略传进来的this值,仍然使用实例的this
: oThis //如果this不是新创建的函数实例,就绑定指定的this值
, aArgs.concat(Array.prototype.slice.call(arguments)));
//aArgs是bind函数的参数(前面提到的固定参数)
//后面的arguments是fBound传入的参数
};
//判断this有没有prototype
if(this.prototype) {
//fNOP即Function.prototype是没有prototype属性的
fNOP.prototype = this.prototype;
}
//绑定函数是目标函数的实例,它会创建目标函数的副本
fBound.prototype = new fNOP();
return fBound;
}
}

call()apply()

call()apply()方法的作用一样,语法基本一致,唯一的区别是call()接受参数的方式是逐个传入,apply()则是接受一组参数数组。

这两个方法都是调用一个函数,并且给这个函数指定一个this值,同样也可以传入参数。

1
2
3
4
5
6
7
8
9
10
11
function Product(name, price) {
this.name = name;
this.price = price;
}

function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}

console.log(new Food('cheese', 5).name); // 'cheese'

bind(),apply()call()

这三个函数在修改函数执行的上下文时都能指定this值,它们有一些细微的区别。

  1. bind()会返回一个函数,call()apply()直接调用函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let obj = {things: 3};

    let addThings = function(a, b, c){
    return this.things + a + b + c;
    }

    //此时console.log(addThings())输出NaN,因为this指向window,this.things是undefined

    //解决方案1:
    console.log(addThings.call(obj, 1,2, 3))//9

    //解决方案2:
    let arr = [1, 2, 3]
    console.log(addThings.apply(obj, arr)) //9

    //解决方案3:
    console.log(addThings.bind(obj, 1,2,3)); //[Function: bound addThings] 返回一个函数
    console.log(addThings.bind(obj,1,2,3)()); // 9
  2. bind()返回的函数是addThings的副本,而apply()call()在调用时不会创建副本

参考:

Javascript: call(), apply() and bind()

Function.prototype.bind()

The difference between call / apply / bind

Understanding JavaScript Bind ()