前言
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的方式。
-
第一点中,
obj.getName()
打印出了’son’,因为this指向了obj对象。 -
第二点中,我们使用了settimeout函数,这时候getName中的this指向了外层。这是因为这时的
obj.getName()
在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的。 -
第三点,定时器的回调函数里调用了
obj.getName()
,这就相当于在全局环境中执行了obj.getName,道理跟第一点是等价的。 -
第四点等价于第2点,它很直观地说明,为什么第二点返回的是’father’,就是因为第二点在全局环境中执行了
console.log(this.name)
,所以打印出来的肯定就是全局的name了。 -
第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())