浅析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的来龙去脉说清楚了。



以上是关于浅析JavaScript中Function对象 之 详解call&apply的主要内容,如果未能解决你的问题,请参考以下文章

Javascript自执行匿名函数(function() { })()的原理浅析

JavaScript arguments对象浅析

JavaScript (JS) 面向对象编程 浅析 (含对象函数原型链解析)

浅析 var that = this;

浅析JavaScript访问对象属性和方法及区别

JavaScript浅析JavaScript对象如何添加属性和方法