前言

JavaScript中apply、call和bind三兄弟通常被用来改变this指针的指向。这样设计的目的是什么,它们之间又有什么区别?

this指针的表现

var name="father";
let obj={
  name:"son",
  getName:function () {
    console.log(this.name);
  }
};
// 1. son,this指向obj对象
obj.getName(); 
// 2. father,this指向外层对象。
setTimeout(obj.getName,1); 
// 3. son,this指向obj对象,等价于第1点
setTimeout(function(){
  obj.getName()
},1); 
// 3-1. son,第3点的另一种写法
setTimeout(obj.getName(),1);
// 4. father,this指向外层对象,等价于第2点
setTimeout(function(){
  console.log(this.name);
},1);

首先,看看上面的四种打印name的方式。

  1. 第一点中,obj.getName()打印出了’son’,因为this指向了obj对象。

  2. 第二点中,我们使用了settimeout函数,这时候getName中的this指向了外层。这是因为这时的obj.getName()在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的。

  3. 第三点,定时器的回调函数里调用了obj.getName(),这就相当于在全局环境中执行了obj.getName,道理跟第一点是等价的。

  4. 第四点等价于第2点,它很直观地说明,为什么第二点返回的是’father’,就是因为第二点在全局环境中执行了console.log(this.name),所以打印出来的肯定就是全局的name了。

  5. 第3-1点和第2点的区别,只差了一个()。第2点没有(),相当于一个函数声明,这个函数变成了settimeout的回调函数;第3-1点有(),相当于一次函数执行,所以settimeout方法把它放到了自己的回调函数中,等价于第3点。

日常开发中的解决方式

日常开发中,我们遇到定时器等回调函数需要外层作用域的情况,通常是最简单粗暴的let that = this解决。

var name="father";
let that = this
let obj={
  name:"son",
  getName:function () {
    console.log(this.name);
  }
};
let obj1={
  name:"son",
  getName:function () {
    console.log(that.name);
  }
};
setTimeout(obj.getName(),10);// son
setTimeout(obj1.getName(),10);// father

上面obj中的this指向的是obj。如果我们需要它指向外层的对象,可以手动先把this对象赋值给其他变量that,这样obj1中的调用的that就是外层的this函数了。

apply、call、bind的区别

  • 都可以改变this指针的指向。

  • 第一个参数都是this对象,如果没有这个参数或参数设置为undefined或null,则默认指向全局作用域。

  • 都可以带参传递,apply是数组形式,call是参数枚举,bind可以多次传入

  • apply和call直接返回执行结果,bind返回的是函数,需要再调用一次

call方法

call方法的第1个参数是this指向的对象,如果是null,则指向全局作用域。

call方法的第2~n个参数是函数所带的参数,必须一个个列出来。

  • call方法实现this指向改变。下面的obj.getName()原本指向obj,利用call方法带传null直接让this指向了全局作用域。打印出了father
var name="father";
let that = this
let obj={
  name:"son",
  getName:function () {
    console.log(this.name);
  }
};
setTimeout(obj.getName(),10);// son
setTimeout(obj.getName.call(null),10);// father
  • call方法带参。下面call中,第一个参数把this指针指向了person1,同时带参。
var person = {
  firstName: '',
  fullName: function(lastName,age) {
    console.log(this.firstName + lastName + " " + age + "")
  }
}
var person1 = {
  firstName: '',
}
person.fullName('小红', 12)// 陈小红 12岁
person.fullName.call(person1,'小明', 10) // 郭小明 10岁
  • 利用call实现函数定义时不带参,使用时带参。下面的call第一个参数直接传入新的对象,this指针直接指向新声明的对象。
function fullName(lastName) {
  console.log(this.firstName + lastName)
}
fullName.call({firstName:''},'大炮')// 张大炮

apply方法

  • apply方法跟call唯一的区别是带参的形式,apply是以数组形式传参,call是逐个接受参数
var person = {
  fullName: function(lastName,age) {
    console.log(this.firstName + lastName + " " + age + "")
  }
}
var person1 = {
  firstName: '',
}
person.fullName.call(person1,'小明', 10) // 郭小明 10岁
person.fullName.apply(person1,['小明', 10]) // 郭小明 10岁
  • 因为apply数组传参的特性,我们可以使用它来进行某些特殊的传参。

下面的例子使用max方法筛选最大数值,只能通过一个个数的传值,apply方法可以更优雅的直接传入整个数组

let list = [4,5,6,7,8,9,10,11]
let max1 = Math.max(4,5,6,7,8,9,10,11);// 11
let max2 = Math.max.apply(null,list)//11
  • 由apply方法探究push方法的本质
let alist = ['a1', 'a2'];
let blist = ['b1', 'b2'];
alist.push.apply(null,blist)// 报错

因为有了max的使用经历,很自然我在使用push方式结合apply时自然而然就传入了null,发现居然报错了! 为什么max方法就没有报错,而push就报错了呢?

仔细分析原因,其实max传入null直接将this指向了window作用域,而由于max方法中的动作并不会使用到this对象,所以没有报错。

而push呢?push的本质其实是这样的。

alist.push(blist){
  return [this[0],this[1],blist[0],blist[1]...]
}

push方法在拼接数组的时候使用到了this对象,所以这里直接传null是不行的,我们需要把被push的对象传入

let alist = ['a1', 'a2'];
let blist = ['b1', 'b2'];
alist.push.apply(alist,blist)// ['a1', 'a2', 'b1', 'b2']
let x = ['甚至前面的变量乱写都可以']
x.push.apply(alist,blist)// ["a1", "a2", "b1", "b2", "b1", "b2"]
console.log(alist)

bind方法

bind方法的传值方式跟call一样,但是它不会立即执行,而是返回一个永久改变了this指向的函数,这意味着只要它还没执行,我们可以一直传值进去

let list = [4,5,6,7,8,9,10,11]
let max = null
for(let i = 0;i<list.length;i++){
  max = Math.max.bind(null,list[i])
}
console.log(max())