js apply方法详解
从开始接触js,到前端工作了很久后,javascript函数中this,apply,call这三个方法,非常头痛,非常模糊,看也看不懂,所以2019年后树立总结问题后一定要攻克掌握他们,最近在网上看了一些文章对apply方法和call的一些示例,总算是有些眉目了,做了一下梳理,与君分享。。。
主要解决一下几个问题
1、apply和call的区别在哪里?
2、什么情况下用apply,什么情况下用call
3、apply的其他巧妙用法(一般在什么情况下可以使用apply)
4、bind绑定函数的使用
5、bind的偏函数、和setTimeout一起使用,绑定函数作为构造函数,捷径及实现
我首先从网上查到关于apply和call的定义,然后用示例来解释这个方法的意思和如何去使用
apply:方法能劫持另外一个对象的方法,继承另外一个对象的属性
Function.apply(obj,args),方法能接受两个参数:
obj:这个对象将代替Fcuntion类里this对象
args:这个是数组,它将作为参数传给Function (args —> arguments)
call和apply的意思是一样的,只不过传递的参数列表不一样而已
Function.call(this,param1,param2,param3)
obj:这个对象将代替Function类里this对象
params:这个是一个参数列表
apply和call都是为了改变某个函数运行时的上下文而存在的,换句话说,就是为了改变函数体内部this的指向,当一个obj没有某个方法,但是其他的有,我们可以借助call和apply用其他的方法来操作
apply示例:
<script type='text/javascript'>
/*定义一个人类*/
function Person(name,age){
this.name = name; this.grade = grade;
}
/*定义一个学生类*/
function Student(name,age,grade){
Person.apply(this,arguments); this.grade = grade;
}
/*创建一个学生类*/
var student = new Student('wang',21,'大三');
//测试
alert("name : " + student.name +"\n" + "age : "+student.age + "\n" + "grade : " + student.grade)
//可以看到测试结果是 name:qian age:21 grade:大三
//学生类里面我没有给name和age属性赋值,为什么又存在这两个属性的值呢,这个就是apply的神奇之处
</script>
分析:Person.apply(this,arguments);
this:在创建对象的时候代表student
arguments:是一个数组,也就是[‘wang’,21,‘大三’]
通俗一点讲就是:用student去执行Person这个类里面的内容,在Person这个类里面存在this.name等之类的语句,这样就将属性创建到student对象里面
2、call示例:
在student函数里面将apply中修改成如下:
Person.call(this,name,age);
这样就ok了,
3、什么情况下用apply,什么情况下用call
在给对象参数的情况下,如果参数的形式是数组的时候,比如apply示例里面传递了参数arguments,这个参数是数组类型,并且在调用Person的时候参数的列表是对应一致的(也就是Person和student的参数列表前两位是一致)就可以采用apply,如果我的Person的参数里列表是这样的(age,name),而Student的参数列表是(name,age,grade),这样就可以用call来实现,也就是直接指定参数里列表对应值的位置(person.call(this,age,name,grade))
4、apply的一些其他巧妙用法
细心的人可能已经察觉到,在我调用apply方法的时候,第一个参数是对象(this),第二个参数是一个数组集合,
在调用Person的时候,他需要的不是一个数组,但是为什么他给我一个数组我仍然可以将数组解析为一个一个的参数,这个就是apply的一个巧妙用处,可以将数组默认的转换为一个参数列表([param1,param2,param3]) 转换为param1,parame2,param3),这个如果让我们用程序来实现数组的每一项,来转换为参数的列表,可能都得费1️⃣会功夫,借助apply的这个特性就有了一下高效率的方法。
a) Max.max 可以实现得到数组中最大的一项
因为Math.max参数里面不支持Math.max([param1,param2]),也就是数组;
但是它支持Math.max(param1,param2),所以根据刚才apply的那个特点来解决
var max = Math.max.apply(null,array),这样轻易的可以得到一个数组中最大的一项,apply会将会将一个数组转换为一个参数接一个参数的传递给方法
这块在调用的时候第一个参数给了一个null,这个是因为没有对象去调用这个方法,我只需要用这个方法帮我运算,得到返回的结果就行了,所以直接传递一个null过去
b) Math.min可以实现得到数组中最小的一项
同样的和max是一个思想
var min = Math.min.apply(null,array)
c) Array.prototype.push可以实现两个数组的合并
同样push方法没有提供push一个数组,但它提供了push(param1,param2…paramN
,所以同样可以通过apply来转换一下这个数组,即:
var arr1 = new Array(“1”,“2”,“3”)
var arr2 = new Array(“4”,“5”,“6”)
Array.prototype.push.apply(arr1,arr2);
也可以这样理解,arr1调用了push方法,参数是通过apply将数组转换为参数列表的集合。
通常在什么情况下,可以使用apply类似Math.min等之类的特殊用法:
一般在目标函数只需要n个参数列表,而不是接受一个数组的形式,都可以使用apply来解决这个问题!
d) 验证是否是数组(前提是toString() 方法没有被重写过)
function isArray(obj){
return Object.prototype.toString.call(obj) === ‘[object Array]’
}
e) 类(伪)数组使用数组的方法
var domNode = Array.protype.slice.call(document.getElementByTagName(’*’));
javascript中存在一种名为伪数组的对象结构,比较特别的是arguments对象,还有像调动getElementByTagName,document.childNodes之类的他们返回NodeList对象都属于伪数组,不能应用Array下的push.pop等方法,但是我们能通过Array,prototype.slice.call转换为真正的数组的带有length属性的对象,这样domNOdes就可以应用Array下的所有方法
5、bind 绑定函数
bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值,常见错误就是上面的例子,将方法能够很漂亮的解决这个问题:
this.num = 9;
var mymodule = {
num: 81,
getNum: function() {
console.log(this.num);
}
};
mymodule.getNum(); // 81
var getNum = mymodule.getNum;
getNum(); // 9, 因为在这个例子中,"this"指向全局对象
var boundGetNum = getNum.bind(mymodule);
boundGetNum(); // 81
bind和call和apply一样都是可以改变this的指向
MDN的解释:bind()方法会创建一个新函数,称为绑定函数,当调动这个绑定函数的时候,绑定函数会以创建它时传入bind方法的第一个参数作为this,传入bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数
直接来看看具体如果使用,在常见的单体模式中,我们通常会使用_this,that,self 等保存this,这样我们可以在改变了上下文之后继续引用它,像这样:
var foo = {
bar:1
evendBind:function(){
var _this = this;
$('.someClass').on('click',function(event){
console.log(_this.bar) //1
})
}
}
由于javascript特有的机制,上下文环境在eventBind:function(){} 过渡到$(’.someClass’).on(‘click’,function(event){})发生改变,使用变量保存this这些方法方式都是有用的,也没有什么问题,当然使用bind()可以更加优雅的解决这个问题
var foo = {
bar : 1,
eventBind:function(){
$('.someClass').on('click',function(){
console.log(this.bar) //1
}.bind(this))
}
}
在上述代码里面,bind()创建了一个函数,当这个click事件绑定在被调用的时候,它的this关键词会被设置成被传入的值,因此,这里我们想要上下文this指的都是foo,到bind()函数中,然后,当回调函数被执行的时候,this便指向foo对象,再来一个简单的例子:
var bar = funcion(){
console.log(this.x)
}
var foo = {
x:3
}
bar() // undefind
var func = bar.bind(foo);
func() // 3
这里我们创建了一个新的函数func,当使用bind()创建一个绑定函数之后,他被执行的是偶,它的this会被设置成foo,而不是我们调用bar()时的全局作用域
6、偏函数(partial Function)
这是一个很好的特性,使用bind()我们设定函数的预定义参数,然后调用的时候传入其他参数即可:
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// 预定义参数37
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
7、和setTimeout一起使用
function Bloomer(){
this.petalCount = Math.ceil(Math.random() * 12) +1 ;
}
Bloomer.prototype.bloom = function(){
window.setTimeout(this.declare.bind(this),100)
}
Bloomer.prototype.declare = function(){
console.log('我有' + this.petalCount + '朵花瓣')
}
var bloo = new Bloomer();
bloo.bloom(); //我有五朵花瓣
注意:对于实践处理函数和setInterval方法也是可以使用上面的方法
8、绑定函数作为构造函数
绑定函数也适用于使用new操作符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
console.log(this.x + ',' + this.y);
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// 实现中的例子不支持,
// 原生bind支持:
var YAxisPoint = Point.bind(null, 0/*x*/);
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'
axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(17, 42) instanceof YAxisPoint; // true
9、捷径
bind()也可以为需要特定this值的函数创造捷径。
例如要将一个类数组对象转换为真正的数组,可能的例子如下:
var slice = Array.prototype.slice;
// ...
slice.call(arguments);
如果使用bind()的话,情况变得更简单:
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.call.bind(unboundSlice);
// ...
slice(arguments);
实现
上面的几个小节可以看出bind()有很多的使用场景,但是bind()函数是在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。这就需要我们自己实现bind()函数了。
首先我们可以通过给目标函数指定作用域来简单实现bind()方法:
Function.prototype.bind = function(context){
self = this; //保存this,即调用bind方法的目标函数
return function(){
return self.apply(context,arguments);
};
};
考虑到函数柯里化的情况,我们可以构建一个更加健壮的bind():
Function.prototype.bind = function(context){
var args = Array.prototype.slice.call(arguments, 1),
self = this;
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context,finalArgs);
};
};
这次的bind()方法可以绑定对象,也支持在绑定的时候传参。
继续,Javascript的函数还可以作为构造函数,那么绑定后的函数用这种方式调用时,情况就比较微妙了,需要涉及到原型链的传递:
Function.prototype.bind = function(context){
var args = Array.prototype.slice(arguments, 1),
F = function(){},
self = this,
bound = function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply((this instanceof F ? this : context), finalArgs);
};
F.prototype = self.prototype;
bound.prototype = new F();
return bound;
};
这是《JavaScript Web Application》一书中对bind()的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof,因此这是最严谨的bind()实现。
对于为了在浏览器中能支持bind()函数,只需要对上述函数稍微修改即可:
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(
this instanceof fNOP && oThis ? this : oThis || window,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?
答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。
apply、call、bind比较
那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:
var obj = {
x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81
三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。
也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。
再总结一下:
apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
apply 、 call 、bind 三者都可以利用后续参数传参;
bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。