zoukankan      html  css  js  c++  java
  • 浅析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的来龙去脉说清楚了。

  • 相关阅读:
    python全栈开发day54-mysql库操作、表操作、数据类型、完整性约束
    CentOS7.5安装nodejs 转
    python全栈开发day53-mysql
    python全栈开发day52-bootstrap的运用
    python全栈开发day51-jquery插件、@media媒体查询、移动端单位、Bootstrap框架
    机器学习之交叉验证和网格搜索
    机器学习之混淆矩阵
    机器学习之朴素贝叶斯算法
    机器学习之TF-IDF
    机器学习之K-近邻算法
  • 原文地址:https://www.cnblogs.com/ldq678/p/9711494.html
Copyright © 2011-2022 走看看