浅析JavaScript中Function对象 之 详解call&apply
Posted ldq678
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析JavaScript中Function对象 之 详解call&apply相关的知识,希望对你有一定的参考价值。
函数是js中最复杂的一块内容,其中call() 和 apply()又是重灾区,初学者往往在这个坑里栽倒,这次来分析这2个函数对象的成员
一、函数的角色
在js的体系下,js有3种角色。分别是普通函数、构造器、对象。
1.普通函数
<script type="text/javascript"> function f1(){ console.log(‘这是个函数‘); } </script>
这里声明的f1,它的角色就是个普通函数
2.构造器
<script type="text/javascript"> function Person(name, age){ this.name = name; this.age = age; } var per = new Person(‘james‘, 28); </script>
这里声明的Person,虽然也是函数,但是在本例中它的角色就是构造器。
3.对象
<script type="text/javascript"> var f2 = function(){ console.log(‘这个匿名函数是被当对象使用的‘); }
f2(); </script>
这里的f2是引用类型变量,它指向的是一个函数对象,我们也直接称之为函数对象。
二、this之争
先来看一个例子
<script type="text/javascript"> function f1(){ console.log(this); } f1(); </script>
这个代码执行完之后会输出什么?
答案是:window,那么也就是说,在这个函数中,this是指向window对象的。
为什么呢?原因是,根据我们之前说过的作用域和作用域链的理论,这个f1是在全局作用域下声明的,那么,只要是在全局作用域下声明的变量,对象,函数,通通都被当成window对象的成员。换句话说,这里声明的f1函数,在调用的时候完全可以这么调用: window.f1(); 。我们一般在写程序的时候,往往为了代码简洁,就把window给省了。这一点跟我们直接使用document对象是一个道理。
下面我给上例简单做个变形:
<script type="text/javascript"> function f1(){ "use strict";//使用js严格模式 console.log(this); } f1();//输出undefined </script>
这次程序运行的结果是:undefined。什么意思?意思就是,这次运行之后,f1中this是undefined,而不是上例中的window了。
我们说了,谁是函数的调用者,那么函数中的this就是指向谁,在正常模式下,直接调用f1()和通过window.f1()调用是完全等价的,所以正常模式下看到f1()执行时,它内部的this是指向window对象的。而当我们使用严格模式时,f1()调用时,函数名前面是空的,也就被当作”无主之函数“来调用了。就是没人调用你,那this自然就是undefined。
两者的区别就是,这次在f1中多了一个“use strict";这样一条语句,这条语句是声明,使用js严格模式来解析代码。
那么在这里就简单说下严格模式这个概念。
js有两种运行模式,分别是正常模式和严格模式。我们平时使用的都是正常模式。如果在代码前加一条“use strict";语句,那么其后的代码将以严格模式执行。
设立"严格模式"的目的,主要有以下几个:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
我们从以上代码中,可以得出一个结论就是,在一个函数中,this这个关键字的指向是可以改变的。
三、调用函数的另一种形式
函数声明了以后,可以像传统方法一样调用,比如f1();但也可以使用另一种途径来调用。比如:
<script type="text/javascript"> function f1(){ console.log(this); } f1();//输出window f1.apply();//输出window f1.call();//输出window </script>
这里我们连续3次调用f1函数。第一次是普通的函数调用,第2次和第3次是把f1当对象来使用的,然后调用了f1这个对象的apply方法和call方法。并且我们看到的输出结果都是一致的。
那么问题来了,这个apply()和call()从何而来?
简单,根据原型和原型链理论,如果自己没有定义,那么就一定是从原型链上拿过来的。我们知道所有的函数其实都是内置对象Function的实例,Function的原型对象,也就是Function.prototype上定义了apply()和call(),所以我们自定义的任何函数,都可以通过函数名打点的形式访问到这2个成员。当然此时是把函数以对象的角色来使用的。
其实,apply和call还能传递参数。我们先来个试试
<script type="text/javascript">
‘use strict‘; function f1(){ console.log(this); } f1();//输出undefined f1.apply();//输出undefined f1.call();//输出undefined </script>
把上边的代码稍作改变,使用严格模式,运行结果仍然为输出undefined,这跟上一个小节说的是一回事。
从上边来看,3次调用结果完全相同,我们可以得出一个重要结论:
call和apply就是用来执行这个函数的。(简直是废话)从这两个单词的字母意思来看也是如此,call就是呼叫,调用的意思,apply也是应用,使用的意思。
那为什么要使用apply和call呢?这两个成员的存在的意义何在?
四、call和apply的基本用法
前边讨论的都是不需要传递参数的函数。下面我们来考察需要传递参数的情况。然后再指出call和apply的真实用途。看下面的例子:
<script type="text/javascript"> function add(a, b){ console.log( (a + b) + " : " + this); } add(2,3); </script>
这里的add函数需要2个参数,正常调用我们都非常熟悉。如果想借助apply和call来调用,那么就必须做出改变,如果仅仅是
add.call(2,3);
add.apply(2,3);
这样调用会报错。
call和apply的函数签名是这样的:
call( thisArg [, arg1, arg2, arg3,.....]);
apply(thisArg [ [arg1, arg2, arg3, ...]]);
说明一下:
1.这两个函数的作用完全相同,就是在如果需要传递参数时,参数的形式有点区别。
2.两者第1个参数都是thisArg,顾名思义,要求你传一个对象,表示当函数调用时,要把这个对象当成函数内部的this所指对象。这一点很重要,我们后边会详细介绍
3.如果调用的函数本身需要传递参数,那么这些参数要放在call和apply实参的第2个位置开始传递。并且第一个thisArg必须传递,实在不需要传递,也可以给null。
4.如果被调用的函数就没有参数,那么通过call和apply来调用函数时,可以给thisArg传参,也可以不传,不传就是默认原始的this指向。
看了这么多都要懵了,我们看例子,然后来一一对照着说明。还是上边的函数
<script type="text/javascript"> function add(a, b){ console.log( (a + b) + " : " + this); } add.call(null,2,3); add.apply(null,[2,3]); </script>
输出结果是:,从结果我们可以分析出的结论是:
1.我们传递thisArg是null,但是并没有改变add函数内部的this指向,这时add函数内部,this指向仍然是window对象。
2.call传递参数,需要给离散的,单个的值,如果需要传多个,那么需要把多个值用逗号隔开。
3.apply传递参数,需要给数组,这里的[2,3],就是个数组。 这也是apply和call的唯一差别。其实功能是完全一样的。
接下里,我们再变一变。
<script type="text/javascript"> function add(a, b){ console.log( (a + b) + " : " + this); } var obj = {name:"james", age:18}; add.call(obj,2,3); add.apply(obj,[2,3]); </script>
这次我们声明了一个obj对象,然后让这个对象当做call和apply的thisArg参数传递过去。运行结果是:
我们可以看到这次add函数运行时this指向变了。变成了一个Object对象,这个Object对象就是我们传递过去的obj。现在大家应该清楚call和apply的第一个参数thisArg是干什么用了吧。
现在我们可以指出call()和apply()的定义:执行函数体,并试图改变(篡改)函数体中this关键字的指向。
如果还不明白,我们再看一个例子:
<script type="text/javascript"> function Person(name, age){ this.name = name; this.age = age; } Person.prototype.sayHi = function(){ console.log("你好:" + this.name); } var per = new Person(‘老李‘, 28); per.sayHi();// 输出 “你好:老李" function Student(name, age){ this.name = name; this.age = age; } var stu = new Student(‘老王‘, 18); per.sayHi.call(stu);//输出“你好:老王” per.sayHi.apply(stu);//输出“你好:老王” </script>
代码的注释已经说明了程序执行结果。我们只解释下。本来per调用sayHi(),是要输出this.name。正常调用时,this就是指向由Person这个构造函数实例化出来的这个per对象它自己,所以输出的就是per自己的name属性值:老李。
当使用call和apply时,我们给他传递的thisArg是stu对象,那么这个时候,在执行sayHi方法时,当运行到语句:console.log("你好:" + this.name)时,这个this就被替换成了stu对象,那么this.name当然也就是读取出了:老李 这个值。
从这个案例,我们可以得出以下2个结论:
1.如果call和apply拿来使用,那么十有八九就是用来改变函数内部this指向的。否则你直接正常调用好了,还整什么call和apply?多此一举
2.Student本身没有声明sayHi(),那么正常情况下,老李这个stu对象,正常情况下是不能打招呼的,但这里我们确实达到了让老李这个stu对象打招呼的目的。我们把这种用法称之为js方法借用。
怎么理解这个借用呢?首先借用还是要忠实的执行原来声明时定义的代码,只不过是替换掉this这个”主体“而已。打个比方,就相当于你有一张加油卡,你拿加油卡是给你的汽车加油,我没有加油卡,我想加油怎么办呢?我可以借你卡来一用,我借过来了,加油就是给我的车加油。当然不管是改变不改变this,都不能改变函数执行的代码逻辑,你拿了卡只能加油,我借过来卡也只能执行加油的操作,不可能拿了加油卡去银行取钱。
五、实际用途
call和apply的实际用途有2种:
1.方法借用
2.继承(其实也是借用)
继承的问题我们放在其他帖子中讨论,这里只给出2个方法借用的例子。
例1:求数组的最大值
方法1.自己写个函数,使用for循环遍历这个数组,这个方法谁都会,不赘述。
方法2.借用Math.max()。这个Math是系统内置对象,它给我们提供了很多数学运算的方法,比如Math.max()就是求一组数的最大值。但是这个max函数不支持传递数组,只支持传递单个单个的离散数据。所以,如果想传递一个数组给它,会报错的。好了,虽然不能直接调用,但是我们可以借用。
<script type="text/javascript"> var ary = [22, 334, 33, 21, 83]; var max = Math.max.apply(null, ary); console.log(max); </script>
这里需要解释下为什么这么调用,为什么用apply,又为什么要给他传一个null。
1.Math.max()的签名是:Math.max(arg1, arg2, arg3...);
2.我们这里不需要改变this值,只需要通过方法借用,把数组ary里的各个项,当做max函数要的离散的参数传给它,借用一下即可。
所以,我们给apply方法传递的thisArg参数为null,因为我们无意改变max函数在执行时的this指向。再多说一句,由于max函数在内部执行的时候,根本就没使用到this这个关键字,所以,这个时候你给thisArg传什么都无所谓,包括把ary自己传过去也行。如下: var max = Math.max.apply(ary, ary); 。
为什么选用apply而不用call,因为我们这ary是一个数组,apply接受的参数正好是个数组,而call只接收离散的单个单个的参数,所以本例中只能选用apply来实现借用。
例2.把伪数组转化为数组。
方法1.自己写一个函数,通过for循环遍历搞定。
方法2.借用Array对象的slice()函数
<script type="text/javascript"> var wsz = {0:‘james‘, 1:‘terry‘, 2:‘jerry‘, 3:‘tod‘, length:4}; var ary1 = Array.prototype.slice.call(wsz); var ary2 = Array.prototype.slice.apply(wsz); console.log(ary1); console.log(ary2); </script>
输出是:
这个例子中,使用apply和call是一样的效果。下面来做解释说明:
1.slice()函数作用是从目标函数中截取连续的一部分元素形成一个新的数组,并返回这个新数组。其签名是:Array.prototype.slice(start, end)。如果只传1个参数,将以这个参数为起始点,开始截取目标数组中的元素直到末尾;如果不传参数,返回的新数组将与老数组完全相同
2.我们现在的例子,就是想让伪数组wsz中每一个键值对(length除外)都转化成数组的一个项,而不是某一部分,所以我们不需要给slice传递start参数和end参数。所以我们只需要传递给call或者apply那个thisArg参数。我们这里必须把待转化的对象wsz作为thisArg,因为在slice方法内部,是通过this这个关键字来遍历数组成员的。所以这时候要借用slice,就必须让slice函数的代码在执行时,this必须指向当前待转化的对象wsz。所以这里,我们传递给call和apply的参数wsz,是传递给thisArg的。
3.如果你真的是要把伪数组wsz中的某几个连续的成员转化成数组,那么就必须传start和end参数了。形如:
var ary1 = Array.prototype.slice.call(wsz, 1, 3); var ary2 = Array.prototype.slice.apply(wsz, [1, 3]);
这里我们应该看到,call传递的是单个的离散的值,apply就要传递一个数组。
4.其实还可以这么写 var ary1 = [].slice.call(wsz, 1, 3); 其中[]就是一个空数组对象。由于slice方法是定义在Array的原型对象中的,所以,所有的数组实例对象都能通过原型链访问到slice方法,而且这种写法更简单。我们在开发中往往使用这种更简化的写法。大家要看得懂。
好了,到这里我就把call和apply的来龙去脉说清楚了。