应用背景
apply,call,bind出现的目的都是切换/固定函数内部this的指向(即函数执行时所在的作用域)。
- Function.prototype.call()
- Function.prototype.apply()
- Function.prototye.bind()
Function.prototype.call()
函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。例如:
var obj = {};
var f = function () {
console.log(this);
}
f();//window
f.call(obj);//obj
上面的代码在控制台运行f()时this指向全局环境window(不同的环境对应的全局环境不同,node为global);调用f.call(obj)时,call函数会改变函数f内部的指向为obj对象。
call()函数参数f.call(thisArgs, arg1,arg2,...)
call函数的第一个参数是this指向的对象,1,如果thisArgs不是一个对象,则会自动包装为对应的对象; 2,如果thisArgs为空、null、undefined,则默认传入全局对象;arg1,arg2,...是传入函数的参数。例如:
var name = '李四';
var obj = {
name: '张三'
};
var f = function (){
console.log(this.name);
};
f();//李四
f.call();//李四
f.call(null);//李四
f.call(undefined);//李四
f.call(window);//李四
f.call(obj);//张三
f.call('王五');//undefined
call()函数的特殊应用--调用对象的原生方法
var obj = {};
obj.hasOwnProperty('toString');//false
// 覆盖掉obj继承的方法
obj.hasOwnProperty = function () {
return true;
}
// 覆盖掉之后就得不到正确的结果,调用自己定义的hasOwnProperty方法
obj.hasOwnProperty('toString');//true
// 如下调用就可解决该问题,直接在obj对象调用原型链上的hasOwnProperty方法
Object.prototype.hasOwnProperty.call(obj, 'toString');//false
call()函数将hasOwnProperty方法的原始定义放到obj对象上执行,这样即使覆盖了hasOwnProperty方法也不会影响结果。
call()函数的特殊应用--实现继承
function a(){
this.name = '张三';
this.getInfo = function (){
console.log('a:', this.name, this.age);
}
}
function b(){
this.age = 18;
a.call(this);//this表示对象本身
}
var bt = new b();
console.log(bt);
运行结果:
new关键字实例化bt对象时会将所有this关键字的属性都生成了新对象的属性,也就是说new关键字会将所有的和this有关的属性都会转化为新对象的属性,那么当call方法将b的this对象赋值给a方法时,new也会检测到并且当做是b的属性一起实例化,所以实现继承的方式call只是起了个替换this对象的作用,主要工作还是在new 关键字,它能自动检测和this绑定的属性,全部添加到要实例化的对象上面。
Function.prototype.apply()
apply函数与call函数类似,唯一的区别是他接收一个数组作为执行时的参数,使用方式为:
func.apply(thisArgs, [arg1,arg2,...]);
//实例
function f(a,b){
console.log(a*b);
}
f.call(null, 2,2);//4
f.apply(null,[2,2]);//4
第一个参数thisArgs为this指向的对象,也就是函数执行时所在的作用域;(1)如果thisArgs为null或者undedined,则与call函数一样,相当于传入了全局对象。
apply函数的特殊应用:
(1)寻找数组中最大的元素
var a = [3,8,10,5,2];
Math.max.apply(Math, a);//10
Math.max.apply(null, a);//10
Math.max.apply(undefined, a);//10
(2)转换数组的空元素为undefined
通过apply方法,利用Array构造函数将数组的空元素变为undefined
var a = ['a',,'b'];
var b = Array.apply(null, a);//???这里调用Array函数的时候发生了什么??
//不是应该用new调用吗?
console.log(b);//["a", undefined, "b"]
将数组空元素转换为undefined的用处:
数组的forEach方法会跳过空元素,但是不会跳过undefined(也不会跳过null),这样遍历数组会有不一样的结果。实例如下:
var a = ['a',,'b'];
var print = function (i) {
console.log(i);
};
a.forEach(print);
//a
//b
Array.apply(null,a).forEach(print);
//a
//undefined
//b
(3)转换类数组对象为真正的数组--利用数组的slice方法
var a = {
0: 1,
1: 2,
length: 2
};
var b = {
0:1,
};
var c = {
0: 1,
length: 2,
};
var d = {
length: 2,
};
Array.prototype.slice.apply(a);//[1,2]
Array.prototype.slice.apply(b);//[]
Array.prototype.slice.apply(c);//[1,](有一个空元素)
Array.prototype.slice.apply(d);//[,] (两个空元素)
Array.apply(null,Array.prototype.slice.apply(d));//[undefined, undefined]
由上可知,apply方法的参数都是对象,返回结果是数组,但是有两个需要注意的地方是:一是必须有length属性,以及对应的数字键,该方法才起作用;二是length属性值大于拥有的键值对时,会返回空元素,这个时候要想让其返回undefined,可参考上面的方法。
(3)绑定回调函数的对象
回调函数中的this问题:
var o = new Object();
o.f = function () {
console.log(this === o);
};
//jQuery的写法
$('#button').on('click', o.f);//false
上面点击按钮后控制台显示false的原因就是点击的时候,由于f方法是在按钮对象的环境中调用的,因此this不再指向o对象,而是指向按钮的DOM对象,为了解决上述问题,我们可以使用apply或者call方法。如下:
var o = new Object();
o.f = function () {
console.log(this === o);
};
var f = function () {
o.f.apply(o);
//或者o.f.call(o);
};
//jQuery的写法
$('#button').on('click', f);//true
由于调用o.f函数的时候显示绑定的该函数的运行环境,点击按钮以后控制台打印true;同时apply和call函数都是会立即执行的,所以这里使用一个函数f进行包装。
Function.prototype.bind()
bind方法主要是将函数体内的this对象绑定到某个对象,然后返回一个新函数。
var d = new Date();
d.getTime();//1561281958569
var print = d.getTime;
print();
//固定d.getTime函数内部的this变量
var prints = d.getTime.bind(d);
prints();//1561282104241
d.getTime赋给变量print之后再调用该方法就报错(如上)。原因就是getTime方法内部的this原本指向Date对象的实例,赋值给print方法后,this不再指向Date对象的实例。如果用bind方法将getTime方法内部的this固定到Date对象的实例,就可以随心所欲的赋值给其他变量了。
var counter = {
count: 0,
inc: function () {
this.count++;
console.log(this === counter);
}
};
var f = counter.inc.bind(counter);
f();//1,true
//this也可以绑定到其他的对象
var obj = {
count: 30,
}
var ff = counter.inc.bind(obj);
ff();//31, false
bind函数参数
function.prototype.bind(thisArg, arg1, arg2,...)bind函数的参数和call函数一样,第一个参数thisArg是所要绑定的this的对象。arg1,arg2,...是目标函数被调用时,预先绑定到函数参数列表的参数。调用bind函数之后的返回值是:一个原函数的拷贝,并拥有指定的thsi值和初始参数。请参考MDN解释。
var add = function (x,y) {
return x *this.m + y * this.n;
};
var obj = {
m: 1,
n: 2,
};
var newAdd = add.bind(obj, 5);
newAdd(5);//15
var m = 3;
var n = 3;
var newAdds = add.bind(null, 5);
newAdds(5);//30
var newAddp = add.bind(undefined, 5);
newAddp(5);//30
如果bind函数的第一个参数是null或者undefined,和call,apply的规则是一样的,相当于将this绑定到了全局对象。
bind函数使用注意事项:
(1)bind用于事件监听
document.getElementById('button').addEventListener('click', o.m.bind(o));
document.getElementById('button').removeEventListener('click', o.m.bind(o));
上面给按钮绑定一个click事件的时候使用bind方法生成一个匿名函数,导致无法取消绑定,因此写法有误,正确的写法应该是:
var listener = o.m.bind(o);
document.getElementById('button').addEventListener('click', listener);
document.getElementById('button').removeEventListener('click', listener);
(2)bind函数与回调函数结合使用
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
conso.log(this === counter);
}
};
var callIt = function (callback) {
callback();
};
callIt(counter.inc.bind(counter));
console.log(counter.count);//1,true
上面callIt函数内部调用回调函数时,如果直接传入counter.inc,则counter.inc内部的this会指向全局对象,但是用bind方法绑定之后,counter.inc中的this总是指向counter。
(3)数组的forEach中的使用
var obj = {
name: 'john',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
console.log(this === window);
})
},
}
obj.print();
//true
//true
//true
上面代码中,obj.print内部this.times的this是指向obj的,这个没有问题。但是,forEach方法的回调函数内部的this.name却是指向全局对象,导致没有办法取到值,要解决这个问题就要使用bind函数固定this。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
console.log(this === window);
}.bind(this))
}
obj.print();
//john
//false
...
(4)结合call方法的使用
[1,2,3].slice(0,1);//[1]
Array.prototype.slice.call([1, 2, 3], 0, 1);//[1]
上面的代码中,数组的slice方法从数组按照指定位置和长度切分出另一个数组。这样做的本质是在数组上面调用Array.prototype.slice方法,因此可以用call方法表达这个过程。这里可以理解为call方法内部的this指向slice。
call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写。
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0,1);//[1]
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1,2,3];
push(4);
console.log(a);//[1,2,3,4]
a.pop();//4
上面代码的含义就是,将Array.prototype.slice变成Function.prototype.call方法所在的对象,调用时就变成了Array.prototype.slice.call,其他的两个push,pop也一样。如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以改写。
function f() {
console.log(this.v);
};
var obj = {
v: 123
};
f.bind(obj)();
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f,obj)();//123
上面代码的含义就是,将Function.prototype.bind方法绑定在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函数实例上使用。
总结